Skip to content

Commit fcf0125

Browse files
jhaygood86eXpl0it3r
authored andcommitted
Add HtmlRenderer.WinUI adapter project for WinUI 3 / Win2D
New adapter beside HtmlRenderer.WinForms and HtmlRenderer.WPF, targeting WinUI 3 (Windows App SDK) via Win2D for 2D rendering. Implements the full RAdapter/RGraphics/RFont/RImage/RBrush/RPen/RGraphicsPath/RControl/ RContextMenu adapter surface, an HtmlControl/HtmlPanel/HtmlLabel/HtmlRender control layer mirroring the WPF adapter's shape, dark-mode support via UISettings, CF_HTML clipboard copy/paste, and a minimal demo app with the same sample-browser experience as the WinForms/WPF demos. Solution/csproj wiring: new HtmlRenderer.WinUI and HtmlRenderer.Demo.WinUI projects added to HtmlRenderer.sln; InternalsVisibleTo added to the core HtmlRenderer.csproj for the new adapter's test-seam access.
1 parent 2da9762 commit fcf0125

28 files changed

Lines changed: 4094 additions & 0 deletions

‎Source/Demo/WinUI/App.xaml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Application
2+
x:Class="TheArtOfDev.HtmlRenderer.Demo.WinUI.App"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
5+
6+
<Application.Resources>
7+
<ResourceDictionary>
8+
<ResourceDictionary.MergedDictionaries>
9+
<!-- Supplies the shared theme resources (e.g. TabViewButtonBackground) that TreeView's
10+
own default style depends on - without this, any non-trivial control throws a
11+
XamlParseException at InitializeComponent time because those keys don't exist. -->
12+
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
13+
</ResourceDictionary.MergedDictionaries>
14+
</ResourceDictionary>
15+
</Application.Resources>
16+
</Application>

‎Source/Demo/WinUI/App.xaml.cs‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
using Microsoft.UI.Xaml;
14+
15+
namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
16+
{
17+
/// <summary>
18+
/// Minimal application entry point - hosts one <see cref="MainWindow"/> with a single
19+
/// <see cref="TheArtOfDev.HtmlRenderer.WinUI.HtmlPanel"/> rendering a hardcoded sample document, just
20+
/// enough to prove the WinUI 3 control renders real HTML end to end.
21+
/// </summary>
22+
public partial class App : Application
23+
{
24+
private Window _window;
25+
26+
public App()
27+
{
28+
InitializeComponent();
29+
}
30+
31+
protected override void OnLaunched(LaunchActivatedEventArgs args)
32+
{
33+
_window = new MainWindow();
34+
_window.Activate();
35+
}
36+
}
37+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
4+
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
5+
<OutputType>WinExe</OutputType>
6+
<RootNamespace>TheArtOfDev.HtmlRenderer.Demo.WinUI</RootNamespace>
7+
<AssemblyName>HtmlRendererWinUIDemo</AssemblyName>
8+
<UseWinUI>true</UseWinUI>
9+
<EnableWindowsTargeting>true</EnableWindowsTargeting>
10+
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
11+
<!-- Unpackaged WinUI 3 app: WindowsPackageType=None + a self-contained deploy (both the .NET runtime
12+
and the Windows App SDK runtime), since an unpackaged app has no MSIX identity to resolve a
13+
framework-package dependency through - a genuinely new deployment concern the WPF/WinForms demos
14+
don't have (see the plan's Decisions section). -->
15+
<WindowsPackageType>None</WindowsPackageType>
16+
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
17+
<SelfContained>true</SelfContained>
18+
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
19+
<Platforms>x64</Platforms>
20+
</PropertyGroup>
21+
<PropertyGroup>
22+
<ApplicationManifest>app.manifest</ApplicationManifest>
23+
</PropertyGroup>
24+
<ItemGroup>
25+
<ProjectReference Include="..\..\HtmlRenderer.WinUI\HtmlRenderer.WinUI.csproj" />
26+
<ProjectReference Include="..\..\HtmlRenderer\HtmlRenderer.csproj" />
27+
<ProjectReference Include="..\Common\HtmlRenderer.Demo.Common.csproj" />
28+
</ItemGroup>
29+
<ItemGroup>
30+
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.4.0" />
31+
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.4.*" />
32+
</ItemGroup>
33+
</Project>

