Skip to content

Add Uno SVG control package, tests, sample, and docs#474

Merged
wieslawsoltes merged 6 commits into
masterfrom
feature/svg-controls-skia-uno
Mar 21, 2026
Merged

Add Uno SVG control package, tests, sample, and docs#474
wieslawsoltes merged 6 commits into
masterfrom
feature/svg-controls-skia-uno

Conversation

@wieslawsoltes
Copy link
Copy Markdown
Owner

PR Summary: Add Svg.Controls.Skia.Uno

Branch

  • feature/svg-controls-skia-uno

Commit breakdown

  1. 3f265718 Add Svg.Controls.Skia.Uno package
  2. 7764851a Add Uno control tests
  3. f03cc9a5 Add Uno desktop and mobile sample
  4. cf2b9f51 Document Uno SVG package

Overview

This change adds a new Uno Platform control package, Svg.Controls.Skia.Uno, modeled after Svg.Controls.Skia.Avalonia where the API maps cleanly. The Uno implementation renders directly on Uno.WinUI.Graphics2DSK.SKCanvasElement and exposes the same core control-facing concepts: Path, Source, reusable SvgSource, stretch behavior, cache control, wireframe rendering, filter disabling, zoom/pan, hit testing, and access to the underlying SKSvg.

The package is intentionally scoped to the Uno Skia renderer path in v1. Avalonia-only concepts such as SvgImage, SvgResource, and markup extensions were not ported; Uno usage is centered on Svg plus reusable SvgSource resources.

Main implementation changes

New package: Svg.Controls.Skia.Uno

Added a new packable Uno library project under src/Svg.Controls.Skia.Uno/ with:

  • Uno.Svg.Skia.Svg
  • Uno.Svg.Skia.SvgSource
  • Uno.Svg.Skia.StretchDirection
  • internal cache-key and render-layout helpers

Key behavior:

  • Inherits from SKCanvasElement
  • Renders through RenderOverride(SKCanvas canvas, Size area)
  • Preserves SvgSource > Path > Source precedence
  • Supports EnableCache, Wireframe, DisableFilters, Zoom, PanX, PanY
  • Supports ZoomToPoint(...), TryGetPicturePoint(...), and HitTestElements(...)
  • Clones externally supplied SvgSource instances before applying per-control CSS or render options
  • Uses cache keys built from path + css + entities, not path alone

Uno-safe SvgSource

Added async loading APIs:

  • LoadAsync(...)
  • ReLoadAsync(...)

Path handling includes:

  • /Assets/... mapped to ms-appx:///Assets/...
  • support for ms-appx, file, and http(s) URIs
  • relative path resolution against baseUri

The implementation keeps sync loaders for:

  • inline SVG strings
  • Stream
  • SvgDocument

SDK and dependency alignment

Updated:

  • global.json to pin Uno.Sdk via msbuild-sdks
  • build/SkiaSharp.v3.props
  • build/SkiaSharp.Native.v3.props

The SkiaSharp v3 overrides were aligned to 3.119.1 because current Uno Skia packages restore against that version.

Tests and solution integration

Added tests/Svg.Controls.Skia.Uno.UnitTests/ and wired it into Svg.Skia.slnx.

Coverage added for:

  • SvgSource.LoadAsync(...)
  • SvgSource.ReLoadAsync(...)
  • SvgSource.Clone()
  • SvgSource.RebuildFromModel()
  • path normalization rules
  • cancellation behavior
  • cache-key correctness for CSS and entity changes
  • render-layout transform math
  • zoom-to-point math
  • parameter merging for shared SvgSource usage
  • isolation of a shared external SvgSource
  • propagation of Wireframe and DisableFilters

The standalone Uno sample was intentionally not added to the root solution so the default repo build/test path remains workload-free.

Sample app

Added samples/UnoSvgSkiaSample/ as a standalone Uno single-project sample.

