From d550621f358a2293300c2abcc5fba5d05ca7115b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?=
Date: Fri, 13 Nov 2026 16:09:41 +0100
Subject: [PATCH 1/3] Fix and clean up the WinUI adapter
- Make the controls focusable and keep the root ScrollViewer from taking the focus of the panel
- Raise the container events synchronously on the UI thread
- Use DoubleTapped and pass every press on to the core
- Pass the font file to DirectWrite, which only finds a family that isn't installed that way
- Take a font file in HtmlRender.AddFontFamily
- Load images without a UI thread continuation and save them asynchronously
- Implement drag and drop of the selection
- Measure text including the trailing whitespace
- Remove the generated comments
---
.../Adapters/BrushAdapter.cs | 21 +-
.../Adapters/ControlAdapter.cs | 16 +-
.../Adapters/FontAdapter.cs | 33 +-
.../Adapters/FontFamilyAdapter.cs | 37 +-
.../Adapters/GraphicsAdapter.cs | 59 +--
.../Adapters/ImageAdapter.cs | 3 +-
.../HtmlRenderer.WinUI/Adapters/PenAdapter.cs | 5 +-
.../Adapters/WinUIAdapter.cs | 183 +++----
Source/HtmlRenderer.WinUI/HtmlContainer.cs | 9 +-
Source/HtmlRenderer.WinUI/HtmlControl.cs | 467 ++++++++++--------
Source/HtmlRenderer.WinUI/HtmlPanel.cs | 27 +-
Source/HtmlRenderer.WinUI/HtmlRender.cs | 31 +-
.../Utilities/ClipboardHelper.cs | 54 +-
.../Utilities/WindowsTheme.cs | 8 -
14 files changed, 446 insertions(+), 507 deletions(-)
diff --git a/Source/HtmlRenderer.WinUI/Adapters/BrushAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/BrushAdapter.cs
index c000fd9ad..ab92f24e8 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/BrushAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/BrushAdapter.cs
@@ -42,27 +42,10 @@ public ICanvasBrush Brush
}
///
- /// No-op, matching HtmlRenderer.WPF's own BrushAdapter.Dispose exactly.
+ /// Does nothing, the core also disposes the cached solid brushes, which would break their next use.
///
///
- /// calls brush.Dispose() unconditionally
- /// after every background fill, including for solid-color brushes obtained from
- /// - which is a
- /// process-wide singleton CACHE (see that method's own doc comment: "cached... instance"),
- /// reused for the lifetime of the adapter, not a fresh per-call resource. WPF's Brush
- /// needs no real disposal, so that unconditional call is harmless there - it's the reason this
- /// pattern was never caught before. Win2D's is a real unmanaged/GPU
- /// resource: actually disposing it here permanently kills the shared cached brush for every
- /// color after its first use, so the SECOND time any element needs (for example) a white
- /// background, the disposed brush silently paints nothing - confirmed by reproducing exactly
- /// this symptom (background-color:white showing the page's own gradient background through
- /// instead) via two consecutive real calls against the
- /// same document. Not disposing here leaks gradient/texture brushes (the one kind of
- /// that genuinely is fresh-per-call, from
- /// /)
- /// - accepted deliberately, mirroring WPF's identical no-op for its own gradient/texture
- /// Brush objects, since the alternative (silently breaking every cached solid-color fill
- /// after its first paint) is far worse than a small, bounded resource lifetime extension.
+ /// Gradient and texture brushes aren't cached, they're left to the finalizer.
///
public override void Dispose()
{
diff --git a/Source/HtmlRenderer.WinUI/Adapters/ControlAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/ControlAdapter.cs
index 517c8f671..02a89b70f 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/ControlAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/ControlAdapter.cs
@@ -14,6 +14,7 @@
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
using TheArtOfDev.HtmlRenderer.Core.Utils;
+using TheArtOfDev.HtmlRenderer.WinUI.Utilities;
namespace TheArtOfDev.HtmlRenderer.WinUI.Adapters
{
@@ -21,11 +22,7 @@ namespace TheArtOfDev.HtmlRenderer.WinUI.Adapters
/// Adapter for WinUI Control for core.
///
///
- /// Unlike WPF's static queries, WinUI 3 has no queryable
- /// global mouse-button state - caches the last-known pressed/position state
- /// from its own PointerPressed/PointerReleased/PointerMoved handlers, and this
- /// adapter (constructed fresh per call, mirroring HtmlRenderer.WPF's own ControlAdapter
- /// usage pattern) just reads those cached fields back off it.
+ /// WinUI has no global mouse state to query, the pointer state is the last one has seen.
///
internal sealed class ControlAdapter : RControl
{
@@ -85,14 +82,7 @@ public override void SetCursorIBeam()
public override void DoDragDropCopy(object dragDropData)
{
- // WinUI 3 has no synchronous "start a drag now" API analogous to WPF's
- // System.Windows.DragDrop.DoDragDrop - a drag operation there is always initiated by the
- // framework via CanDrag/DragStarting on the source element in response to a user gesture, not
- // called imperatively from application code mid-selection. Wiring that up is a real, separate
- // piece of work (hooking DragStarting on the CanvasControl/UserControl and populating its
- // DataPackage from dragDropData) that is out of scope for this foundation skeleton - see the
- // implementation report's deviations section. Copy-to-clipboard (Ctrl+C) is unaffected, since
- // it goes through RAdapter.SetToClipboard, not this method.
+ (_control as HtmlControl)?.StartDragDrop((ClipboardHelper.Content)dragDropData);
}
public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth)
diff --git a/Source/HtmlRenderer.WinUI/Adapters/FontAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/FontAdapter.cs
index 609638968..0ad2ff07a 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/FontAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/FontAdapter.cs
@@ -22,24 +22,14 @@ namespace TheArtOfDev.HtmlRenderer.WinUI.Adapters
/// Adapter for Win2D font.
///
///
- /// Win2D/DirectWrite exposes no low-level per-glyph metrics table the way WPF's
- /// does (see the plan's Step 1 spike risk #2), so
- /// / can't be read directly off the font the way
- /// HtmlRenderer.WPF's own FontAdapter does (FontFamily.LineSpacing/
- /// Typeface.UnderlinePosition). Instead they're derived by laying out a representative probe
- /// string once at construction time and reading -
- /// gives the line height directly, but there is no underline-
- /// position metric at all on Win2D's public surface, so is a documented
- /// heuristic (see below), not a measured value.
+ /// The line metrics come from laying out a probe string, the underline position is approximated from them.
///
internal sealed class FontAdapter : RFont
{
#region Fields and Consts
///
- /// A representative probe string used to derive line metrics - includes an ascender-tall and a
- /// descender-tall glyph so the measured line height reflects a normal line of text, not just
- /// x-height characters.
+ /// Has an ascender and a descender, so the line height covers a normal line of text.
///
private const string ProbeString = "Ág";
@@ -49,13 +39,12 @@ internal sealed class FontAdapter : RFont
private readonly CanvasTextFormat _textFormat;
///
- /// the size of the font, in points (unscaled) - matches the unit
- /// receives it in, mirroring HtmlRenderer.WPF's own FontAdapter.Size.
+ /// the size of the font, in points.
///
private readonly double _size;
///
- /// Cached font height, in device-independent pixels (Win2D/WinUI's coordinate unit, same as WPF's).
+ /// Cached font height, in device-independent pixels.
///
private readonly double _height;
@@ -75,7 +64,7 @@ internal sealed class FontAdapter : RFont
/// Init.
///
/// the device to build the probe layout with
- /// the font family name (system-installed or an @font-face family registered with a live )
+ /// the font family name
/// the font size, in points
/// the font style
public FontAdapter(CanvasDevice device, string fontFamily, double size, RFontStyle style)
@@ -85,15 +74,11 @@ public FontAdapter(CanvasDevice device, string fontFamily, double size, RFontSty
_textFormat = new CanvasTextFormat
{
FontFamily = fontFamily,
- // 96/72 converts points -> device-independent pixels, matching WPF's own FontAdapter,
- // whose Height/UnderlineOffset bake in the same conversion factor and whose DrawString/
- // MeasureString multiply the raw font size by it at every use.
+ // Points to device-independent pixels
FontSize = (float)(96d / 72d * size),
FontWeight = (style & RFontStyle.Bold) == RFontStyle.Bold ? FontWeights.Bold : FontWeights.Normal,
FontStyle = (style & RFontStyle.Italic) == RFontStyle.Italic ? Windows.UI.Text.FontStyle.Italic : Windows.UI.Text.FontStyle.Normal,
- // The core engine performs its own line-breaking/word-wrap over runs it measures one at a
- // time (see RGraphics.MeasureString) - every layout built from this format, for measuring
- // or for drawing, is expected to be a single unwrapped line.
+ // The core breaks the lines itself
WordWrapping = CanvasWordWrapping.NoWrap,
};
@@ -104,9 +89,7 @@ public FontAdapter(CanvasDevice device, string fontFamily, double size, RFontSty
{
_height = lineMetrics[0].Height;
var baseline = lineMetrics[0].Baseline;
- // No underline-position metric is exposed by Win2D's public surface - approximate it
- // as a small step below the baseline, proportional to the line's descent region. This
- // is a heuristic, not a measured value; see this class's own remarks.
+ // A bit below the baseline, into the descent
_underlineOffset = baseline + (_height - baseline) * 0.15;
}
else
diff --git a/Source/HtmlRenderer.WinUI/Adapters/FontFamilyAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/FontFamilyAdapter.cs
index 797997a36..c7f7cbdb8 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/FontFamilyAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/FontFamilyAdapter.cs
@@ -10,7 +10,7 @@
// - Sun Tsu,
// "The Art of War"
-using Microsoft.Graphics.Canvas.Text;
+using System;
using TheArtOfDev.HtmlRenderer.Adapters;
namespace TheArtOfDev.HtmlRenderer.WinUI.Adapters
@@ -18,48 +18,39 @@ namespace TheArtOfDev.HtmlRenderer.WinUI.Adapters
///
/// Adapter for Win2D/DirectWrite font family object for core.
///
- ///
- /// Unlike WPF's , which is itself the object that must
- /// stay alive to keep a loaded font usable, Win2D/DirectWrite resolves a
- /// string by name at layout-construction time. For an @font-face family that name only resolves
- /// while the backing that registered it is still alive - so this adapter
- /// holds a strong reference to that font set (null for ordinary system-installed families) purely to
- /// keep it from being garbage collected for as long as this handle is
- /// referenced (by 's own cache).
- ///
internal sealed class FontFamilyAdapter : RFontFamily
{
///
- /// the family name, as resolved by DirectWrite at text-layout time.
+ /// the family name.
///
private readonly string _name;
///
- /// Strong reference to the backing font set for an @font-face family, keeping it alive for
- /// as long as this handle is referenced. Null for ordinary system-installed families.
+ /// The font file of an @font-face or custom family, null for system families.
///
- private readonly CanvasFontSet _fontSet;
+ private readonly Uri _fontFile;
///
/// Init.
///
- public FontFamilyAdapter(string name, CanvasFontSet fontSet = null)
+ public FontFamilyAdapter(string name, Uri fontFile = null)
{
_name = name;
- _fontSet = fontSet;
+ _fontFile = fontFile;
}
- ///
- /// the backing font set for an @font-face family, or null for an ordinary system family.
- ///
- public CanvasFontSet FontSet
+ public override string Name
{
- get { return _fontSet; }
+ get { return _name; }
}
- public override string Name
+ ///
+ /// The family for , DirectWrite
+ /// only finds a family that isn't installed with the font file in front of the name.
+ ///
+ public string TextFormatFamily
{
- get { return _name; }
+ get { return _fontFile != null ? _fontFile.AbsoluteUri + "#" + _name : _name; }
}
}
}
diff --git a/Source/HtmlRenderer.WinUI/Adapters/GraphicsAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/GraphicsAdapter.cs
index b9fbf89f5..195da19c3 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/GraphicsAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/GraphicsAdapter.cs
@@ -33,16 +33,12 @@ internal sealed class GraphicsAdapter : RGraphics
#region Fields and Consts
///
- /// The wrapped Win2D drawing session - null for a measure-only instance (see the parameterless/
- /// device-only constructors), matching HtmlRenderer.WPF's own GraphicsAdapter's
- /// null-DrawingContext measure-only mode.
+ /// The Win2D drawing session, null for a measure-only instance.
///
private readonly CanvasDrawingSession _g;
///
- /// The device used to build measurement-only /
- /// instances - Win2D's text/geometry APIs need a live device even when there's no drawing session
- /// (unlike WPF's , which needs none).
+ /// Text layouts and geometries need a device, also when only measuring.
///
private readonly CanvasDevice _device;
@@ -52,17 +48,13 @@ internal sealed class GraphicsAdapter : RGraphics
private readonly bool _releaseGraphics;
///
- /// The stack of active Win2D clip layers, one pushed per /
- /// call - Win2D layers are and must be disposed in LIFO order, unlike
- /// WPF's , which needs no such bookkeeping.
+ /// The active clip layers, one per /, disposed in reverse order.
///
private readonly Stack _layerStack = new Stack();
///
- /// A single entry of : the pushed Win2D layer and, for
- /// , the geometry masking it. The mask has to outlive the layer - D2D
- /// reads it until the layer is popped - so the two are released together in .
- /// Both are null on a measure-only instance, which has no session to push a layer on.
+ /// A clip layer and the geometry masking it, D2D reads the mask until the layer is popped.
+ /// Both are null on a measure-only instance.
///
private readonly struct ClipLayer
{
@@ -135,9 +127,6 @@ public override void PopClip()
public override void PushClip(RRect rect)
{
_clipStack.Push(rect);
-
- // Measure-only instance - the clip is still tracked for layout, but there is no session to
- // push it onto (same null-session guard as SetAntiAliasSmoothingMode below).
_layerStack.Push(_g != null
? new ClipLayer(_g.CreateLayer(1f, Utils.Convert(rect)), null)
: new ClipLayer(null, null));
@@ -154,9 +143,6 @@ public override void PushClipExclude(RRect rect)
return;
}
- // full/excluded are only inputs to the combine, so they are released right away; combined is
- // handed to the layer and released with it. All three are unmanaged D2D resources created on
- // every clip-exclude of every paint, so none of them can be left to the finalizer.
CanvasGeometry combined;
using (var full = CanvasGeometry.CreateRectangle(_device, Utils.Convert(current)))
using (var excluded = CanvasGeometry.CreateRectangle(_device, Utils.Convert(rect)))
@@ -185,31 +171,9 @@ public override void ReturnPreviousSmoothingMode(object prevMode)
public override RSize MeasureString(string str, RFont font)
{
- var fontAdapter = (FontAdapter)font;
- using (var layout = new CanvasTextLayout(_device, str, fontAdapter.TextFormat, float.MaxValue, float.MaxValue))
- {
- var width = layout.LayoutBounds.Width;
- if (width <= 0 && str.Length > 0)
- {
- // CanvasTextLayout.LayoutBounds is an ink-bounds rectangle that excludes trailing
- // whitespace advance entirely - a string made up only of whitespace (notably the single
- // space GetWhitespaceWidth measures, and cached forever once wrong) always comes back
- // as zero-width. Recover the true advance width via an append-a-sentinel-glyph trick:
- // lay out str+sentinel and sentinel alone: the difference is str's own advance width.
- width = MeasureAdvanceWidthViaSentinel(str, fontAdapter);
- }
- return new RSize(width, font.Height);
- }
- }
-
- private double MeasureAdvanceWidthViaSentinel(string str, FontAdapter fontAdapter)
- {
- const string sentinel = "|";
- using (var combined = new CanvasTextLayout(_device, str + sentinel, fontAdapter.TextFormat, float.MaxValue, float.MaxValue))
- using (var sentinelOnly = new CanvasTextLayout(_device, sentinel, fontAdapter.TextFormat, float.MaxValue, float.MaxValue))
+ using (var layout = new CanvasTextLayout(_device, str, ((FontAdapter)font).TextFormat, float.MaxValue, float.MaxValue))
{
- var width = combined.LayoutBounds.Width - sentinelOnly.LayoutBounds.Width;
- return width > 0 ? width : 0;
+ return new RSize(layout.LayoutBoundsIncludingTrailingWhitespace.Width, font.Height);
}
}
@@ -225,10 +189,11 @@ public override void MeasureString(string str, RFont font, double maxWidth, out
return;
}
- if (layout.LayoutBounds.Width <= maxWidth)
+ var width = layout.LayoutBoundsIncludingTrailingWhitespace.Width;
+ if (width <= maxWidth)
{
charFit = str.Length;
- charFitWidth = layout.LayoutBounds.Width;
+ charFitWidth = width;
return;
}
@@ -240,10 +205,8 @@ public override void MeasureString(string str, RFont font, double maxWidth, out
}
else
{
- // Beyond the text's own extent but MeasureString(str,font) said it doesn't fit maxWidth -
- // shouldn't normally happen, but fail safe to "whole string fits".
charFit = str.Length;
- charFitWidth = layout.LayoutBounds.Width;
+ charFitWidth = width;
}
}
}
diff --git a/Source/HtmlRenderer.WinUI/Adapters/ImageAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/ImageAdapter.cs
index 3d1e4b929..faba2421a 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/ImageAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/ImageAdapter.cs
@@ -41,8 +41,7 @@ public CanvasBitmap Bitmap
get { return _bitmap; }
}
- // Device pixels, matching WPF's own choice (PixelWidth/PixelHeight) rather than DIPs
- // (CanvasBitmap.Size), so image layout matches native resolution the same way across both backends.
+ // Pixels rather than DIPs, so the image's DPI doesn't change its layout size
public override double Width
{
get { return _bitmap.SizeInPixels.Width; }
diff --git a/Source/HtmlRenderer.WinUI/Adapters/PenAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/PenAdapter.cs
index 52f8ec7c4..18e68a1a8 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/PenAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/PenAdapter.cs
@@ -21,10 +21,7 @@ namespace TheArtOfDev.HtmlRenderer.WinUI.Adapters
/// Adapter for Win2D pen (stroke) objects for core.
///
///
- /// Win2D has no standalone "pen" object the way GDI+/WPF do - stroking is expressed as a
- /// (brush, width, ) triple passed to each
- /// CanvasDrawingSession.Draw* call. This adapter lazily materializes that triple, mirroring
- /// HtmlRenderer.WPF's own PenAdapter's lazy CreatePen() pattern.
+ /// Win2D has no pen, brush, width and stroke style are passed to every draw call.
///
internal sealed class PenAdapter : RPen
{
diff --git a/Source/HtmlRenderer.WinUI/Adapters/WinUIAdapter.cs b/Source/HtmlRenderer.WinUI/Adapters/WinUIAdapter.cs
index 1f0527695..8843e16b9 100644
--- a/Source/HtmlRenderer.WinUI/Adapters/WinUIAdapter.cs
+++ b/Source/HtmlRenderer.WinUI/Adapters/WinUIAdapter.cs
@@ -22,12 +22,15 @@
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
+using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.Core.Network;
using TheArtOfDev.HtmlRenderer.WinUI.Utilities;
using Windows.ApplicationModel.DataTransfer;
+using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.ViewManagement;
@@ -41,8 +44,7 @@ internal sealed class WinUIAdapter : RAdapter
{
#region Fields and Consts
- // One HttpClient shared for the adapter's (process) lifetime - see WpfAdapter's own field for why
- // this must be declared before _instance (static field initializer ordering).
+ // Has to be initialized before _instance, the constructor uses it
private static readonly HttpClient _sharedHttpClient = new HttpClient();
///
@@ -56,30 +58,24 @@ internal sealed class WinUIAdapter : RAdapter
private static readonly List ValidColorNamesLc;
///
- /// The shared Win2D device this whole adapter (and every control it services) draws with -
- /// created once via before any
- /// exists, confirmed usable that way by the plan's Step 1 spike.
+ /// The Win2D device all resources are created on and all controls draw with.
///
private readonly CanvasDevice _device;
///
- /// The system settings object the app-theme subscription in the constructor hangs off - kept in a
- /// field for the adapter lifetime because ColorValuesChanged only lives as long as the UISettings
- /// instance does, so a local would be collected and the notification would never arrive.
+ /// ColorValuesChanged is only raised as long as the UISettings instance is alive.
///
private readonly UISettings _uiSettings;
- // Backs LoadFontFaceFontInt's temp-file registration - CanvasFontSet's documented in-memory story
- // is a file Uri, mirroring WPF's own Fonts.GetFontFamilies(Uri) constraint (see that class's own
- // remarks) - one directory per process, cleaned up by the OS's normal temp-file housekeeping.
+ ///
+ /// Win2D can only load a font set from a file, so the @font-face fonts are written to this directory.
+ ///
private static readonly string _fontFaceTempDirectory = CreateFontFaceTempDirectory();
- private static string CreateFontFaceTempDirectory()
- {
- var dir = Path.Combine(Path.GetTempPath(), "HtmlRenderer.FontFace." + Guid.NewGuid().ToString("N"));
- Directory.CreateDirectory(dir);
- return dir;
- }
+ ///
+ /// Cached app theme, null when it needs to be re-read.
+ ///
+ private RColorScheme? _colorScheme;
#endregion
@@ -99,8 +95,6 @@ private WinUIAdapter()
{
_device = CanvasDevice.GetSharedDevice();
- // Interactive UI backend - fetching a real http(s): image or stylesheet out of the box is the
- // expected behavior, same reasoning as WpfAdapter's own constructor.
NetworkLoader = new HttpClientNetworkLoader(_sharedHttpClient, (Uri)null);
AddFontFamilyMapping("monospace", "Courier New");
@@ -127,8 +121,7 @@ private WinUIAdapter()
}
catch
{
- // System font enumeration is a nice-to-have (helps IsFontExists recognize installed
- // families upfront); its failure shouldn't prevent the adapter itself from initializing.
+ // Without the system fonts, families are still resolved by name when the font is created
}
_uiSettings = new UISettings();
@@ -158,8 +151,7 @@ public CanvasDevice Device
}
///
- /// Rendering onto a Windows surface, so the document should follow the user's app theme.
- /// Cached and invalidated on a system preference change rather than read per query.
+ /// The user's app theme, cached until the system colors change.
///
public override RColorScheme SystemColorScheme
{
@@ -173,11 +165,6 @@ public override RColorScheme SystemColorScheme
}
}
- ///
- /// Cached app theme; null when it needs to be re-read.
- ///
- private RColorScheme? _colorScheme;
-
protected override RColor GetColorInt(string colorName)
{
if (!ValidColorNamesLc.Contains(colorName.ToLower()))
@@ -226,9 +213,17 @@ protected override RImage ConvertImageInt(object image)
protected override RImage ImageFromStreamInt(Stream memoryStream)
{
- var randomAccessStream = ToRandomAccessStreamAsync(memoryStream).GetAwaiter().GetResult();
- var bitmap = CanvasBitmap.LoadAsync(_device, randomAccessStream).AsTask().GetAwaiter().GetResult();
- return new ImageAdapter(bitmap);
+ // Blocking is fine as long as no continuation needs the calling thread, which may be the UI thread
+ using (var memory = new MemoryStream())
+ {
+ memoryStream.CopyTo(memory);
+ memory.Position = 0;
+ using (var randomAccessStream = memory.AsRandomAccessStream())
+ {
+ var bitmap = CanvasBitmap.LoadAsync(_device, randomAccessStream).AsTask().GetAwaiter().GetResult();
+ return new ImageAdapter(bitmap);
+ }
+ }
}
protected override RFont CreateFontInt(string family, double size, RFontStyle style)
@@ -238,15 +233,9 @@ protected override RFont CreateFontInt(string family, double size, RFontStyle st
protected override RFont CreateFontInt(RFontFamily family, double size, RFontStyle style)
{
- return new FontAdapter(_device, ((FontFamilyAdapter)family).Name, size, style);
+ return new FontAdapter(_device, ((FontFamilyAdapter)family).TextFormatFamily, size, style);
}
- ///
- /// Loads one @font-face face's bytes as a Win2D via a temp file,
- /// mirroring HtmlRenderer.WPF's own WpfAdapter.LoadFontFaceFontInt (same reasoning:
- /// there is no supported in-memory-only load path). Confirmed by the plan's Step 1 spike:
- /// 's constructor takes a single , not an array.
- ///
protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath)
{
var faceDirectory = Path.Combine(_fontFaceTempDirectory, Guid.NewGuid().ToString("N"));
@@ -254,17 +243,17 @@ protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string file
var tempFilePath = Path.Combine(faceDirectory, "face.ttf");
File.WriteAllBytes(tempFilePath, fontBytes);
- var fontSet = new CanvasFontSet(new Uri(tempFilePath));
- if (fontSet.Fonts.Count == 0)
- return null;
-
- var name = GetPreferredFamilyName(fontSet.Fonts[0].FamilyNames);
- return name != null ? new FontFamilyAdapter(name, fontSet) : null;
+ var fontFile = new Uri(tempFilePath);
+ using (var fontSet = new CanvasFontSet(fontFile))
+ {
+ var name = fontSet.Fonts.Count > 0 ? GetPreferredFamilyName(fontSet.Fonts[0].FamilyNames) : null;
+ return name != null ? new FontFamilyAdapter(name, fontFile) : null;
+ }
}
protected override object GetClipboardDataObjectInt(string html, string plainText)
{
- return ClipboardHelper.CreateDataObject(html, plainText);
+ return new ClipboardHelper.Content(html, plainText);
}
protected override void SetToClipboardInt(string text)
@@ -293,50 +282,20 @@ protected override RContextMenu CreateContextMenuInt()
return new ContextMenuAdapter();
}
- ///
- /// Show a save-file dialog and encode the image to the chosen path.
- ///
- ///
- /// An unpackaged WinUI 3 app's needs an owner HWND handed to it via
- /// 's runtime counterpart - resolved here
- /// from the passed-in control's XamlRoot via ,
- /// since this adapter (a process-wide singleton) has no Window reference of its own to fall
- /// back on. If no control is given, or its window can't be resolved, the save is silently skipped -
- /// this is a real, narrow gap versus WPF's parameterless SaveFileDialog.ShowDialog(); see the
- /// implementation report.
- ///
protected override void SaveToFileInt(RImage image, string name, string extension, RControl control = null)
{
- var element = (control as ControlAdapter)?.Control;
- if (element?.XamlRoot == null)
- return;
-
- var hwnd = Win32Interop.GetWindowFromWindowId(element.XamlRoot.ContentIslandEnvironment.AppWindowId);
- if (hwnd == IntPtr.Zero)
- return;
-
- var picker = new FileSavePicker
- {
- SuggestedFileName = name,
- DefaultFileExtension = extension,
- };
- picker.FileTypeChoices.Add("Image", new List { ".png", ".bmp", ".jpg", ".tif", ".gif" });
- WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
-
- var file = picker.PickSaveFileAsync().AsTask().GetAwaiter().GetResult();
- if (file == null)
- return;
-
- var format = GetBitmapFileFormat(Path.GetExtension(file.Path));
- var bitmap = ((ImageAdapter)image).Bitmap;
- using (var stream = file.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite).AsTask().GetAwaiter().GetResult())
- {
- bitmap.SaveAsync(stream, format).AsTask().GetAwaiter().GetResult();
- }
+ _ = SaveToFileAsync(((ImageAdapter)image).Bitmap, name, extension, (control as ControlAdapter)?.Control);
}
#region Private/Protected methods
+ private static string CreateFontFaceTempDirectory()
+ {
+ var dir = Path.Combine(Path.GetTempPath(), "HtmlRenderer.FontFace." + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ return dir;
+ }
+
///
/// Get solid color brush for the given color.
///
@@ -346,11 +305,9 @@ private CanvasSolidColorBrush GetSolidColorBrush(RColor color)
}
///
- /// Pick a family name from a font's family-names table, preferring the "en-us" entry (matching
- /// WPF's own XmlLanguage-keyed preference in its FontFamilyAdapter) and falling back
- /// to whatever entry exists first.
+ /// Pick a family name from a font's family names, preferring the "en-us" one.
///
- private static string GetPreferredFamilyName(IReadOnlyDictionary familyNames)
+ internal static string GetPreferredFamilyName(IReadOnlyDictionary familyNames)
{
if (familyNames == null || familyNames.Count == 0)
return null;
@@ -387,31 +344,45 @@ private static CanvasBitmapFileFormat GetBitmapFileFormat(string ext)
}
///
- /// Copy a .NET into an in-memory WinRT -
- /// the shape Win2D's
- /// needs, since 's seam only ever gives this a plain
- /// .NET stream.
+ /// Show a save file dialog and encode the image to the chosen file.
///
- private static async Task ToRandomAccessStreamAsync(Stream stream)
+ ///
+ /// The picker of an unpackaged app needs the window handle, which is taken from the control. Without a
+ /// control nothing is saved.
+ ///
+ private static async Task SaveToFileAsync(CanvasBitmap bitmap, string name, string extension, UIElement element)
{
- byte[] bytes;
- using (var memory = new MemoryStream())
+ if (element?.XamlRoot == null)
+ return;
+
+ var hwnd = Win32Interop.GetWindowFromWindowId(element.XamlRoot.ContentIslandEnvironment.AppWindowId);
+ if (hwnd == IntPtr.Zero)
+ return;
+
+ try
{
- await stream.CopyToAsync(memory).ConfigureAwait(false);
- bytes = memory.ToArray();
- }
+ var picker = new FileSavePicker
+ {
+ SuggestedFileName = name,
+ DefaultFileExtension = extension,
+ };
+ picker.FileTypeChoices.Add("Image", new List { ".png", ".bmp", ".jpg", ".tif", ".gif" });
+ WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
+
+ var file = await picker.PickSaveFileAsync();
+ if (file == null)
+ return;
- var randomAccessStream = new InMemoryRandomAccessStream();
- using (var writer = new DataWriter(randomAccessStream.GetOutputStreamAt(0)))
+ using (var stream = await file.OpenAsync(FileAccessMode.ReadWrite))
+ {
+ stream.Size = 0;
+ await bitmap.SaveAsync(stream, GetBitmapFileFormat(Path.GetExtension(file.Path)));
+ }
+ }
+ catch (Exception ex)
{
- writer.WriteBytes(bytes);
- await writer.StoreAsync();
- await writer.FlushAsync();
- writer.DetachStream();
+ (element as HtmlControl)?.ReportError(HtmlRenderErrorType.Image, "Failed to save the image", ex);
}
-
- randomAccessStream.Seek(0);
- return randomAccessStream;
}
#endregion
diff --git a/Source/HtmlRenderer.WinUI/HtmlContainer.cs b/Source/HtmlRenderer.WinUI/HtmlContainer.cs
index 29e90c6eb..9d686020f 100644
--- a/Source/HtmlRenderer.WinUI/HtmlContainer.cs
+++ b/Source/HtmlRenderer.WinUI/HtmlContainer.cs
@@ -350,22 +350,19 @@ public void HandleMouseUp(Control parent, PointerRoutedEventArgs e)
ArgChecker.AssertArgNotNull(e, "e");
var point = e.GetCurrentPoint(parent);
- var isLeft = point.Properties.PointerUpdateKind == Microsoft.UI.Input.PointerUpdateKind.LeftButtonReleased
- || point.Properties.PointerUpdateKind == Microsoft.UI.Input.PointerUpdateKind.Other;
- var mouseEvent = new RMouseEvent(isLeft);
+ var mouseEvent = new RMouseEvent(point.Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased);
_htmlContainerInt.HandleMouseUp(new ControlAdapter(parent), Utils.Convert(point.Position), mouseEvent);
}
///
/// Handle mouse double click to select word under the mouse.
///
- public void HandleMouseDoubleClick(Control parent, PointerRoutedEventArgs e)
+ public void HandleMouseDoubleClick(Control parent, DoubleTappedRoutedEventArgs e)
{
ArgChecker.AssertArgNotNull(parent, "parent");
ArgChecker.AssertArgNotNull(e, "e");
- var point = e.GetCurrentPoint(parent);
- _htmlContainerInt.HandleMouseDoubleClick(new ControlAdapter(parent), Utils.Convert(point.Position));
+ _htmlContainerInt.HandleMouseDoubleClick(new ControlAdapter(parent), Utils.Convert(e.GetPosition(parent)));
}
///
diff --git a/Source/HtmlRenderer.WinUI/HtmlControl.cs b/Source/HtmlRenderer.WinUI/HtmlControl.cs
index 3f596537a..4a7499fff 100644
--- a/Source/HtmlRenderer.WinUI/HtmlControl.cs
+++ b/Source/HtmlRenderer.WinUI/HtmlControl.cs
@@ -11,9 +11,9 @@
// "The Art of War"
using System;
+using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
@@ -25,6 +25,7 @@
using TheArtOfDev.HtmlRenderer.Core;
using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.WinUI.Utilities;
+using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
namespace TheArtOfDev.HtmlRenderer.WinUI
@@ -32,28 +33,38 @@ namespace TheArtOfDev.HtmlRenderer.WinUI
///
/// Provides HTML rendering using the text property.
/// WinUI 3 control that will render html content in it's client rectangle.
- /// The control will handle pointer and keyboard events on it to support html text selection,
- /// copy-paste and mouse clicks.
+ /// The control will handle pointer and keyboard events on it to support html text selection, copy-paste and mouse clicks.
///
/// The major differential to use HtmlPanel or HtmlLabel is size and scrollbars.
/// If the size of the control depends on the html content the HtmlLabel should be used.
/// If the size is set by some kind of layout then HtmlPanel is more suitable, also shows scrollbars if the html contents is larger than the control client rectangle.
///
+ ///
+ ///
LinkClicked event:
+ /// Raised when the user clicks on a link in the html.
+ /// Allows canceling the execution of the link.
+ ///
+ ///
+ ///
StylesheetLoad event:
+ /// Raised when a stylesheet is about to be loaded by file path or URI by link element.
+ /// This event allows to provide the stylesheet manually or provide new source (file or uri) to load from.
+ /// If no alternative data is provided the original source will be used.
+ ///
+ ///
+ ///
ImageLoad event:
+ /// Raised when an image is about to be loaded by file path or URI.
+ /// This event allows to provide the image manually, if not handled the image will be loaded from file or download from URI.
+ ///
+ ///
+ ///
RenderError event:
+ /// Raised when an error occurred during html rendering.
+ ///
+ ///
+ /// All events are raised on the UI thread.
+ ///
///
- ///
- /// WinUI 3's has no OnRender(DrawingContext)
- /// override point the way WPF's does, so a composed child replaces
- /// inheritance-based rendering - see . WinUI 3 also has no
- /// EventManager.RegisterRoutedEvent equivalent for user-defined bubbling events, so unlike
- /// HtmlRenderer.WPF's HtmlControl, whose events are custom s
- /// wrapped in a generic RoutedEventArgs<T>, this control's events are plain
- /// (that wrapper type is dropped entirely, not ported).
- ///
public class HtmlControl : UserControl
{
- ///
- /// The cursor shapes this control can be asked to show over the rendered html.
- ///
internal enum CursorShape
{
Default,
@@ -63,37 +74,19 @@ internal enum CursorShape
#region Fields and Consts
- ///
- /// How close together (in milliseconds) two pointer presses must land to be treated as a double-click.
- ///
- private const double DoubleClickThresholdMs = 500;
-
- ///
- /// How close together (in DIPs) two pointer presses must land to be treated as a double-click.
- ///
- private const double DoubleClickThresholdPixels = 4;
-
///
/// Underline html container instance.
///
protected readonly HtmlContainer _htmlContainer;
///
- /// The composed canvas child that all drawing happens on.
+ /// The canvas the html is drawn on.
///
protected readonly CanvasControl _canvas;
///
- /// The root panel hosting (and, for , its scrollbars).
+ /// The content of the control, holds and any additional chrome like scrollbars.
///
- ///
- /// WPF's own HtmlPanel adds its scrollbars as raw visual children via
- /// FrameworkElement.AddVisualChild/GetVisualChild, a WPF-only low-level visual-tree
- /// API with no WinUI 3 equivalent - has no public/protected
- /// extensibility point for adding a child that isn't part of an actual XAML content tree. A real
- /// panel is used here instead: still a mechanical, idiomatic-WinUI substitute for
- /// the same "extra chrome children alongside the canvas" need, not a behavioral rewrite.
- ///
protected readonly Grid _rootPanel;
///
@@ -107,25 +100,36 @@ internal enum CursorShape
protected Point _lastScrollOffset;
///
- /// The dispatcher queue captured at construction time, used to marshal
- /// events (which can fire on a thread-pool thread - see 's async
- /// operations) back onto the UI thread, mirroring WPF's CheckAccess()/Dispatcher.Invoke
- /// double-dispatch pattern.
+ /// Used to raise the container events on the UI thread.
///
private readonly DispatcherQueue _dispatcherQueue;
+ ///
+ /// Canceled when the UI thread's queue shuts down, the queued events then never run.
+ ///
+ private readonly CancellationToken _dispatcherShutdown;
+
+ ///
+ /// One per UI thread, the queue keeps its handler alive, so the handler can't reference a control.
+ ///
+ [ThreadStatic]
+ private static CancellationTokenSource _threadDispatcherShutdown;
+
///
/// Tracks the in-flight call, if any, so a newer call can supersede an
/// older one still awaiting .
///
private CancellationTokenSource _pendingLoad;
- private RPoint _lastPointerLocation;
- private bool _isLeftButtonPressed;
- private bool _isRightButtonPressed;
+ ///
+ /// The last pointer state seen by the control, there's no global mouse state to query in WinUI.
+ ///
+ private PointerPoint _lastPointerPoint;
- private DateTime _lastPointerPressedTime = DateTime.MinValue;
- private Point _lastPointerPressedPosition;
+ ///
+ /// The data of the drag operation started by the selection, null if none is in progress.
+ ///
+ private ClipboardHelper.Content _dragData;
#endregion
@@ -145,17 +149,25 @@ internal enum CursorShape
protected HtmlControl()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
-
- _rootPanel = new Grid();
- _canvas = new CanvasControl
+ if (_threadDispatcherShutdown == null)
{
- HorizontalAlignment = HorizontalAlignment.Stretch,
- VerticalAlignment = VerticalAlignment.Stretch,
- };
+ var shutdown = new CancellationTokenSource();
+ _dispatcherQueue.ShutdownStarting += (sender, args) => shutdown.Cancel();
+ _threadDispatcherShutdown = shutdown;
+ }
+ _dispatcherShutdown = _threadDispatcherShutdown.Token;
+
+ // A UserControl isn't a tab stop by default, without focus there are no key events for copy and scrolling
+ IsTabStop = true;
+
+ _canvas = new CanvasControl();
_canvas.Draw += OnCanvasDraw;
+ _rootPanel = new Grid();
_rootPanel.Children.Add(_canvas);
Content = _rootPanel;
+ DragStarting += OnDragStarting;
+
_htmlContainer = new HtmlContainer();
_htmlContainer.LoadComplete += OnLoadCompleteInternal;
_htmlContainer.LinkClicked += OnLinkClickedInternal;
@@ -184,17 +196,30 @@ protected HtmlControl()
///
/// Raised when a stylesheet is about to be loaded by file path or URI by link element.
+ /// This event allows to provide the stylesheet manually or provide new source (file or uri) to load from.
+ /// If no alternative data is provided the original source will be used.
///
public event EventHandler StylesheetLoad;
///
/// Raised when an image is about to be loaded by file path or URI.
+ /// This event allows to provide the image manually, if not handled the image will be loaded from file or download from URI.
///
public event EventHandler ImageLoad;
///
- /// Gets or sets a value indicating if image loading only when visible should be avoided (default - false).
+ /// Gets or sets a value indicating if image loading only when visible should be avoided (default - false).
+ /// True - images are loaded as soon as the html is parsed.
+ /// False - images that are not visible because of scroll location are not loaded until they are scrolled to.
///
+ ///
+ /// Images late loading improve performance if the page contains image outside the visible scroll area, especially if there is large
+ /// amount of images, as all image loading is delayed (downloading and loading into memory).
+ /// Late image loading may effect the layout and actual size as image without set size will not have actual size until they are loaded
+ /// resulting in layout change during user scroll.
+ /// Early image loading may also effect the layout if image without known size above the current scroll location are loaded as they
+ /// will push the html elements down.
+ ///
public bool AvoidImagesLateLoading
{
get { return (bool)GetValue(AvoidImagesLateLoadingProperty); }
@@ -202,7 +227,8 @@ public bool AvoidImagesLateLoading
}
///
- /// Is content selection is enabled for the rendered html (default - true).
+ /// Is content selection is enabled for the rendered html (default - true).
+ /// If set to 'false' the rendered html will be static only with ability to click on links.
///
public bool IsSelectionEnabled
{
@@ -254,43 +280,45 @@ public virtual string SelectedHtml
}
///
- /// The last known pointer location relative to this control, as tracked from its own pointer
- /// events - WinUI 3 has no queryable global pointer-button state the way WPF's
- /// does, so reads
- /// this cached value instead.
+ /// The last known pointer location relative to this control.
///
internal RPoint LastPointerLocation
{
- get { return _lastPointerLocation; }
+ get { return _lastPointerPoint != null ? Utils.Convert(_lastPointerPoint.Position) : RPoint.Empty; }
}
///
- /// Whether the left pointer button was down as of the last pointer event this control observed.
+ /// Whether the left pointer button was down as of the last pointer event of this control.
///
internal bool IsLeftButtonPressed
{
- get { return _isLeftButtonPressed; }
+ get { return _lastPointerPoint != null && _lastPointerPoint.Properties.IsLeftButtonPressed; }
}
///
- /// Whether the right pointer button was down as of the last pointer event this control observed.
+ /// Whether the right pointer button was down as of the last pointer event of this control.
///
internal bool IsRightButtonPressed
{
- get { return _isRightButtonPressed; }
+ get { return _lastPointerPoint != null && _lastPointerPoint.Properties.IsRightButtonPressed; }
}
///
/// Get html from the current DOM tree with inline style.
///
+ /// generated html
public virtual string GetHtml()
{
return _htmlContainer != null ? _htmlContainer.GetHtml() : null;
}
///
- /// Get the rectangle of html element as calculated by html layout.
+ /// Get the rectangle of html element as calculated by html layout.
+ /// Element if found by id (id attribute on the html element).
+ /// Note: to get the screen rectangle you need to adjust by the hosting control.
///
+ /// the id of the element to get its rectangle
+ /// the rectangle of the element or null if not found
public virtual Rect? GetElementRectangle(string elementId)
{
return _htmlContainer != null ? _htmlContainer.GetElementRectangle(elementId) : null;
@@ -305,14 +333,50 @@ public void ClearSelection()
}
///
- /// Set the cursor shown over this control's rendered html.
+ /// Sets the html of this control and awaits the async load - the real entry point behind the
+ /// / dependency-property callbacks,
+ /// for callers that want to await completion or cancel an in-flight load.
///
///
- /// is a protected member - it can only be set from within
- /// a subclass's own code, unlike WPF's public Control.Cursor
- /// setter, so (an external class) calls through this method
- /// rather than setting a cursor property directly.
+ /// A new call before a previous one finishes supersedes it: the previous call's
+ /// keeps running (it has no mid-flight cancellation point of
+ /// its own) but its post-completion side effects - invalidate, error reporting - are discarded
+ /// once it resumes, so only the newest call's results ever reach the control.
///
+ /// the html to set
+ /// optional: cancel this specific call without affecting others
+ public async Task SetTextAsync(string html, CancellationToken cancellationToken = default)
+ {
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ _pendingLoad?.Cancel();
+ _pendingLoad = cts;
+
+ try
+ {
+ await _htmlContainer.SetHtml(html, _baseCssData);
+ }
+ catch (Exception ex)
+ {
+ if (cts == _pendingLoad)
+ {
+ OnRenderError(new HtmlRenderErrorEventArgs(HtmlRenderErrorType.General, "Failed to set html", ex));
+ }
+ return;
+ }
+
+ if (cts != _pendingLoad || cts.IsCancellationRequested)
+ {
+ return;
+ }
+
+ InvalidateMeasure();
+ _canvas.Invalidate();
+ InvokeMouseMove();
+ }
+
+ ///
+ /// Set the cursor shown over the control, can only be set from within the control.
+ ///
internal void SetCursorShape(CursorShape shape)
{
InputSystemCursorShape cursorShape;
@@ -332,67 +396,43 @@ internal void SetCursorShape(CursorShape shape)
}
///
- /// Invalidate the composed canvas, causing a repaint.
+ /// Invalidate the canvas, causing a repaint.
///
internal void InvalidateCanvas()
{
- _canvas?.Invalidate();
+ _canvas.Invalidate();
}
- #region Protected methods
-
///
- /// Perform paint of the html in the control.
+ /// Raise the event, for errors outside of the container.
///
- private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args)
+ internal void ReportError(HtmlRenderErrorType type, string message, Exception exception)
{
- var g = args.DrawingSession;
- var renderSize = new Size(sender.ActualWidth, sender.ActualHeight);
-
- if (Background is SolidColorBrush backgroundBrush && backgroundBrush.Opacity > 0)
- g.FillRectangle(new Rect(0, 0, renderSize.Width, renderSize.Height), backgroundBrush.Color);
-
- var borderThickness = BorderThickness;
- if (borderThickness != new Thickness(0) && BorderBrush is SolidColorBrush borderColorBrush)
- {
- var color = borderColorBrush.Color;
- if (borderThickness.Top > 0)
- g.FillRectangle(new Rect(0, 0, renderSize.Width, borderThickness.Top), color);
- if (borderThickness.Bottom > 0)
- g.FillRectangle(new Rect(0, renderSize.Height - borderThickness.Bottom, renderSize.Width, borderThickness.Bottom), color);
- if (borderThickness.Left > 0)
- g.FillRectangle(new Rect(0, 0, borderThickness.Left, renderSize.Height), color);
- if (borderThickness.Right > 0)
- g.FillRectangle(new Rect(renderSize.Width - borderThickness.Right, 0, borderThickness.Right, renderSize.Height), color);
- }
+ OnRenderError(new HtmlRenderErrorEventArgs(type, message, exception));
+ }
- var htmlWidth = HtmlWidth(renderSize);
- var htmlHeight = HtmlHeight(renderSize);
- if (_htmlContainer != null && htmlWidth > 0 && htmlHeight > 0)
- {
- var clip = new Rect(Padding.Left + borderThickness.Left, Padding.Top + borderThickness.Top, htmlWidth, htmlHeight);
- using (g.CreateLayer(1f, clip))
- {
- _htmlContainer.Location = new Point(Padding.Left + borderThickness.Left, Padding.Top + borderThickness.Top);
- _htmlContainer.PerformPaint(g, clip);
- }
+ ///
+ /// Start dragging the given data with the pointer that is currently pressed on the control.
+ ///
+ internal void StartDragDrop(ClipboardHelper.Content data)
+ {
+ if (_dragData != null || _lastPointerPoint == null || !_lastPointerPoint.IsInContact)
+ return;
- if (!_lastScrollOffset.Equals(_htmlContainer.ScrollOffset))
- {
- _lastScrollOffset = _htmlContainer.ScrollOffset;
- InvokeMouseMove();
- }
- }
+ _dragData = data;
+ _ = DragDropAsync(_lastPointerPoint);
}
+ #region Protected methods
+
///
/// Handle pointer move to handle hover cursor and text selection.
///
protected override void OnPointerMoved(PointerRoutedEventArgs e)
{
base.OnPointerMoved(e);
- UpdatePointerState(e);
- _htmlContainer?.HandleMouseMove(this, Utils.Convert(_lastPointerLocation));
+ _lastPointerPoint = e.GetCurrentPoint(this);
+ _htmlContainer?.HandleMouseMove(this, _lastPointerPoint.Position);
}
///
@@ -405,30 +445,13 @@ protected override void OnPointerExited(PointerRoutedEventArgs e)
}
///
- /// Handle pointer press to handle selection, and hand-rolled double-click detection.
+ /// Handle pointer press to handle selection.
///
protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
base.OnPointerPressed(e);
- UpdatePointerState(e);
-
- var point = e.GetCurrentPoint(this).Position;
- var now = DateTime.UtcNow;
- var isDoubleClick = (now - _lastPointerPressedTime).TotalMilliseconds <= DoubleClickThresholdMs
- && Distance(point, _lastPointerPressedPosition) <= DoubleClickThresholdPixels;
-
- if (isDoubleClick)
- {
- // Reset so a third rapid click starts a fresh detection window rather than chaining.
- _lastPointerPressedTime = DateTime.MinValue;
- _htmlContainer?.HandleMouseDoubleClick(this, e);
- }
- else
- {
- _lastPointerPressedTime = now;
- _lastPointerPressedPosition = point;
- _htmlContainer?.HandleMouseDown(this, e);
- }
+ _lastPointerPoint = e.GetCurrentPoint(this);
+ _htmlContainer?.HandleMouseDown(this, e);
}
///
@@ -437,10 +460,19 @@ protected override void OnPointerPressed(PointerRoutedEventArgs e)
protected override void OnPointerReleased(PointerRoutedEventArgs e)
{
base.OnPointerReleased(e);
- UpdatePointerState(e);
+ _lastPointerPoint = e.GetCurrentPoint(this);
_htmlContainer?.HandleMouseUp(this, e);
}
+ ///
+ /// Handle double tap to select word under the pointer.
+ ///
+ protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e)
+ {
+ base.OnDoubleTapped(e);
+ _htmlContainer?.HandleMouseDoubleClick(this, e);
+ }
+
///
/// Handle key down event for selection, copy and scrollbars handling.
///
@@ -497,7 +529,7 @@ protected virtual void OnRefresh(HtmlRefreshEventArgs e)
{
if (e.Layout)
InvalidateMeasure();
- _canvas?.Invalidate();
+ _canvas.Invalidate();
}
///
@@ -521,59 +553,83 @@ protected virtual double HtmlHeight(Size size)
///
protected virtual void InvokeMouseMove()
{
- _htmlContainer.HandleMouseMove(this, Utils.Convert(_lastPointerLocation));
+ _htmlContainer.HandleMouseMove(this, Utils.Convert(LastPointerLocation));
}
+ #endregion
+
+ #region Private methods
+
///
- /// Sets the html of this control and awaits the async load - the real entry point behind the
- /// / dependency-property callbacks.
+ /// Perform paint of the html in the control.
///
- public async Task SetTextAsync(string html, CancellationToken cancellationToken = default)
+ private void OnCanvasDraw(CanvasControl sender, CanvasDrawEventArgs args)
{
- var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- _pendingLoad?.Cancel();
- _pendingLoad = cts;
+ var g = args.DrawingSession;
+ var renderSize = new Size(sender.ActualWidth, sender.ActualHeight);
- try
+ if (Background is SolidColorBrush backgroundBrush && backgroundBrush.Opacity > 0)
+ g.FillRectangle(new Rect(0, 0, renderSize.Width, renderSize.Height), backgroundBrush.Color);
+
+ var borderThickness = BorderThickness;
+ if (borderThickness != new Thickness(0) && BorderBrush is SolidColorBrush borderBrush)
{
- await _htmlContainer.SetHtml(html, _baseCssData);
+ var color = borderBrush.Color;
+ if (borderThickness.Top > 0)
+ g.FillRectangle(new Rect(0, 0, renderSize.Width, borderThickness.Top), color);
+ if (borderThickness.Bottom > 0)
+ g.FillRectangle(new Rect(0, renderSize.Height - borderThickness.Bottom, renderSize.Width, borderThickness.Bottom), color);
+ if (borderThickness.Left > 0)
+ g.FillRectangle(new Rect(0, 0, borderThickness.Left, renderSize.Height), color);
+ if (borderThickness.Right > 0)
+ g.FillRectangle(new Rect(renderSize.Width - borderThickness.Right, 0, borderThickness.Right, renderSize.Height), color);
}
- catch (Exception ex)
+
+ var htmlWidth = HtmlWidth(renderSize);
+ var htmlHeight = HtmlHeight(renderSize);
+ if (_htmlContainer != null && htmlWidth > 0 && htmlHeight > 0)
{
- if (cts == _pendingLoad)
+ var clip = new Rect(Padding.Left + borderThickness.Left, Padding.Top + borderThickness.Top, htmlWidth, htmlHeight);
+ using (g.CreateLayer(1f, clip))
{
- OnRenderError(new HtmlRenderErrorEventArgs(HtmlRenderErrorType.General, "Failed to set html", ex));
+ _htmlContainer.Location = new Point(clip.X, clip.Y);
+ _htmlContainer.PerformPaint(g, clip);
}
- return;
- }
- if (cts != _pendingLoad || cts.IsCancellationRequested)
- {
- return;
+ if (!_lastScrollOffset.Equals(_htmlContainer.ScrollOffset))
+ {
+ _lastScrollOffset = _htmlContainer.ScrollOffset;
+ InvokeMouseMove();
+ }
}
-
- InvalidateMeasure();
- _canvas?.Invalidate();
- InvokeMouseMove();
}
- #endregion
-
- #region Private methods
-
- private void UpdatePointerState(PointerRoutedEventArgs e)
+ private async Task DragDropAsync(PointerPoint pointerPoint)
{
- var point = e.GetCurrentPoint(this);
- _isLeftButtonPressed = point.Properties.IsLeftButtonPressed;
- _isRightButtonPressed = point.Properties.IsRightButtonPressed;
- _lastPointerLocation = Utils.Convert(point.Position);
+ try
+ {
+ await StartDragAsync(pointerPoint);
+ }
+ catch (Exception ex)
+ {
+ OnRenderError(new HtmlRenderErrorEventArgs(HtmlRenderErrorType.KeyboardMouse, "Failed drag and drop", ex));
+ }
+ finally
+ {
+ _dragData = null;
+ }
}
- private static double Distance(Point a, Point b)
+ private void OnDragStarting(UIElement sender, DragStartingEventArgs args)
{
- var dx = a.X - b.X;
- var dy = a.Y - b.Y;
- return Math.Sqrt(dx * dx + dy * dy);
+ if (_dragData == null)
+ {
+ args.Cancel = true;
+ return;
+ }
+
+ ClipboardHelper.Fill(args.Data, _dragData);
+ args.AllowedOperations = DataPackageOperation.Copy;
}
///
@@ -611,58 +667,83 @@ private static void OnDependencyProperty_valueChanged(DependencyObject dependenc
}
}
- #region Private event handlers (marshal onto the UI thread)
+ ///
+ /// Run the given action on the UI thread and wait for it, the load events need the handler's changes to
+ /// the event args before the container continues.
+ ///
+ private void InvokeOnUIThread(Action action)
+ {
+ if (_dispatcherQueue.HasThreadAccess)
+ {
+ action();
+ return;
+ }
+
+ // Not disposed, a queued action may still set it after the queue shut down
+ var done = new ManualResetEventSlim();
+ ExceptionDispatchInfo error = null;
+ var queued = _dispatcherQueue.TryEnqueue(() =>
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception ex)
+ {
+ error = ExceptionDispatchInfo.Capture(ex);
+ }
+ finally
+ {
+ done.Set();
+ }
+ });
+
+ // Once the queue shuts down, there's no one left to raise the event to
+ if (!queued)
+ return;
+
+ try
+ {
+ done.Wait(_dispatcherShutdown);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ error?.Throw();
+ }
private void OnLoadCompleteInternal(object sender, EventArgs e)
{
- if (_dispatcherQueue.HasThreadAccess)
- OnLoadComplete(e);
- else
- _dispatcherQueue.TryEnqueue(() => OnLoadComplete(e));
+ InvokeOnUIThread(() => OnLoadComplete(e));
}
private void OnLinkClickedInternal(object sender, HtmlLinkClickedEventArgs e)
{
- if (_dispatcherQueue.HasThreadAccess)
- OnLinkClicked(e);
- else
- _dispatcherQueue.TryEnqueue(() => OnLinkClicked(e));
+ InvokeOnUIThread(() => OnLinkClicked(e));
}
private void OnRenderErrorInternal(object sender, HtmlRenderErrorEventArgs e)
{
- if (_dispatcherQueue.HasThreadAccess)
- OnRenderError(e);
- else
- _dispatcherQueue.TryEnqueue(() => OnRenderError(e));
+ InvokeOnUIThread(() => OnRenderError(e));
}
private void OnStylesheetLoadInternal(object sender, HtmlStylesheetLoadEventArgs e)
{
- if (_dispatcherQueue.HasThreadAccess)
- OnStylesheetLoad(e);
- else
- _dispatcherQueue.TryEnqueue(() => OnStylesheetLoad(e));
+ InvokeOnUIThread(() => OnStylesheetLoad(e));
}
private void OnImageLoadInternal(object sender, HtmlImageLoadEventArgs e)
{
- if (_dispatcherQueue.HasThreadAccess)
- OnImageLoad(e);
- else
- _dispatcherQueue.TryEnqueue(() => OnImageLoad(e));
+ InvokeOnUIThread(() => OnImageLoad(e));
}
private void OnRefreshInternal(object sender, HtmlRefreshEventArgs e)
{
- if (_dispatcherQueue.HasThreadAccess)
- OnRefresh(e);
- else
- _dispatcherQueue.TryEnqueue(() => OnRefresh(e));
+ InvokeOnUIThread(() => OnRefresh(e));
}
#endregion
-
- #endregion
}
}
diff --git a/Source/HtmlRenderer.WinUI/HtmlPanel.cs b/Source/HtmlRenderer.WinUI/HtmlPanel.cs
index 6e665ca29..06c75116c 100644
--- a/Source/HtmlRenderer.WinUI/HtmlPanel.cs
+++ b/Source/HtmlRenderer.WinUI/HtmlPanel.cs
@@ -30,14 +30,7 @@ namespace TheArtOfDev.HtmlRenderer.WinUI
/// The control will handle pointer and keyboard events on it to support html text selection, copy-paste and mouse clicks.
///
///
- /// See for more info. Unlike WPF's own HtmlPanel, which docks its
- /// scrollbars via raw visual children (FrameworkElement.AddVisualChild/manual
- /// ArrangeOverride pixel math) - a WPF-only API with no WinUI 3 equivalent - this control docks
- /// its two s as ordinary children of ,
- /// positioned via /
- /// instead of a manual per-frame Rect computation. The HTML-side layout math (max width,
- /// scrollbar min/max/viewport, visibility toggling to avoid a wrap/re-layout thrash) is still a
- /// mechanical, close port of WPF's own.
+ /// See for more info.
///
public class HtmlPanel : HtmlControl
{
@@ -65,10 +58,7 @@ public HtmlPanel()
_verticalScrollBar = new ScrollBar
{
Orientation = Orientation.Vertical,
- // A standalone ScrollBar (outside a ScrollViewer) defaults IndicatorMode to None, which
- // renders nothing at all regardless of Visibility - confirmed via a documented WinUI 3
- // issue (https://learn.microsoft.com/en-us/answers/questions/792524), whose fix is
- // exactly this: set IndicatorMode explicitly.
+ // A standalone ScrollBar shows nothing with the default IndicatorMode of None
IndicatorMode = ScrollingIndicatorMode.MouseIndicator,
Width = 18,
Visibility = Visibility.Collapsed,
@@ -92,10 +82,6 @@ public HtmlPanel()
_htmlContainer.ScrollChange += OnScrollChange;
- // WPF overrides TextProperty's own PropertyMetadata for the HtmlPanel type
- // (DependencyProperty.OverrideMetadata) to reset scroll position on text change - WinUI 3's
- // DependencyProperty has no per-subclass metadata override, so this uses the instance-level
- // RegisterPropertyChangedCallback substitute instead.
RegisterPropertyChangedCallback(TextProperty, (d, dp) =>
{
_horizontalScrollBar.Value = _verticalScrollBar.Value = 0;
@@ -191,12 +177,15 @@ protected Size PerformHtmlLayout(Size constraint)
}
///
- /// Handle pointer release to set focus on the control.
+ /// Handle pointer press to set focus on the control.
///
- protected override void OnPointerReleased(PointerRoutedEventArgs e)
+ protected override void OnPointerPressed(PointerRoutedEventArgs e)
{
- base.OnPointerReleased(e);
Focus(FocusState.Pointer);
+ base.OnPointerPressed(e);
+
+ // Otherwise the root ScrollViewer takes the focus
+ e.Handled = true;
}
///
diff --git a/Source/HtmlRenderer.WinUI/HtmlRender.cs b/Source/HtmlRenderer.WinUI/HtmlRender.cs
index 1bc086bcd..671aca0a2 100644
--- a/Source/HtmlRenderer.WinUI/HtmlRender.cs
+++ b/Source/HtmlRenderer.WinUI/HtmlRender.cs
@@ -36,20 +36,23 @@ public static class HtmlRender
/// Adds a font family to be used in html rendering.
/// The added font will be used by all rendering function including and all WinUI 3 controls.
///
- ///
- /// Unlike WPF's AddFontFamily(FontFamily), WinUI/DirectWrite has no lightweight standalone
- /// "font family object" outside of a loaded - pass the font set that
- /// registered (see 's
- /// own use of it) when adding a custom-loaded family, or leave it null for an already
- /// system-installed family name.
- ///
- /// The font family name to add.
- /// optional: the font set that backs the family, kept alive for as long as the family is referenced
- public static void AddFontFamily(string familyName, CanvasFontSet fontSet = null)
+ ///
+ /// HtmlRender.AddFontFamily(new Uri(@"C:\fonts\CustomFont.ttf"));
+ ///
+ /// The font file with the font families to add, a file or ms-appx:/// URI.
+ public static void AddFontFamily(Uri fontFile)
{
- ArgChecker.AssertArgNotNullOrEmpty(familyName, "familyName");
+ ArgChecker.AssertArgNotNull(fontFile, "fontFile");
- WinUIAdapter.Instance.AddFontFamily(new FontFamilyAdapter(familyName, fontSet));
+ using (var fontSet = new CanvasFontSet(fontFile))
+ {
+ foreach (var font in fontSet.Fonts)
+ {
+ var name = WinUIAdapter.GetPreferredFamilyName(font.FamilyNames);
+ if (name != null)
+ WinUIAdapter.Instance.AddFontFamily(new FontFamilyAdapter(name, fontFile));
+ }
+ }
}
///
@@ -153,9 +156,7 @@ public static Task RenderAsync(CanvasDrawingSession g, string html, Point
/// The HTML will be layout by the given size but will be clipped if cannot fit.
///
///
- /// Unlike WPF's RenderToImageAsync, which needs a UI-thread-affine RenderTargetBitmap,
- /// Win2D's is a plain GPU/CPU resource with no UI-thread
- /// affinity - this needs no dispatcher/UI thread at all.
+ /// Doesn't need the UI thread.
///
/// HTML source to render
/// The size of the image to render into, layout html by width and clipped by height
diff --git a/Source/HtmlRenderer.WinUI/Utilities/ClipboardHelper.cs b/Source/HtmlRenderer.WinUI/Utilities/ClipboardHelper.cs
index bcd68e435..bd8c24318 100644
--- a/Source/HtmlRenderer.WinUI/Utilities/ClipboardHelper.cs
+++ b/Source/HtmlRenderer.WinUI/Utilities/ClipboardHelper.cs
@@ -15,37 +15,37 @@
namespace TheArtOfDev.HtmlRenderer.WinUI.Utilities
{
///
- /// Helper to set HTML fragment and plain text data to the clipboard.
+ /// Helper to set HTML fragment and plain text data to the clipboard or a drag operation.
///
- ///
- /// HtmlRenderer.WPF's own -based helper hand-builds the
- /// full CF_HTML header (Version:/StartHTML:/EndHTML:/StartFragment:/
- /// EndFragment: with byte offsets computed via manual UTF-8 byte counting) because WPF's own
- /// clipboard API has no higher-level "give me an HTML fragment, I'll build the header" helper.
- ///
- /// The WinRT clipboard API does: takes a plain HTML
- /// fragment string and returns it already wrapped in a correct CF_HTML header (offsets included), and
- /// is documented to accept exactly that pre-built CF_HTML
- /// string. That makes WPF's entire byte-offset-arithmetic GetHtmlDataString implementation
- /// (StartFragment/EndFragment comment injection, UTF-8 byte counting, header back-patching)
- /// unnecessary here - confirmed during implementation per the plan's Step 6 note anticipating exactly
- /// this outcome.
- ///
- ///
internal static class ClipboardHelper
{
///
- /// Create a with given html and plain-text ready to be used for clipboard
- /// or drag-drop operation.
+ /// HTML fragment and plain text to put on the clipboard or drag.
///
- /// a html fragment
- /// the plain text
- public static DataPackage CreateDataObject(string html, string plainText)
+ ///
+ /// A drag operation brings its own , so the content is kept separate from it.
+ ///
+ public sealed class Content
{
- var dataPackage = new DataPackage();
- dataPackage.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(html ?? string.Empty));
- dataPackage.SetText(plainText ?? string.Empty);
- return dataPackage;
+ public Content(string html, string plainText)
+ {
+ Html = html ?? string.Empty;
+ PlainText = plainText ?? string.Empty;
+ }
+
+ public string Html { get; }
+
+ public string PlainText { get; }
+ }
+
+ ///
+ /// Set the given content as HTML and plain text on the data package.
+ ///
+ public static void Fill(DataPackage dataPackage, Content content)
+ {
+ // CreateHtmlFormat builds the CF_HTML header with its byte offsets
+ dataPackage.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(content.Html));
+ dataPackage.SetText(content.PlainText);
}
///
@@ -55,7 +55,9 @@ public static DataPackage CreateDataObject(string html, string plainText)
/// the plain text
public static void CopyToClipboard(string html, string plainText)
{
- Clipboard.SetContent(CreateDataObject(html, plainText));
+ var dataPackage = new DataPackage();
+ Fill(dataPackage, new Content(html, plainText));
+ Clipboard.SetContent(dataPackage);
}
///
diff --git a/Source/HtmlRenderer.WinUI/Utilities/WindowsTheme.cs b/Source/HtmlRenderer.WinUI/Utilities/WindowsTheme.cs
index b8f0041e7..489efb628 100644
--- a/Source/HtmlRenderer.WinUI/Utilities/WindowsTheme.cs
+++ b/Source/HtmlRenderer.WinUI/Utilities/WindowsTheme.cs
@@ -19,14 +19,6 @@ namespace TheArtOfDev.HtmlRenderer.WinUI.Utilities
/// Reads the Windows app theme, which is what prefers-color-scheme reports for on-screen
/// rendering.
///
- ///
- /// The registry read is ported byte-for-byte from HtmlRenderer.WPF's own
- /// Utilities.WindowsTheme - it has no WPF-specific dependency, it's the same
- /// HKCU\...\Personalize\AppsUseLightTheme value WPF reads. Only the change-notification
- /// source differs (see 's constructor, which subscribes to
- /// Windows.UI.ViewManagement.UISettings.ColorValuesChanged instead of WPF's
- /// SystemEvents.UserPreferenceChanged - the modern WinRT-native signal for a WinUI 3 app).
- ///
internal static class WindowsTheme
{
private const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
From 49fe88c6b0d7f800cecc3d2cbab1e89a837a1359 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?=
Date: Fri, 13 Nov 2026 16:09:45 +0100
Subject: [PATCH 2/3] Bring the WinUI demo on par with the WPF demo
---
Source/Demo/WinUI/App.xaml | 8 +-
Source/Demo/WinUI/App.xaml.cs | 24 +-
Source/Demo/WinUI/GenerateImageWindow.xaml | 19 +
Source/Demo/WinUI/GenerateImageWindow.xaml.cs | 130 +++++++
.../Demo/WinUI/HtmlRenderer.Demo.WinUI.csproj | 12 +-
Source/Demo/WinUI/HtmlRenderingHelper.cs | 143 +++++++
Source/Demo/WinUI/MainControl.xaml | 56 +++
Source/Demo/WinUI/MainControl.xaml.cs | 357 ++++++++++++++++++
Source/Demo/WinUI/MainWindow.xaml | 36 +-
Source/Demo/WinUI/MainWindow.xaml.cs | 198 ++++------
Source/Demo/WinUI/SampleWindow.xaml | 45 +++
Source/Demo/WinUI/SampleWindow.xaml.cs | 87 +++++
Source/Demo/WinUI/WindowHelper.cs | 76 ++++
13 files changed, 1032 insertions(+), 159 deletions(-)
create mode 100644 Source/Demo/WinUI/GenerateImageWindow.xaml
create mode 100644 Source/Demo/WinUI/GenerateImageWindow.xaml.cs
create mode 100644 Source/Demo/WinUI/HtmlRenderingHelper.cs
create mode 100644 Source/Demo/WinUI/MainControl.xaml
create mode 100644 Source/Demo/WinUI/MainControl.xaml.cs
create mode 100644 Source/Demo/WinUI/SampleWindow.xaml
create mode 100644 Source/Demo/WinUI/SampleWindow.xaml.cs
create mode 100644 Source/Demo/WinUI/WindowHelper.cs
diff --git a/Source/Demo/WinUI/App.xaml b/Source/Demo/WinUI/App.xaml
index d8ca34c0e..6e653679b 100644
--- a/Source/Demo/WinUI/App.xaml
+++ b/Source/Demo/WinUI/App.xaml
@@ -1,14 +1,12 @@
-
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ RequestedTheme="Light">
+
-
diff --git a/Source/Demo/WinUI/App.xaml.cs b/Source/Demo/WinUI/App.xaml.cs
index cad246ffb..ec0316443 100644
--- a/Source/Demo/WinUI/App.xaml.cs
+++ b/Source/Demo/WinUI/App.xaml.cs
@@ -10,15 +10,14 @@
// - Sun Tsu,
// "The Art of War"
+using System;
+using System.IO;
using Microsoft.UI.Xaml;
+using TheArtOfDev.HtmlRenderer.Demo.Common;
+using TheArtOfDev.HtmlRenderer.WinUI;
namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
{
- ///
- /// Minimal application entry point - hosts one with a single
- /// rendering a hardcoded sample document, just
- /// enough to prove the WinUI 3 control renders real HTML end to end.
- ///
public partial class App : Application
{
private Window _window;
@@ -30,8 +29,23 @@ public App()
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
+ SamplesLoader.Init("WinUI", typeof(HtmlRender).Assembly.GetName().Version.ToString());
+
+ LoadCustomFonts();
+
_window = new MainWindow();
_window.Activate();
}
+
+ ///
+ /// Load custom fonts to be used by renderer HTMLs
+ ///
+ private static void LoadCustomFonts()
+ {
+ // Win2D loads fonts only from files
+ var file = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".ttf");
+ File.WriteAllBytes(file, Common.Resources.CustomFont);
+ HtmlRender.AddFontFamily(new Uri(file));
+ }
}
}
diff --git a/Source/Demo/WinUI/GenerateImageWindow.xaml b/Source/Demo/WinUI/GenerateImageWindow.xaml
new file mode 100644
index 000000000..4b4420f1f
--- /dev/null
+++ b/Source/Demo/WinUI/GenerateImageWindow.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Demo/WinUI/GenerateImageWindow.xaml.cs b/Source/Demo/WinUI/GenerateImageWindow.xaml.cs
new file mode 100644
index 000000000..ea25efc8b
--- /dev/null
+++ b/Source/Demo/WinUI/GenerateImageWindow.xaml.cs
@@ -0,0 +1,130 @@
+// "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.IO;
+using System.Threading.Tasks;
+using Microsoft.Graphics.Canvas;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media.Imaging;
+using TheArtOfDev.HtmlRenderer.Demo.Common;
+using TheArtOfDev.HtmlRenderer.WinUI;
+using Windows.Foundation;
+using Windows.Storage;
+using Windows.Storage.Streams;
+
+namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
+{
+ public sealed partial class GenerateImageWindow : Window
+ {
+ private readonly string _html;
+ private CanvasRenderTarget _generatedImage;
+
+ public GenerateImageWindow(string html)
+ {
+ _html = html;
+
+ InitializeComponent();
+
+ WindowHelper.Setup(this, "Generate Image", 0.56, 0.64);
+
+ _imageBoxBorder.SizeChanged += OnFirstSizeChanged;
+ Closed += (sender, args) => _generatedImage?.Dispose();
+ }
+
+ private async void OnFirstSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ _imageBoxBorder.SizeChanged -= OnFirstSizeChanged;
+ await GenerateImage();
+ }
+
+ private async void OnGenerateImage_Click(object sender, RoutedEventArgs e)
+ {
+ await GenerateImage();
+ }
+
+ private async void OnSaveToFile_Click(object sender, RoutedEventArgs e)
+ {
+ if (_generatedImage == null)
+ return;
+
+ try
+ {
+ var picker = WindowHelper.CreateSavePicker(this);
+ picker.SuggestedFileName = "image";
+ picker.DefaultFileExtension = ".png";
+ picker.FileTypeChoices.Add("Images", new List { ".png", ".bmp", ".jpg", ".tif", ".gif" });
+
+ var file = await picker.PickSaveFileAsync();
+ if (file == null)
+ return;
+
+ using var stream = await file.OpenAsync(FileAccessMode.ReadWrite);
+ stream.Size = 0;
+ await _generatedImage.SaveAsync(stream, GetBitmapFileFormat(Path.GetExtension(file.Path)));
+ }
+ catch (Exception ex)
+ {
+ await WindowHelper.ShowMessageAsync(Content.XamlRoot, ex.ToString(), "Failed to save the image");
+ }
+ }
+
+ private async Task GenerateImage()
+ {
+ var size = new Size(_imageBoxBorder.ActualWidth, _imageBoxBorder.ActualHeight);
+ if (size.Width > 0 && size.Height > 0)
+ {
+ try
+ {
+ _generatedImage?.Dispose();
+ _generatedImage = await HtmlRender.RenderToImageAsync(_html, size, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoad);
+
+ using var stream = new InMemoryRandomAccessStream();
+ await _generatedImage.SaveAsync(stream, CanvasBitmapFileFormat.Png);
+ stream.Seek(0);
+
+ var bitmap = new BitmapImage();
+ await bitmap.SetSourceAsync(stream);
+ _imageBox.Source = bitmap;
+ }
+ catch (Exception ex)
+ {
+ _generatedImage = null;
+ await WindowHelper.ShowMessageAsync(Content.XamlRoot, ex.ToString(), "Failed to generate the image");
+ }
+ }
+ }
+
+ ///
+ /// Get the Win2D format to encode the image with for the given file extension, default is PNG.
+ ///
+ private static CanvasBitmapFileFormat GetBitmapFileFormat(string ext)
+ {
+ switch (ext.ToLower())
+ {
+ case ".jpg":
+ case ".jpeg":
+ return CanvasBitmapFileFormat.Jpeg;
+ case ".bmp":
+ return CanvasBitmapFileFormat.Bmp;
+ case ".tif":
+ case ".tiff":
+ return CanvasBitmapFileFormat.Tiff;
+ case ".gif":
+ return CanvasBitmapFileFormat.Gif;
+ default:
+ return CanvasBitmapFileFormat.Png;
+ }
+ }
+ }
+}
diff --git a/Source/Demo/WinUI/HtmlRenderer.Demo.WinUI.csproj b/Source/Demo/WinUI/HtmlRenderer.Demo.WinUI.csproj
index 14b61a3c3..725b35a20 100644
--- a/Source/Demo/WinUI/HtmlRenderer.Demo.WinUI.csproj
+++ b/Source/Demo/WinUI/HtmlRenderer.Demo.WinUI.csproj
@@ -6,12 +6,8 @@
TheArtOfDev.HtmlRenderer.Demo.WinUIHtmlRendererWinUIDemotrue
- truetrue
-
+
Nonetruetrue
@@ -23,8 +19,7 @@
html.ico
-
+
@@ -34,12 +29,13 @@
+
-
+
diff --git a/Source/Demo/WinUI/HtmlRenderingHelper.cs b/Source/Demo/WinUI/HtmlRenderingHelper.cs
new file mode 100644
index 000000000..7f356fbca
--- /dev/null
+++ b/Source/Demo/WinUI/HtmlRenderingHelper.cs
@@ -0,0 +1,143 @@
+// "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.IO;
+using System.Threading;
+using Microsoft.Graphics.Canvas;
+using PdfSharp.Drawing;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Demo.Common;
+using Windows.Storage.Streams;
+
+namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
+{
+ internal static class HtmlRenderingHelper
+ {
+ private const string ByEventImageUrl = "https://images.unsplash.com/photo-1608848461950-0fe51dfc41cb?w=500&q=80";
+
+ ///
+ /// Cache for resource images, the images are loaded from the render threads as well
+ ///
+ private static readonly Dictionary _imageCache = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Get image by resource key.
+ ///
+ public static CanvasBitmap TryLoadResourceImage(string src)
+ {
+ lock (_imageCache)
+ {
+ if (!_imageCache.TryGetValue(src, out var image))
+ {
+ using var stream = DemoUtils.GetImageStream(src);
+ if (stream != null)
+ {
+ image = CanvasBitmap.LoadAsync(CanvasDevice.GetSharedDevice(), stream.AsRandomAccessStream()).AsTask().GetAwaiter().GetResult();
+ _imageCache[src] = image;
+ }
+ }
+ return image;
+ }
+ }
+
+ ///
+ /// Get the resource image of the given key as data URI, to show the html in a browser.
+ ///
+ public static string TryGetResourceImageDataUri(string src)
+ {
+ using var stream = DemoUtils.GetImageStream(src);
+ if (stream == null)
+ return null;
+
+ using var memory = new MemoryStream();
+ stream.CopyTo(memory);
+ var bytes = memory.ToArray();
+ var mimeType = bytes.Length > 3 && bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F' ? "image/gif" : "image/png";
+ return "data:" + mimeType + ";base64," + Convert.ToBase64String(bytes);
+ }
+
+ ///
+ /// On image load in renderer set the image by event async.
+ ///
+ public static void OnImageLoad(object sender, HtmlImageLoadEventArgs args)
+ {
+ ImageLoad(args, false);
+ }
+
+ ///
+ /// On image load in renderer set the image by event async.
+ ///
+ public static void OnImageLoadPdfSharp(object sender, HtmlImageLoadEventArgs args)
+ {
+ ImageLoad(args, true);
+ }
+
+ ///
+ /// On image load in renderer set the image by event async.
+ ///
+ private static void ImageLoad(HtmlImageLoadEventArgs e, bool pdfSharp)
+ {
+ var img = TryLoadResourceImage(e.Src);
+ var imgObj = pdfSharp ? (object)(img != null ? CreatePdfSharpImage(img) : null) : img;
+
+ if (!e.Handled && e.Attributes != null)
+ {
+ if (e.Attributes.ContainsKey("byevent"))
+ {
+ if (int.TryParse(e.Attributes["byevent"], out var delay))
+ {
+ e.Handled = true;
+ ThreadPool.QueueUserWorkItem(state =>
+ {
+ Thread.Sleep(delay);
+ e.Callback(ByEventImageUrl);
+ });
+ }
+ else
+ {
+ e.Callback(ByEventImageUrl);
+ }
+ return;
+ }
+
+ if (e.Attributes.ContainsKey("byrect"))
+ {
+ var split = e.Attributes["byrect"].Split(',');
+ var defaultResourceImage = TryLoadResourceImage("htmlicon");
+ var defaultImg = pdfSharp && defaultResourceImage != null ? (object)CreatePdfSharpImage(defaultResourceImage) : defaultResourceImage;
+ e.Callback(imgObj ?? defaultImg, int.Parse(split[0]), int.Parse(split[1]), int.Parse(split[2]), int.Parse(split[3]));
+ return;
+ }
+ }
+
+ if (img != null)
+ e.Callback(imgObj);
+ }
+
+ ///
+ /// Convert the image to PNG before creating the , PdfSharp doesn't support GIF.
+ ///
+ private static XImage CreatePdfSharpImage(CanvasBitmap image)
+ {
+ using var stream = new InMemoryRandomAccessStream();
+ image.SaveAsync(stream, CanvasBitmapFileFormat.Png).AsTask().GetAwaiter().GetResult();
+
+ stream.Seek(0);
+ var memory = new MemoryStream();
+ stream.AsStreamForRead().CopyTo(memory);
+ memory.Position = 0;
+ return XImage.FromStream(memory);
+ }
+ }
+}
diff --git a/Source/Demo/WinUI/MainControl.xaml b/Source/Demo/WinUI/MainControl.xaml
new file mode 100644
index 000000000..fbcef0e35
--- /dev/null
+++ b/Source/Demo/WinUI/MainControl.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Demo/WinUI/MainControl.xaml.cs b/Source/Demo/WinUI/MainControl.xaml.cs
new file mode 100644
index 000000000..5e821f9cb
--- /dev/null
+++ b/Source/Demo/WinUI/MainControl.xaml.cs
@@ -0,0 +1,357 @@
+// "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.Text;
+using System.Text.RegularExpressions;
+using Microsoft.UI.Dispatching;
+using Microsoft.UI.Text;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using TheArtOfDev.HtmlRenderer.Core.Entities;
+using TheArtOfDev.HtmlRenderer.Demo.Common;
+
+namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
+{
+ public sealed partial class MainControl : UserControl
+ {
+ #region Fields and Consts
+
+ ///
+ /// the name of the tree node root for all performance samples
+ ///
+ private const string PerformanceSamplesTreeNodeName = "Performance Samples";
+
+ ///
+ /// The sample of each tree node, the node itself only shows the name
+ ///
+ private readonly Dictionary _nodeToSample = new Dictionary();
+
+ ///
+ /// timer to update the rendered html when html in editor changes with delay
+ ///
+ private readonly DispatcherQueueTimer _updateHtmlTimer;
+
+ ///
+ /// The html last set to the editor by the code, to tell it apart from the user's changes
+ ///
+ private string _setEditorText;
+
+ #endregion
+
+ public MainControl()
+ {
+ InitializeComponent();
+
+ _htmlPanel.RenderError += OnRenderError;
+ _htmlPanel.LinkClicked += OnLinkClicked;
+ _htmlPanel.StylesheetLoad += DemoUtils.OnStylesheetLoad;
+ _htmlPanel.ImageLoad += HtmlRenderingHelper.OnImageLoad;
+ _htmlPanel.LoadComplete += (sender, args) => _htmlPanel.ScrollToElement("C4");
+
+ _htmlTooltipLabel.AvoidImagesLateLoading = true;
+ _htmlTooltipLabel.StylesheetLoad += DemoUtils.OnStylesheetLoad;
+ _htmlTooltipLabel.ImageLoad += HtmlRenderingHelper.OnImageLoad;
+ _htmlTooltipLabel.Text = "
" + Common.Resources.Tooltip + "
";
+
+ _updateHtmlTimer = DispatcherQueue.CreateTimer();
+ _updateHtmlTimer.Interval = TimeSpan.FromSeconds(1);
+ _updateHtmlTimer.IsRepeating = false;
+ _updateHtmlTimer.Tick += OnUpdateHtmlTimerTick;
+
+ LoadSamples();
+ }
+
+ ///
+ /// used ignore html editor updates when updating separately
+ ///
+ public bool UpdateLock { get; set; }
+
+ ///
+ /// In browser view if to show original html or the html generated from the html control
+ ///
+ public bool UseGeneratedHtml { get; set; }
+
+ ///
+ /// Show\Hide the web browser viewer.
+ ///
+ public void ShowWebBrowserView(bool show)
+ {
+ _webView.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
+ _viewsGrid.ColumnDefinitions[1].Width = show ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
+
+ if (show)
+ UpdateWebBrowserHtml();
+ }
+
+ ///
+ /// Update the html shown in the web browser
+ ///
+ public async void UpdateWebBrowserHtml()
+ {
+ if (_webView.Visibility == Visibility.Visible)
+ {
+ try
+ {
+ await _webView.EnsureCoreWebView2Async();
+ _webView.NavigateToString(UseGeneratedHtml ? _htmlPanel.GetHtml() : GetFixedHtml());
+ }
+ catch (Exception ex)
+ {
+ await WindowHelper.ShowMessageAsync(XamlRoot, ex.ToString(), "Failed to show the HTML in WebView2");
+ }
+ }
+ }
+
+ public string GetHtml()
+ {
+ return UseGeneratedHtml ? _htmlPanel.GetHtml() : GetHtmlEditorText();
+ }
+
+ ///
+ /// Set the html and lay it out right away, used to measure the performance.
+ ///
+ public void SetHtml(string html)
+ {
+ _htmlPanel.Text = html;
+ _htmlPanel.UpdateLayout();
+ }
+
+ #region Private methods
+
+ ///
+ /// Loads the tree of document samples
+ ///
+ private void LoadSamples()
+ {
+ var showcaseRoot = AddRootNode("HTML Renderer", SamplesLoader.ShowcaseSamples);
+ AddRootNode("Test Samples", SamplesLoader.TestSamples);
+ if (SamplesLoader.PerformanceSamples.Count > 0)
+ AddRootNode(PerformanceSamplesTreeNodeName, SamplesLoader.PerformanceSamples);
+
+ showcaseRoot.IsExpanded = true;
+ if (showcaseRoot.Children.Count > 0)
+ {
+ _samplesTreeView.SelectedNode = showcaseRoot.Children[0];
+ ShowSample(showcaseRoot.Children[0]);
+ }
+ }
+
+ private TreeViewNode AddRootNode(string name, List samples)
+ {
+ var root = new TreeViewNode { Content = name };
+ _samplesTreeView.RootNodes.Add(root);
+ foreach (var sample in samples)
+ {
+ var node = new TreeViewNode { Content = sample.Name };
+ _nodeToSample[node] = sample;
+ root.Children.Add(node);
+ }
+ return root;
+ }
+
+ ///
+ /// On tree view node click load the html to the html panel and html editor.
+ ///
+ private void OnSamplesTreeView_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
+ {
+ if (args.AddedItems.Count > 0 && args.AddedItems[0] is TreeViewNode node)
+ ShowSample(node);
+ }
+
+ private void ShowSample(TreeViewNode node)
+ {
+ if (!_nodeToSample.TryGetValue(node, out var sample))
+ return;
+
+ UpdateLock = true;
+
+ // Coloring the large performance samples takes too long
+ SetEditorText(sample.Html, !Equals(node.Parent?.Content, PerformanceSamplesTreeNodeName) && _coloredCheckBox.IsChecked == true);
+
+ try
+ {
+ _htmlPanel.AvoidImagesLateLoading = !sample.FullName.Contains("Many images");
+ _htmlPanel.Text = sample.Html;
+ UpdateFixedHtmlView();
+ }
+ catch (Exception ex)
+ {
+ SetFixedHtmlText(string.Empty, false);
+ _ = WindowHelper.ShowMessageAsync(XamlRoot, ex.ToString(), "Failed to render HTML");
+ }
+
+ UpdateLock = false;
+
+ UpdateWebBrowserHtml();
+ }
+
+ ///
+ /// On text change in the html editor update
+ ///
+ private void OnHtmlEditor_TextChanged(object sender, RoutedEventArgs e)
+ {
+ // TextChanged is raised later, also for the text set by the code
+ if (!UpdateLock && GetHtmlEditorText() != _setEditorText)
+ {
+ _updateHtmlTimer.Stop();
+ _updateHtmlTimer.Start();
+ }
+ }
+
+ ///
+ /// Update the html renderer with text from html editor.
+ ///
+ private void OnUpdateHtmlTimerTick(DispatcherQueueTimer sender, object args)
+ {
+ UpdateLock = true;
+
+ try
+ {
+ _htmlPanel.Text = GetHtmlEditorText();
+ UpdateFixedHtmlView();
+ }
+ catch (Exception ex)
+ {
+ SetFixedHtmlText(string.Empty, false);
+ _ = WindowHelper.ShowMessageAsync(XamlRoot, ex.ToString(), "Failed to render HTML");
+ }
+
+ UpdateWebBrowserHtml();
+
+ UpdateLock = false;
+ }
+
+ ///
+ /// Fix the raw html by replacing the resource keys of images and stylesheets with their data,
+ /// a page shown from a string can't load local files.
+ ///
+ /// fixed html
+ private string GetFixedHtml()
+ {
+ var html = GetHtmlEditorText();
+
+ html = Regex.Replace(html, @"src=\""(\w.*?)\""", match =>
+ {
+ var dataUri = HtmlRenderingHelper.TryGetResourceImageDataUri(match.Groups[1].Value);
+ return dataUri != null ? string.Format("src=\"{0}\"", dataUri) : match.Value;
+ }, RegexOptions.IgnoreCase);
+
+ html = Regex.Replace(html, @"href=\""(\w.*?)\""", match =>
+ {
+ var stylesheet = DemoUtils.GetStylesheet(match.Groups[1].Value);
+ return stylesheet != null
+ ? string.Format("href=\"data:text/css;base64,{0}\"", Convert.ToBase64String(Encoding.UTF8.GetBytes(stylesheet)))
+ : match.Value;
+ }, RegexOptions.IgnoreCase);
+
+ return html;
+ }
+
+ ///
+ /// Update the fixed html shown in the read-only editor.
+ ///
+ private void UpdateFixedHtmlView()
+ {
+ SetFixedHtmlText(HtmlPrettyPrinter.Format(_htmlPanel.GetHtml() ?? string.Empty), _coloredCheckBox.IsChecked == true);
+ }
+
+ ///
+ /// Set formatted HTML text in the fixed HTML viewer.
+ ///
+ private void SetFixedHtmlText(string text, bool color)
+ {
+ // The document of a read-only box can't be changed
+ _fixedHtmlEditor.IsReadOnly = false;
+ SetRtf(_fixedHtmlEditor, text ?? string.Empty, color);
+ _fixedHtmlEditor.IsReadOnly = true;
+ }
+
+ ///
+ /// Reload the html shown in the html editor by running coloring again.
+ ///
+ private void OnRefresh_Click(object sender, RoutedEventArgs e)
+ {
+ SetEditorText(GetHtmlEditorText(), true);
+ }
+
+ ///
+ /// Toggle if the html syntax is colored.
+ ///
+ private void OnColored_Click(object sender, RoutedEventArgs e)
+ {
+ SetEditorText(GetHtmlEditorText(), _coloredCheckBox.IsChecked == true);
+ UpdateFixedHtmlView();
+ }
+
+ ///
+ /// Show error raised from html renderer.
+ ///
+ private void OnRenderError(object sender, HtmlRenderErrorEventArgs args)
+ {
+ _ = WindowHelper.ShowMessageAsync(XamlRoot, args.Message + (args.Exception != null ? "\r\n" + args.Exception : null), "Error in Html Renderer");
+ }
+
+ ///
+ /// On specific link click handle it here.
+ ///
+ private void OnLinkClicked(object sender, HtmlLinkClickedEventArgs args)
+ {
+ if (args.Link == "SayHello")
+ {
+ _ = WindowHelper.ShowMessageAsync(XamlRoot, "Hello you!", "Hello");
+ args.Handled = true;
+ }
+ else if (args.Link == "ShowSampleForm")
+ {
+ new SampleWindow().Activate();
+ args.Handled = true;
+ }
+ }
+
+ ///
+ /// Set the html in the editor without triggering an update of the rendered html.
+ ///
+ private void SetEditorText(string text, bool color)
+ {
+ SetRtf(_htmlEditor, text, color);
+ _setEditorText = GetHtmlEditorText();
+ }
+
+ ///
+ /// Set html syntax colored text, uncolored text goes through the highlighter as well to escape it for RTF.
+ ///
+ private static void SetRtf(RichEditBox editor, string text, bool color)
+ {
+ var black = System.Drawing.Color.Black;
+ var rtf = color ? HtmlSyntaxHighlighter.Process(text) : HtmlSyntaxHighlighter.Process(text, black, black, black, black, black, black);
+
+ // RichEditBox shows the RTF as text without a version and charset in the header
+ if (rtf.StartsWith(@"{\rtf{", StringComparison.Ordinal))
+ rtf = @"{\rtf1\ansi" + rtf.Substring(5);
+
+ editor.Document.SetText(TextSetOptions.FormatRtf, rtf);
+ }
+
+ ///
+ /// Get the html text from the html editor control.
+ ///
+ private string GetHtmlEditorText()
+ {
+ _htmlEditor.Document.GetText(TextGetOptions.UseCrlf, out var text);
+ return text;
+ }
+
+ #endregion
+ }
+}
diff --git a/Source/Demo/WinUI/MainWindow.xaml b/Source/Demo/WinUI/MainWindow.xaml
index 44b3b4aea..59792b615 100644
--- a/Source/Demo/WinUI/MainWindow.xaml
+++ b/Source/Demo/WinUI/MainWindow.xaml
@@ -2,19 +2,29 @@
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">
-
+ xmlns:local="using:TheArtOfDev.HtmlRenderer.Demo.WinUI">
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Demo/WinUI/MainWindow.xaml.cs b/Source/Demo/WinUI/MainWindow.xaml.cs
index 8fb7304d4..853a6572d 100644
--- a/Source/Demo/WinUI/MainWindow.xaml.cs
+++ b/Source/Demo/WinUI/MainWindow.xaml.cs
@@ -11,176 +11,118 @@
// "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 PdfSharp;
using TheArtOfDev.HtmlRenderer.Demo.Common;
-using Windows.Storage.Streams;
+using TheArtOfDev.HtmlRenderer.PdfSharp;
+using Windows.ApplicationModel.DataTransfer;
namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
{
- ///
- /// Hosts a of the same showcase/test/performance samples the WPF and WinForms
- /// demos expose (via the shared, UI-framework-agnostic /
- /// from HtmlRenderer.Demo.Common) next to a
- /// that renders whichever sample is selected.
- ///
- ///
- /// Node content is a plain (the sample/category name), matching Microsoft's own
- /// documented RootNodes pattern exactly (see
- /// https://learn.microsoft.com/en-us/windows/apps/develop/ui/controls/tree-view) - no
- /// 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
- /// TreeView.ItemTemplate's {Binding Name}, which rendered no text and never expanded -
- /// switched to this simpler, documented shape instead of chasing the exact binding-context rules for
- /// that pattern. maps each leaf node back to its
- /// since the node's own displayed Content is just its name.
- ///
public sealed partial class MainWindow : Window
{
- private readonly Dictionary _nodeToSample = new Dictionary();
-
public MainWindow()
{
InitializeComponent();
- Title = "HTML Renderer - WinUI 3 Demo";
-
- // Unlike the WPF/WinForms demo windows, a WinUI 3 window does not pick up the executable
- // icon stamped by the csproj ApplicationIcon property, so the title bar and taskbar show the
- // generic default until AppWindow is pointed at the .ico explicitly. BaseDirectory rather
- // than a relative path: SetIcon resolves against the working directory, which is not the app
- // directory when the demo is launched from elsewhere.
- AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "html.ico"));
-
- SamplesLoader.Init("WinUI", typeof(TheArtOfDev.HtmlRenderer.WinUI.HtmlRender).Assembly.GetName().Version.ToString());
-
- // Without this, showcase samples' 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 falls through to the built-in "failed to load" icon instead.
- RendererPanel.ImageLoad += OnImageLoad;
-
- LoadSamples();
+ WindowHelper.Setup(this, "HTML Renderer WinUI Demo", 0.7, 0.8);
}
- private void LoadSamples()
+ ///
+ /// Open sample window.
+ ///
+ private void OnOpenSampleWindow_Click(object sender, RoutedEventArgs e)
{
- 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]);
- }
+ new SampleWindow().Activate();
}
- private void AddSampleNode(TreeViewNode root, HtmlSample sample)
+ ///
+ /// Toggle if to show split view of HtmlPanel and WebView2 control.
+ ///
+ private void OnShowBrowserView_Click(object sender, RoutedEventArgs e)
{
- var node = new TreeViewNode { Content = sample.Name };
- _nodeToSample[node] = sample;
- root.Children.Add(node);
+ _mainControl.ShowWebBrowserView(_showBrowserView.IsChecked == true);
}
- private void OnSamplesTreeViewSelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args)
+ ///
+ /// Open the current html in the default browser.
+ ///
+ private void OnOpenExternal_Click(object sender, RoutedEventArgs e)
{
- foreach (var node in sender.SelectedNodes)
- {
- if (_nodeToSample.TryGetValue(node, out var sample))
- {
- ShowSample(sample);
- break;
- }
- }
+ var tmpFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".htm");
+ File.WriteAllText(tmpFile, _mainControl.GetHtml());
+ Process.Start(new ProcessStartInfo(tmpFile) { UseShellExecute = true });
}
- private void ShowSample(HtmlSample sample)
+ ///
+ /// Toggle the use generated html button state.
+ ///
+ private void OnUseGeneratedHtml_Click(object sender, RoutedEventArgs e)
{
- try
- {
- RendererPanel.AvoidImagesLateLoading = !sample.FullName.Contains("Many images");
- RendererPanel.Text = sample.Html;
- }
- catch (Exception ex)
- {
- Debug.WriteLine(ex);
- }
+ _mainControl.UseGeneratedHtml = _useGeneratedHtml.IsChecked == true;
+ _mainControl.UpdateWebBrowserHtml();
}
///
- /// Resolves the demo's logical icon keys (e.g. src="HtmlIcon") via
- /// - real URLs/paths DemoUtils doesn't recognize fall
- /// through untouched ( left false) so the default
- /// loader still handles them.
+ /// Open generate image window for the current html.
///
- private void OnImageLoad(object sender, HtmlImageLoadEventArgs e)
+ private void OnGenerateImage_Click(object sender, RoutedEventArgs e)
{
- var stream = DemoUtils.GetImageStream(e.Src);
- if (stream == null)
- return;
-
- e.Handled = true;
- _ = LoadIconAsync(e, stream);
+ new GenerateImageWindow(_mainControl.GetHtml()).Activate();
}
- private static async Task LoadIconAsync(HtmlImageLoadEventArgs e, Stream stream)
+ ///
+ /// Create PDF using PdfSharp project, save to file and open that file.
+ ///
+ private async void OnGeneratePdf_Click(object sender, RoutedEventArgs e)
{
try
{
- byte[] bytes;
- using (stream)
- using (var memory = new MemoryStream())
- {
- await stream.CopyToAsync(memory).ConfigureAwait(false);
- bytes = memory.ToArray();
- }
+ var config = new PdfGenerateConfig();
+ config.PageSize = PageSize.A4;
+ config.SetMargins(20);
- using var randomAccessStream = new InMemoryRandomAccessStream();
- using (var writer = new DataWriter(randomAccessStream.GetOutputStreamAt(0)))
+ var html = _mainControl.GetHtml();
+
+ // Off the UI thread, as the image conversion blocks on Win2D
+ var pdfFile = await Task.Run(async () =>
{
- 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);
+ var doc = await PdfGenerator.GeneratePdf(html, config, null, DemoUtils.OnStylesheetLoad, HtmlRenderingHelper.OnImageLoadPdfSharp);
+ var file = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".pdf");
+ doc.Save(file);
+ return file;
+ });
+
+ Process.Start(new ProcessStartInfo(pdfFile) { UseShellExecute = true });
}
catch (Exception ex)
{
- Debug.WriteLine(ex);
- e.Callback();
+ await WindowHelper.ShowMessageAsync(Content.XamlRoot, ex.ToString(), "Failed to generate PDF");
}
}
+
+ ///
+ /// Execute performance test by setting all sample HTMLs in a loop.
+ ///
+ private async void OnRunPerformance_Click(object sender, RoutedEventArgs e)
+ {
+ _mainControl.UpdateLock = true;
+ _toolBar.IsEnabled = false;
+
+ // There's no paint in between, unlike WinForms and WPF, WinUI has no way to pump the messages
+ var msg = DemoUtils.RunSamplesPerformanceTest(_mainControl.SetHtml);
+
+ var dataPackage = new DataPackage();
+ dataPackage.SetText(msg);
+ Clipboard.SetContent(dataPackage);
+
+ await WindowHelper.ShowMessageAsync(Content.XamlRoot, msg, "Test run results");
+
+ _mainControl.UpdateLock = false;
+ _toolBar.IsEnabled = true;
+ }
}
}
diff --git a/Source/Demo/WinUI/SampleWindow.xaml b/Source/Demo/WinUI/SampleWindow.xaml
new file mode 100644
index 000000000..8582913cf
--- /dev/null
+++ b/Source/Demo/WinUI/SampleWindow.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Demo/WinUI/SampleWindow.xaml.cs b/Source/Demo/WinUI/SampleWindow.xaml.cs
new file mode 100644
index 000000000..3fdb4ff2f
--- /dev/null
+++ b/Source/Demo/WinUI/SampleWindow.xaml.cs
@@ -0,0 +1,87 @@
+// "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;
+using Microsoft.UI.Xaml.Input;
+using TheArtOfDev.HtmlRenderer.Demo.Common;
+using TheArtOfDev.HtmlRenderer.WinUI;
+
+namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
+{
+ public sealed partial class SampleWindow : Window
+ {
+ private HtmlControl _selectedControl;
+
+ public SampleWindow()
+ {
+ InitializeComponent();
+
+ WindowHelper.Setup(this, "Sample Window", 0.56, 0.64);
+
+ _htmlLabel.Text = DemoUtils.SampleHtmlLabelText;
+ _htmlPanel.Text = DemoUtils.SampleHtmlPanelText;
+
+ // The controls handle the pointer events themselves, so listen to the handled ones as well
+ _htmlLabel.AddHandler(UIElement.PointerReleasedEvent, new PointerEventHandler(OnHtmlControl_PointerReleased), true);
+ _htmlPanel.AddHandler(UIElement.PointerReleasedEvent, new PointerEventHandler(OnHtmlControl_PointerReleased), true);
+
+ SelectControl(_htmlLabel);
+ }
+
+ private void OnHtmlControl_PointerReleased(object sender, PointerRoutedEventArgs e)
+ {
+ SelectControl((HtmlControl)sender);
+ }
+
+ private void SelectControl(HtmlControl control)
+ {
+ _selectedControl = control;
+
+ var label = control as HtmlLabel;
+ _selectedControlText.Text = label != null ? "HtmlLabel" : "HtmlPanel";
+ _isSelectionEnabled.IsChecked = control.IsSelectionEnabled;
+ _isContextMenuEnabled.IsChecked = control.IsContextMenuEnabled;
+ _avoidImagesLateLoading.IsChecked = control.AvoidImagesLateLoading;
+ _autoSize.IsChecked = label?.AutoSize ?? false;
+ _autoSizeHeightOnly.IsChecked = label?.AutoSizeHeightOnly ?? false;
+ _autoSize.IsEnabled = _autoSizeHeightOnly.IsEnabled = label != null;
+ _text.Text = control.Text ?? string.Empty;
+ _baseStylesheet.Text = control.BaseStylesheet ?? string.Empty;
+ }
+
+ private void OnPropertyChanged(object sender, RoutedEventArgs e)
+ {
+ _selectedControl.IsSelectionEnabled = _isSelectionEnabled.IsChecked == true;
+ _selectedControl.IsContextMenuEnabled = _isContextMenuEnabled.IsChecked == true;
+ _selectedControl.AvoidImagesLateLoading = _avoidImagesLateLoading.IsChecked == true;
+
+ if (_selectedControl is HtmlLabel label)
+ {
+ if (ReferenceEquals(sender, _autoSize))
+ label.AutoSize = _autoSize.IsChecked == true;
+ else if (ReferenceEquals(sender, _autoSizeHeightOnly))
+ label.AutoSizeHeightOnly = _autoSizeHeightOnly.IsChecked == true;
+
+ // AutoSize and AutoSizeHeightOnly exclude each other
+ _autoSize.IsChecked = label.AutoSize;
+ _autoSizeHeightOnly.IsChecked = label.AutoSizeHeightOnly;
+ }
+
+ if (_selectedControl.BaseStylesheet != _baseStylesheet.Text)
+ _selectedControl.BaseStylesheet = _baseStylesheet.Text;
+ if (_selectedControl.Text != _text.Text)
+ _selectedControl.Text = _text.Text;
+
+ _selectedControl.InvalidateMeasure();
+ }
+ }
+}
diff --git a/Source/Demo/WinUI/WindowHelper.cs b/Source/Demo/WinUI/WindowHelper.cs
new file mode 100644
index 000000000..565276807
--- /dev/null
+++ b/Source/Demo/WinUI/WindowHelper.cs
@@ -0,0 +1,76 @@
+// "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.IO;
+using System.Threading.Tasks;
+using Microsoft.UI;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Graphics;
+using Windows.Storage.Pickers;
+
+namespace TheArtOfDev.HtmlRenderer.Demo.WinUI
+{
+ internal static class WindowHelper
+ {
+ ///
+ /// Set the demo icon, size the window relative to the work area and center it.
+ ///
+ public static void Setup(Window window, string title, double widthFactor, double heightFactor)
+ {
+ window.Title = title;
+
+ // A WinUI window doesn't take the icon from the executable
+ window.AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "html.ico"));
+
+ var workArea = DisplayArea.GetFromWindowId(window.AppWindow.Id, DisplayAreaFallback.Primary).WorkArea;
+ var width = (int)(workArea.Width * widthFactor);
+ var height = (int)(workArea.Height * heightFactor);
+ window.AppWindow.MoveAndResize(new RectInt32(workArea.X + (workArea.Width - width) / 2, workArea.Y + (workArea.Height - height) / 2, width, height));
+ }
+
+ ///
+ /// Create a file picker for the window, an unpackaged app has to hand over the window handle.
+ ///
+ public static FileSavePicker CreateSavePicker(Window window)
+ {
+ var picker = new FileSavePicker();
+ WinRT.Interop.InitializeWithWindow.Initialize(picker, Win32Interop.GetWindowFromWindowId(window.AppWindow.Id));
+ return picker;
+ }
+
+ ///
+ /// Show a message box like dialog.
+ ///
+ public static async Task ShowMessageAsync(XamlRoot xamlRoot, string message, string title)
+ {
+ var dialog = new ContentDialog
+ {
+ XamlRoot = xamlRoot,
+ Title = title,
+ Content = new ScrollViewer { Content = new TextBlock { Text = message, IsTextSelectionEnabled = true, TextWrapping = TextWrapping.Wrap } },
+ CloseButtonText = "OK",
+ };
+
+ try
+ {
+ await dialog.ShowAsync();
+ }
+ catch (Exception)
+ {
+ // Only one dialog can be open at a time, a second error while one is shown is dropped
+ }
+ }
+ }
+}
From acaa08ef4a71ba3f166d09482025c9136afff765 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?=
Date: Fri, 13 Nov 2026 16:09:45 +0100
Subject: [PATCH 3/3] Package and publish HtmlRenderer.WinUI
- Depend on Win2D only, which works with Windows App SDK 1.8 or newer
- Pack the WinUI package in CI on Windows
- Fix the quick start and list the requirements in the README
---
.github/workflows/build.yml | 11 +++---
README.md | 6 ++--
.../HtmlRenderer.WinUI.csproj | 19 +++++-----
Source/HtmlRenderer.WinUI/README.md | 35 ++++++++++++-------
Source/HtmlRenderer/HtmlRenderer.csproj | 6 ++--
5 files changed, 44 insertions(+), 33 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2f3e6ab48..07faf1b4d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -49,12 +49,7 @@ jobs:
- 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.
+ # WinUI needs the Windows-only XAML and MSIX tooling, the other platforms build everything else
$target = $IsWindows ? "Source/HtmlRenderer.sln" : "Source/HtmlRenderer.CrossPlatform.slnf"
echo "path=$target" >> $env:GITHUB_OUTPUT
@@ -107,6 +102,10 @@ jobs:
- name: Create HtmlRenderer.WPF NuGet package
run: dotnet pack Source/HtmlRenderer.WPF/HtmlRenderer.WPF.csproj --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg --no-build --verbosity normal --output ${{ env.NuGetDirectory }}
+ - name: Create HtmlRenderer.WinUI NuGet package
+ if: runner.os == 'Windows'
+ run: dotnet pack Source/HtmlRenderer.WinUI/HtmlRenderer.WinUI.csproj --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg --no-build --verbosity normal --output ${{ env.NuGetDirectory }}
+
- name: Upload NuGet package artifacts
uses: actions/upload-artifact@v7
with:
diff --git a/README.md b/README.md
index 0a8553037..1c8dde57d 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# HTML Renderer
-**Cross framework** (WinForms/WPF/PDF/Metro/Mono/etc.), **Multipurpose** (UI Controls / Image generation / PDF generation / etc.), **100% managed** (C#), High performance HTML Rendering library.
+**Cross framework** (WinForms/WPF/WinUI/PDF/Mono/etc.), **Multipurpose** (UI Controls / Image generation / PDF generation / etc.), **100% managed** (C#), High performance HTML Rendering library.
The library is 100% managed **C#** code without any external dependencies (no WebBrowser control, ActiveX / COM or MSHTML dll), the only requirement is **.NET Standard 2.0 / .NET 8.0 or higher**.
@@ -22,6 +22,7 @@ The latest NuGet packages can be found on NuGet.org:
* [HtmlRenderer.WinForms](https://www.nuget.org/packages/HtmlRenderer.WinForms)
* [HtmlRenderer.WPF](https://www.nuget.org/packages/HtmlRenderer.WPF)
+* [HtmlRenderer.WinUI](https://www.nuget.org/packages/HtmlRenderer.WinUI)
* [HtmlRenderer.PdfSharp](https://www.nuget.org/packages/HtmlRenderer.PdfSharp)
* [HtmlRenderer.Core](https://www.nuget.org/packages/HtmlRenderer.Core)
@@ -33,6 +34,7 @@ The latest NuGet packages can be found on NuGet.org:
* Support text selection, copy-paste and context menu.
* WinForms controls: HtmlPanel, HtmlLabel and HtmlToolTip.
* WPF controls: HtmlPanel and HtmlLabel.
+* WinUI 3 controls: HtmlPanel and HtmlLabel.
* Works on Mono.
* Create images/PDFs from HTML snippets.
* Handles "real world" malformed HTML, it doesn't have to be XHTML.
@@ -43,7 +45,7 @@ The latest NuGet packages can be found on NuGet.org:
* Powerful [Demo application](https://codeplexarchive.org/ProjectTab/Wiki/HtmlRenderer/Documentation/Demo%20application) to explore and learn the library.
-## WinForms/WPF controls
+## WinForms/WPF/WinUI controls
* *HtmlPanel* - The full power of HTML control build to replace WebBrowser control, accepts HTML, text selection, scrollbars, link click intercept, image load intercept and much more.
* *HtmlLabel* - As WinForms label but accepts HTML, text selection, auto-size capabilities, transparent background and more.
diff --git a/Source/HtmlRenderer.WinUI/HtmlRenderer.WinUI.csproj b/Source/HtmlRenderer.WinUI/HtmlRenderer.WinUI.csproj
index ab640e6bc..406d8e42e 100644
--- a/Source/HtmlRenderer.WinUI/HtmlRenderer.WinUI.csproj
+++ b/Source/HtmlRenderer.WinUI/HtmlRenderer.WinUI.csproj
@@ -5,30 +5,29 @@
LibraryTheArtOfDev.HtmlRenderer.WinUItrue
- truetrue
- None
- false
+
+ falseHtmlRenderer.WinUIHTML Renderer for WinUI 3
- html render renderer draw control WinUI Win2D
+ html render renderer draw control WinUI WinUI3 Win2D WindowsAppSDKMultipurpose (UI Controls / Image generation), 100% managed (C#), High performance HTML Rendering library for WinUI 3.
-HTML UI in .NET WinUI 3 applications using controls or static rendering.
+HTML UI in .NET WinUI 3 applications using controls or static rendering, drawn with Win2D.
Features and Benefits:
---
* Controls: HtmlPanel, HtmlLabel.
* Create images from HTML snippets.
-* 100% managed code and no external dependencies, no ActiveX, no MSHTML.
+* 100% managed code, no ActiveX, no MSHTML, no WebView2.
* Extensive HTML 4.01 and CSS level 2 specifications support.
* Support separating CSS from HTML by loading stylesheet code separately.
-* Support text selection, copy-paste and context menu.
+* Support text selection, copy-paste, drag & drop and context menu.
* Handles "real world" malformed HTML, it doesn't have to be XHTML.
-* Lightweight, only two DLLs (~300K).
+* Works with Windows App SDK 1.8 or newer.
* High performance and low memory footprint.
* Extendable and configurable.Multipurpose (UI Controls / Image generation), 100% managed (C#), High performance HTML Rendering library for WinUI 3.
@@ -37,8 +36,8 @@ Features and Benefits:
-
-
+
+
diff --git a/Source/HtmlRenderer.WinUI/README.md b/Source/HtmlRenderer.WinUI/README.md
index fe9835718..938a57c77 100644
--- a/Source/HtmlRenderer.WinUI/README.md
+++ b/Source/HtmlRenderer.WinUI/README.md
@@ -1,9 +1,20 @@
# Welcome to the HTML Renderer WinUI 3 library!
This library provides the rich formatting power of HTML in your WinUI 3 .NET applications using
-simple controls or static rendering code.
+simple controls or static rendering code, drawn with Win2D.
For more info see HTML Renderer on GitHub: https://github.com/ArthurHub/HTML-Renderer
+## REQUIREMENTS
+
+- A WinUI 3 app on Windows App SDK 1.8 or newer, packaged or unpackaged
+- Windows 10 version 1809 (build 17763) or newer
+- A platform specific build (x86, x64 or arm64), as Win2D ships a native DLL per platform
+
+## DEMO APPLICATION
+
+HTML Renderer Demo application showcases HTML Renderer capabilities, use it to explore and learn
+on the library: https://github.com/ArthurHub/HTML-Renderer/tree/master/Source/Demo/WinUI
+
## FEEDBACK / RELEASE NOTES
If you have problems, wish to report a bug, or have a suggestion, please open an issue on the
@@ -16,12 +27,12 @@ For full release notes and all versions see: https://github.com/ArthurHub/HTML-R
## Quick Start: Use HTML panel control on a WinUI 3 window
```xaml
-
+ xmlns:html="using:TheArtOfDev.HtmlRenderer.WinUI">
-
+
```
@@ -29,12 +40,12 @@ For full release notes and all versions see: https://github.com/ArthurHub/HTML-R
## Quick Start: Create image from HTML snippet
```csharp
-class Program
-{
- private static async Task Main(string[] args)
- {
- var bitmap = await HtmlRender.RenderToImageAsync("