‎Source/Demo/WinUI/MainWindow.xaml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Window
2+
x:Class="TheArtOfDev.HtmlRenderer.Demo.WinUI.MainWindow"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:winui="using:TheArtOfDev.HtmlRenderer.WinUI">
6+
7+
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
8+
<Grid.ColumnDefinitions>
9+
<ColumnDefinition Width="280"/>
10+
<ColumnDefinition Width="*"/>
11+
</Grid.ColumnDefinitions>
12+
13+
<TreeView x:Name="SamplesTreeView"
14+
Grid.Column="0"
15+
SelectionMode="Single"
16+
SelectionChanged="OnSamplesTreeViewSelectionChanged"/>
17+
18+
<winui:HtmlPanel x:Name="RendererPanel" Grid.Column="1" />
19+
</Grid>
20+
</Window>
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
using System;
14+
using System.Collections.Generic;
15+
using System.Diagnostics;
16+
using System.IO;
17+
using System.Threading.Tasks;
18+
using Microsoft.Graphics.Canvas;
19+
using Microsoft.UI.Xaml;
20+
using Microsoft.UI.Xaml.Controls;
21+
using TheArtOfDev.HtmlRenderer.Core.Entities;
22+
using TheArtOfDev.HtmlRenderer.Demo.Common;
23+
using Windows.Storage.Streams;
24+
25+
namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
26+
{
27+
/// <summary>
28+
/// Hosts a <see cref="TreeView"/> of the same showcase/test/performance samples the WPF and WinForms
29+
/// demos expose (via the shared, UI-framework-agnostic <see cref="SamplesLoader"/>/<see cref="HtmlSample"/>
30+
/// from <c>HtmlRenderer.Demo.Common</c>) next to a <see cref="TheArtOfDev.HtmlRenderer.WinUI.HtmlPanel"/>
31+
/// that renders whichever sample is selected.
32+
/// </summary>
33+
/// <remarks>
34+
/// Node content is a plain <see cref="string"/> (the sample/category name), matching Microsoft's own
35+
/// documented <c>RootNodes</c> pattern exactly (see
36+
/// https://learn.microsoft.com/en-us/windows/apps/develop/ui/controls/tree-view) - no
37+
/// <see cref="TreeView.ItemTemplate"/> is needed for that case, since a string displays directly. An
38+
/// earlier version wrapped each node's content in a custom object and bound it via
39+
/// <c>TreeView.ItemTemplate</c>'s <c>{Binding Name}</c>, which rendered no text and never expanded -
40+
/// switched to this simpler, documented shape instead of chasing the exact binding-context rules for
41+
/// that pattern. <see cref="_nodeToSample"/> maps each leaf node back to its <see cref="HtmlSample"/>
42+
/// since the node's own displayed Content is just its name.
43+
/// </remarks>
44+
public sealed partial class MainWindow : Window
45+
{
46+
private readonly Dictionary<TreeViewNode, HtmlSample> _nodeToSample = new Dictionary<TreeViewNode, HtmlSample>();
47+
48+
public MainWindow()
49+
{
50+
InitializeComponent();
51+
52+
Title = "HTML Renderer - WinUI 3 Demo";
53+
54+
SamplesLoader.Init("WinUI", typeof(TheArtOfDev.HtmlRenderer.WinUI.HtmlRender).Assembly.GetName().Version.ToString());
55+
56+
// Without this, showcase samples' <link rel="Stylesheet" href="StyleSheet"> never resolves, so
57+
// e.g. Intro.htm's `.whitehole { background-color:white; border-radius:10px }` class is simply
58+
// never applied at all - not a paint bug, the rule itself never loads. DemoUtils.OnStylesheetLoad
59+
// is the same platform-agnostic handler the WPF/WinForms demos use.
60+
RendererPanel.StylesheetLoad += DemoUtils.OnStylesheetLoad;
61+
62+
// Same idea as StylesheetLoad above: samples reference icons by logical key (e.g.
63+
// src="HtmlIcon") that DemoUtils.GetImageStream resolves to an embedded resource - without
64+
// this, every such <img> falls through to the built-in "failed to load" icon instead.
65+
RendererPanel.ImageLoad += OnImageLoad;
66+
67+
LoadSamples();
68+
}
69+
70+
private void LoadSamples()
71+
{
72+
var showcaseRoot = new TreeViewNode { Content = "HTML Renderer" };
73+
SamplesTreeView.RootNodes.Add(showcaseRoot);
74+
foreach (var sample in SamplesLoader.ShowcaseSamples)
75+
AddSampleNode(showcaseRoot, sample);
76+
77+
var testSamplesRoot = new TreeViewNode { Content = "Test Samples" };
78+
SamplesTreeView.RootNodes.Add(testSamplesRoot);
79+
foreach (var sample in SamplesLoader.TestSamples)
80+
AddSampleNode(testSamplesRoot, sample);
81+
82+
if (SamplesLoader.PerformanceSamples.Count > 0)
83+
{
84+
var perfSamplesRoot = new TreeViewNode { Content = "Performance Samples" };
85+
SamplesTreeView.RootNodes.Add(perfSamplesRoot);
86+
foreach (var sample in SamplesLoader.PerformanceSamples)
87+
AddSampleNode(perfSamplesRoot, sample);
88+
}
89+
90+
showcaseRoot.IsExpanded = true;
91+
if (showcaseRoot.Children.Count > 0)
92+
{
93+
var firstNode = showcaseRoot.Children[0];
94+
SamplesTreeView.SelectedNode = firstNode;
95+
ShowSample(_nodeToSample[firstNode]);
96+
}
97+
}
98+
99+
private void AddSampleNode(TreeViewNode root, HtmlSample sample)
100+
{
101+
var node = new TreeViewNode { Content = sample.Name };
102+
_nodeToSample[node] = sample;
103+
root.Children.Add(node);
104+
}
105+
106+
private void OnSamplesTreeViewSelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
107+
{
108+
foreach (var node in sender.SelectedNodes)
109+
{
110+
if (_nodeToSample.TryGetValue(node, out var sample))
111+
{
112+
ShowSample(sample);
113+
break;
114+
}
115+
}
116+
}
117+
118+
private void ShowSample(HtmlSample sample)
119+
{
120+
try
121+
{
122+
RendererPanel.AvoidImagesLateLoading = !sample.FullName.Contains("Many images");
123+
RendererPanel.Text = sample.Html;
124+
}
125+
catch (Exception ex)
126+
{
127+
Debug.WriteLine(ex);
128+
}
129+
}
130+
131+
/// <summary>
132+
/// Resolves the demo's logical icon keys (e.g. <c>src="HtmlIcon"</c>) via
133+
/// <see cref="DemoUtils.GetImageStream"/> - real URLs/paths DemoUtils doesn't recognize fall
134+
/// through untouched (<see cref="HtmlImageLoadEventArgs.Handled"/> left false) so the default
135+
/// loader still handles them.
136+
/// </summary>
137+
private void OnImageLoad(object sender, HtmlImageLoadEventArgs e)
138+
{
139+
var stream = DemoUtils.GetImageStream(e.Src);
140+
if (stream == null)
141+
return;
142+
143+
e.Handled = true;
144+
_ = LoadIconAsync(e, stream);
145+
}
146+
147+
private static async Task LoadIconAsync(HtmlImageLoadEventArgs e, Stream stream)
148+
{
149+
try
150+
{
151+
byte[] bytes;
152+
using (stream)
153+
using (var memory = new MemoryStream())
154+
{
155+
await stream.CopyToAsync(memory).ConfigureAwait(false);
156+
bytes = memory.ToArray();
157+
}
158+
159+
using var randomAccessStream = new InMemoryRandomAccessStream();
160+
using (var writer = new DataWriter(randomAccessStream.GetOutputStreamAt(0)))
161+
{
162+
writer.WriteBytes(bytes);
163+
await writer.StoreAsync();
164+
await writer.FlushAsync();
165+
writer.DetachStream();
166+
}
167+
randomAccessStream.Seek(0);
168+
169+
var bitmap = await CanvasBitmap.LoadAsync(CanvasDevice.GetSharedDevice(), randomAccessStream);
170+
e.Callback(bitmap);
171+
}
172+
catch (Exception ex)
173+
{
174+
Debug.WriteLine(ex);
175+
e.Callback();
176+
}
177+
}
178+
}
179+
}