The sample demonstrates:

  • asset load via Path
  • inline SVG load via Source
  • reusable SvgSource resources
  • runtime CSS restyling via CurrentCss
  • zoom/pan and hit testing
  • wireframe and filter toggles
  • desktop, WebAssembly, Android, and iOS project heads

Documentation updates

Added new docs pages:

  • site/articles/packages/svg-controls-skia-uno.md
  • site/articles/getting-started/quickstart-uno.md
  • site/articles/xaml/uno-svg-control.md
  • site/articles/advanced/uno-sample-publishing.md

Updated:

  • package menus
  • getting started menus
  • XAML menus
  • advanced menus
  • package overview docs
  • installation docs
  • reference/sample docs
  • API coverage docs
  • site/config.scriban to include Svg.Controls.Skia.Uno in generated API docs with a per-project TargetFramework=net10.0 override

Verification performed

Executed successfully:

  • dotnet build src/Svg.Controls.Skia.Uno/Svg.Controls.Skia.Uno.csproj -c Release
  • dotnet test tests/Svg.Controls.Skia.Uno.UnitTests/Svg.Controls.Skia.Uno.UnitTests.csproj -c Release
  • dotnet build samples/UnoSvgSkiaSample/UnoSvgSkiaSample.csproj -c Release -f net10.0-desktop
  • dotnet build Svg.Skia.slnx -c Release
  • dotnet test Svg.Skia.slnx -c Release
  • dotnet format --no-restore

Scope and current limits

  • Uno package is Skia-renderer-only in v1
  • no Uno SvgImage
  • no Uno brush/resource markup extension equivalent
  • no native-renderer fallback

Suggested PR description

Summary

Add a new Uno Platform SVG control package, Svg.Controls.Skia.Uno, backed by Svg.Skia and SKCanvasElement.

What changed

  • added Svg.Controls.Skia.Uno with Svg and SvgSource
  • added async Uno-safe asset loading and reload support
  • added Uno unit tests and solution integration
  • added a standalone Uno sample app
  • updated docs, menus, and generated API configuration

Validation

  • dotnet build Svg.Skia.slnx -c Release
  • dotnet test Svg.Skia.slnx -c Release
  • dotnet build samples/UnoSvgSkiaSample/UnoSvgSkiaSample.csproj -c Release -f net10.0-desktop

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf2b9f5169

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +517 to +518
var normalizedPath = SvgSource.NormalizePath(path).ToString();
var cacheKey = SvgCacheKey.Create(normalizedPath, parameters);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the XAML base URI for relative Path values

NormalizePath and LoadAsync are both invoked here without any baseUri, so Path="Assets/Icon.svg" is always treated as ms-appx:///Assets/Icon.svg instead of being resolved relative to the XAML file that declared the control. That means any Svg used from a page/resource dictionary in a subfolder cannot load page-relative assets even though SvgSource.NormalizePath(..., baseUri) explicitly supports that scenario.

Useful? React with 👍 / 👎.

Comment on lines +166 to +170
var candidateScaleX = hasWidth ? availableSize.Width / sourceSize.Width : 1.0;
var candidateScaleY = hasHeight ? availableSize.Height / sourceSize.Height : 1.0;

if (stretch == Stretch.Uniform)
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor single-axis constraints when computing Uniform size

When only one dimension is constrained and that constraint is larger than the SVG, the unconstrained axis is left at 1.0 and Stretch.Uniform ends up using Math.Min(..., 1.0). For example, a 100x50 SVG measured with availableSize=(∞, 200) returns 100x50 here instead of scaling to 400x200, so Svg controls with only Height or only Width set will fail to stretch up to the requested size.

Useful? React with 👍 / 👎.

