Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,23 @@ jobs:
- name: Verify SDK Installation
run: dotnet --info

- name: Select build target
id: build-target
run: |
# HtmlRenderer.WinUI/HtmlRenderer.Demo.WinUI (Windows App SDK/Win2D, MSIX PRI generation via
# native makepri.exe/makeappx.exe) can only build on Windows - unlike the WinForms/WPF adapters,
# which cross-compile cleanly everywhere via EnableWindowsTargeting's reference-assembly story,
# there is no non-Windows equivalent for those native tools at all. Non-Windows runners build a
# solution filter excluding just those two projects; everything else still builds and tests
# identically on every OS.
$target = $IsWindows ? "Source/HtmlRenderer.sln" : "Source/HtmlRenderer.CrossPlatform.slnf"
echo "path=$target" >> $env:GITHUB_OUTPUT

- name: Restore Dependencies
run: dotnet restore Source/HtmlRenderer.sln
run: dotnet restore ${{ steps.build-target.outputs.path }}

- name: Build
run: dotnet build Source/HtmlRenderer.sln --configuration Release --no-restore
run: dotnet build ${{ steps.build-target.outputs.path }} --configuration Release --no-restore

- name: Run Tests
if: runner.os == 'Windows'
Expand Down
16 changes: 16 additions & 0 deletions Source/Demo/WinUI/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Application
x:Class="TheArtOfDev.HtmlRenderer.Demo.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Supplies the shared theme resources (e.g. TabViewButtonBackground) that TreeView's
own default style depends on - without this, any non-trivial control throws a
XamlParseException at InitializeComponent time because those keys don't exist. -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
37 changes: 37 additions & 0 deletions Source/Demo/WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

using Microsoft.UI.Xaml;

namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
{
/// <summary>
/// Minimal application entry point - hosts one <see cref="MainWindow"/> with a single
/// <see cref="TheArtOfDev.HtmlRenderer.WinUI.HtmlPanel"/> rendering a hardcoded sample document, just
/// enough to prove the WinUI 3 control renders real HTML end to end.
/// </summary>
public partial class App : Application
{
private Window _window;

public App()
{
InitializeComponent();
}

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
_window = new MainWindow();
_window.Activate();
}
}
}
33 changes: 33 additions & 0 deletions Source/Demo/WinUI/HtmlRenderer.Demo.WinUI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<OutputType>WinExe</OutputType>
<RootNamespace>TheArtOfDev.HtmlRenderer.Demo.WinUI</RootNamespace>
<AssemblyName>HtmlRendererWinUIDemo</AssemblyName>
<UseWinUI>true</UseWinUI>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
<!-- Unpackaged WinUI 3 app: WindowsPackageType=None + a self-contained deploy (both the .NET runtime
and the Windows App SDK runtime), since an unpackaged app has no MSIX identity to resolve a
framework-package dependency through - a genuinely new deployment concern the WPF/WinForms demos
don't have (see the plan's Decisions section). -->
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Platforms>x64</Platforms>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\HtmlRenderer.WinUI\HtmlRenderer.WinUI.csproj" />
<ProjectReference Include="..\..\HtmlRenderer\HtmlRenderer.csproj" />
<ProjectReference Include="..\Common\HtmlRenderer.Demo.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.4.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.*" />
</ItemGroup>
</Project>
20 changes: 20 additions & 0 deletions Source/Demo/WinUI/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Window
x:Class="TheArtOfDev.HtmlRenderer.Demo.WinUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:winui="using:TheArtOfDev.HtmlRenderer.WinUI">

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="280"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<TreeView x:Name="SamplesTreeView"
Grid.Column="0"
SelectionMode="Single"
SelectionChanged="OnSamplesTreeViewSelectionChanged"/>

<winui:HtmlPanel x:Name="RendererPanel" Grid.Column="1" />
</Grid>
</Window>
179 changes: 179 additions & 0 deletions Source/Demo/WinUI/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Graphics.Canvas;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.Demo.Common;
using Windows.Storage.Streams;

namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
{
/// <summary>
/// Hosts a <see cref="TreeView"/> of the same showcase/test/performance samples the WPF and WinForms
/// demos expose (via the shared, UI-framework-agnostic <see cref="SamplesLoader"/>/<see cref="HtmlSample"/>
/// from <c>HtmlRenderer.Demo.Common</c>) next to a <see cref="TheArtOfDev.HtmlRenderer.WinUI.HtmlPanel"/>
/// that renders whichever sample is selected.
/// </summary>
/// <remarks>
/// Node content is a plain <see cref="string"/> (the sample/category name), matching Microsoft's own
/// documented <c>RootNodes</c> pattern exactly (see
/// https://learn.microsoft.com/en-us/windows/apps/develop/ui/controls/tree-view) - no
/// <see cref="TreeView.ItemTemplate"/> is needed for that case, since a string displays directly. An
/// earlier version wrapped each node's content in a custom object and bound it via
/// <c>TreeView.ItemTemplate</c>'s <c>{Binding Name}</c>, which rendered no text and never expanded -
/// switched to this simpler, documented shape instead of chasing the exact binding-context rules for
/// that pattern. <see cref="_nodeToSample"/> maps each leaf node back to its <see cref="HtmlSample"/>
/// since the node's own displayed Content is just its name.
/// </remarks>
public sealed partial class MainWindow : Window
{
private readonly Dictionary<TreeViewNode, HtmlSample> _nodeToSample = new Dictionary<TreeViewNode, HtmlSample>();

public MainWindow()
{
InitializeComponent();

Title = "HTML Renderer - WinUI 3 Demo";

SamplesLoader.Init("WinUI", typeof(TheArtOfDev.HtmlRenderer.WinUI.HtmlRender).Assembly.GetName().Version.ToString());

// Without this, showcase samples' <link rel="Stylesheet" href="StyleSheet"> never resolves, so
// e.g. Intro.htm's `.whitehole { background-color:white; border-radius:10px }` class is simply
// never applied at all - not a paint bug, the rule itself never loads. DemoUtils.OnStylesheetLoad
// is the same platform-agnostic handler the WPF/WinForms demos use.
RendererPanel.StylesheetLoad += DemoUtils.OnStylesheetLoad;

// Same idea as StylesheetLoad above: samples reference icons by logical key (e.g.
// src="HtmlIcon") that DemoUtils.GetImageStream resolves to an embedded resource - without
// this, every such <img> falls through to the built-in "failed to load" icon instead.
RendererPanel.ImageLoad += OnImageLoad;

LoadSamples();
}

private void LoadSamples()
{
var showcaseRoot = new TreeViewNode { Content = "HTML Renderer" };
SamplesTreeView.RootNodes.Add(showcaseRoot);
foreach (var sample in SamplesLoader.ShowcaseSamples)
AddSampleNode(showcaseRoot, sample);

var testSamplesRoot = new TreeViewNode { Content = "Test Samples" };
SamplesTreeView.RootNodes.Add(testSamplesRoot);
foreach (var sample in SamplesLoader.TestSamples)
AddSampleNode(testSamplesRoot, sample);

if (SamplesLoader.PerformanceSamples.Count > 0)
{
var perfSamplesRoot = new TreeViewNode { Content = "Performance Samples" };
SamplesTreeView.RootNodes.Add(perfSamplesRoot);
foreach (var sample in SamplesLoader.PerformanceSamples)
AddSampleNode(perfSamplesRoot, sample);
}

showcaseRoot.IsExpanded = true;
if (showcaseRoot.Children.Count > 0)
{
var firstNode = showcaseRoot.Children[0];
SamplesTreeView.SelectedNode = firstNode;
ShowSample(_nodeToSample[firstNode]);
}
}

private void AddSampleNode(TreeViewNode root, HtmlSample sample)
{
var node = new TreeViewNode { Content = sample.Name };
_nodeToSample[node] = sample;
root.Children.Add(node);
}

private void OnSamplesTreeViewSelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
{
foreach (var node in sender.SelectedNodes)
{
if (_nodeToSample.TryGetValue(node, out var sample))
{
ShowSample(sample);
break;
}
}
}

private void ShowSample(HtmlSample sample)
{
try
{
RendererPanel.AvoidImagesLateLoading = !sample.FullName.Contains("Many images");
RendererPanel.Text = sample.Html;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}

/// <summary>
/// Resolves the demo's logical icon keys (e.g. <c>src="HtmlIcon"</c>) via
/// <see cref="DemoUtils.GetImageStream"/> - real URLs/paths DemoUtils doesn't recognize fall
/// through untouched (<see cref="HtmlImageLoadEventArgs.Handled"/> left false) so the default
/// loader still handles them.
/// </summary>
private void OnImageLoad(object sender, HtmlImageLoadEventArgs e)
{
var stream = DemoUtils.GetImageStream(e.Src);
if (stream == null)
return;

e.Handled = true;
_ = LoadIconAsync(e, stream);
}

private static async Task LoadIconAsync(HtmlImageLoadEventArgs e, Stream stream)
{
try
{
byte[] bytes;
using (stream)
using (var memory = new MemoryStream())
{
await stream.CopyToAsync(memory).ConfigureAwait(false);
bytes = memory.ToArray();
}

using var randomAccessStream = new InMemoryRandomAccessStream();
using (var writer = new DataWriter(randomAccessStream.GetOutputStreamAt(0)))
{
writer.WriteBytes(bytes);
await writer.StoreAsync();
await writer.FlushAsync();
writer.DetachStream();
}
randomAccessStream.Seek(0);

var bitmap = await CanvasBitmap.LoadAsync(CanvasDevice.GetSharedDevice(), randomAccessStream);
e.Callback(bitmap);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
e.Callback();
}
}
}
}
31 changes: 31 additions & 0 deletions Source/Demo/WinUI/app.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="HtmlRenderer.Demo.WinUI.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10/11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>
17 changes: 17 additions & 0 deletions Source/HtmlRenderer.CrossPlatform.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"solution": {
"path": "HtmlRenderer.sln",
"projects": [
"Demo\\Common\\HtmlRenderer.Demo.Common.csproj",
"Demo\\WinForms\\HtmlRenderer.Demo.WinForms.csproj",
"Demo\\WPF\\HtmlRenderer.Demo.WPF.csproj",
"HtmlRenderer\\HtmlRenderer.csproj",
"HtmlRenderer.PdfSharp\\HtmlRenderer.PdfSharp.csproj",
"HtmlRenderer.WinForms\\HtmlRenderer.WinForms.csproj",
"HtmlRenderer.WPF\\HtmlRenderer.WPF.csproj",
"Test\\HtmlRenderer.IntegrationTest\\HtmlRenderer.IntegrationTest.csproj",
"Test\\HtmlRenderer.PdfSharp.Test\\HtmlRenderer.PdfSharp.Test.csproj",
"Test\\HtmlRenderer.Test\\HtmlRenderer.Test.csproj"
]
}
}
Loading
Loading