‎Source/Demo/WinUI/app.manifest‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
3+
<assemblyIdentity version="1.0.0.0" name="HtmlRenderer.Demo.WinUI.app"/>
4+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
5+
<security>
6+
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
7+
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
8+
</requestedPrivileges>
9+
</security>
10+
</trustInfo>
11+
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
12+
<application>
13+
<!-- Windows Vista -->
14+
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
15+
<!-- Windows 7 -->
16+
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
17+
<!-- Windows 8 -->
18+
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
19+
<!-- Windows 8.1 -->
20+
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
21+
<!-- Windows 10/11 -->
22+
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
23+
</application>
24+
</compatibility>
25+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
26+
<windowsSettings>
27+
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
28+
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
29+
</windowsSettings>
30+
</application>
31+
</assembly>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// "Therefore those skilled at the unorthodox
2+
// are infinite as heaven and earth,
3+
// inexhaustible as the great rivers.
4+
// When they come to an end,
5+
// they begin again,
6+
// like the days and months;
7+
// they die and are reborn,
8+
// like the four seasons."
9+
//
10+
// - Sun Tsu,
11+
// "The Art of War"
12+
13+
using Microsoft.Graphics.Canvas.Brushes;
14+
using TheArtOfDev.HtmlRenderer.Adapters;
15+
16+
namespace TheArtOfDev.HtmlRenderer.WinUI.Adapters
17+
{
18+
/// <summary>
19+
/// Adapter for Win2D brushes.
20+
/// </summary>
21+
internal sealed class BrushAdapter : RBrush
22+
{
23+
/// <summary>
24+
/// The actual Win2D brush instance.
25+
/// </summary>
26+
private readonly ICanvasBrush _brush;
27+
28+
/// <summary>
29+
/// Init.
30+
/// </summary>
31+
public BrushAdapter(ICanvasBrush brush)
32+
{
33+
_brush = brush;
34+
}
35+
36+
/// <summary>
37+
/// The actual Win2D brush instance.
38+
/// </summary>
39+
public ICanvasBrush Brush
40+
{
41+
get { return _brush; }
42+
}
43+
44+
/// <summary>
45+
/// No-op, matching <c>HtmlRenderer.WPF</c>'s own <c>BrushAdapter.Dispose</c> exactly.
46+
/// </summary>
47+
/// <remarks>
48+
/// <see cref="Core.Dom.CssBox.PaintBackground"/> calls <c>brush.Dispose()</c> unconditionally
49+
/// after every background fill, including for solid-color brushes obtained from
50+
/// <see cref="TheArtOfDev.HtmlRenderer.Adapters.RAdapter.GetSolidBrush"/> - which is a
51+
/// process-wide singleton CACHE (see that method's own doc comment: "cached... instance"),
52+
/// reused for the lifetime of the adapter, not a fresh per-call resource. WPF's <c>Brush</c>
53+
/// needs no real disposal, so that unconditional call is harmless there - it's the reason this
54+
/// pattern was never caught before. Win2D's <see cref="ICanvasBrush"/> is a real unmanaged/GPU
55+
/// resource: actually disposing it here permanently kills the shared cached brush for every
56+
/// color after its first use, so the SECOND time any element needs (for example) a white
57+
/// background, the disposed brush silently paints nothing - confirmed by reproducing exactly
58+
/// this symptom (background-color:white showing the page's own gradient background through
59+
/// instead) via two consecutive real <see cref="HtmlContainer.PerformPaint"/> calls against the
60+
/// same document. Not disposing here leaks gradient/texture brushes (the one kind of
61+
/// <see cref="RBrush"/> that genuinely is fresh-per-call, from
62+
/// <see cref="WinUIAdapter.CreateLinearGradientBrush"/>/<see cref="GraphicsAdapter.GetTextureBrush"/>)
63+
/// - accepted deliberately, mirroring WPF's identical no-op for its own gradient/texture
64+
/// <c>Brush</c> objects, since the alternative (silently breaking every cached solid-color fill
65+
/// after its first paint) is far worse than a small, bounded resource lifetime extension.
66+
/// </remarks>
67+
public override void Dispose()
68+
{
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)