Comment on lines +501 to +504
if (clone.HasPathSource)
{
await clone.ReLoadAsync(parameters, cancellationToken).ConfigureAwait(false);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep model edits on external path-backed SvgSource clones

This unconditionally reloads every path-backed SvgSource clone from disk/appx, even when no extra CSS/entities need to be applied. If a caller loads a SvgSource, edits source.Svg.Model, calls RebuildFromModel(), and then assigns that object to Svg.SvgSource, those in-memory changes are discarded and the control renders the original file contents instead of the edited model.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ebcc161acb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +462 to +466
result = await LoadExternalSourceAsync(externalSource, cancellationToken).ConfigureAwait(false);
}
else if (!string.IsNullOrWhiteSpace(Path))
{
result = await LoadPathAsync(Path!, cancellationToken).ConfigureAwait(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Marshal async reload completions to the dispatcher

ReloadSourceAsync awaits the path/external-source loaders with ConfigureAwait(false) and then immediately calls SetCurrentSource, ClearSource, and the invalidate methods on the control. For any non-inline load (Path or a path-backed SvgSource), that continuation can resume on a worker thread after file/ms-appx/http I/O, but SKCanvasElement/DependencyObject state in Uno is dispatcher-affine. In practice this makes async image loads prone to cross-thread exceptions or redraw races as soon as the control loads from anything other than the inline Source code path.

Useful? React with 👍 / 👎.

Comment on lines +486 to +490
{
Debug.WriteLine("Failed to load Uno svg control source.");
Debug.WriteLine(e);
DisposeResultIfOwned(result);
ClearSource();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ignore stale load failures before clearing the control

This catch always calls ClearSource() even if the failing task belongs to an older loadVersion. If Path/SvgSource changes quickly and the earlier request throws after a newer one has started, the stale task will cancel _pendingLoadCts and clear _svg, wiping out the newer request. That race is easy to hit with local files because SvgSource.LoadImplAsync only checks cancellation before parsing the file, so a malformed old SVG can still blank a later valid one.

Useful? React with 👍 / 👎.

previous.Dispose();
}

DisposeCache();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep cache storage alive while EnableCache is true

ClearSource() always disposes _cache, but _cache is only recreated when EnableCache changes from false to true. After any clear path (for example setting Path/Source to null or hitting a load error), subsequent LoadPathAsync calls run with EnableCache == true and _cache == null, so nothing is cached until the property is toggled off and back on. That silently disables the control's cache mode for the remainder of its lifetime.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69ef521929

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +113 to +116
if (path.StartsWith("/Assets/", StringComparison.OrdinalIgnoreCase)
|| string.Equals(path, "/Assets", StringComparison.OrdinalIgnoreCase))
{
return new Uri($"ms-appx://{path}", UriKind.Absolute);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map all rooted package paths to ms-appx URIs

Because only /Assets is special-cased here, a packaged SVG referenced as Path="/Images/logo.svg" falls through to the absolute-file handling and resolves to the local filesystem instead of ms-appx:///Images/logo.svg. That breaks any app asset stored outside the Assets folder for both Svg.Path and SvgSource.LoadAsync, even though those files are valid package resources.

Useful? React with 👍 / 👎.

Comment on lines +433 to +435
private void OnUnloaded(object sender, RoutedEventArgs e)
{
CancelPendingLoad();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry canceled loads when the control is loaded again

If a Path or SvgSource load is still in flight when the same Svg instance is temporarily removed from the visual tree (for example via ItemsRepeater virtualization or tab/page reuse), OnUnloaded cancels the request and there is no matching Loaded handler to queue it again. Since the source properties are unchanged, the control stays blank after it is reattached until some other property change forces QueueSourceReload().

Useful? React with 👍 / 👎.

Comment on lines +456 to +457
source._skSvg = skSvg;
source._picture = picture;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Dispose the previous renderer before overwriting it

Replacing _skSvg and _picture here (and in LoadFromCachedStream a few lines below) leaks the previous native objects on every in-place reload. Calls such as SvgSource.ReLoadAsync, PrepareWorkingSource on an already-loaded source, or path-backed SvgSource clones in LoadExternalSourceAsync will accumulate abandoned SKSvg/SKPicture instances when CSS or themes are changed repeatedly.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant