Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
64ea100
docs: plan SvgML foreignObject controls
wieslawsoltes Apr 21, 2026
af997bc
feat(svgml): add foreignObject inline model
wieslawsoltes Apr 21, 2026
f3c29fb
feat(svgml): host foreignObject controls in Avalonia
wieslawsoltes Apr 21, 2026
bfb70d7
feat(svgml): host foreignObject controls in Uno
wieslawsoltes Apr 21, 2026
87a044f
feat(svgml): host foreignObject controls in MAUI
wieslawsoltes Apr 21, 2026
7dc59d9
samples: demonstrate SvgML foreignObject controls
wieslawsoltes Apr 21, 2026
bf12251
fix: avoid MAUI SvgML sample XamlC failure
wieslawsoltes Apr 21, 2026
cb4ebb9
fix: restore MAUI SvgML XAML demos
wieslawsoltes Apr 21, 2026
afaa7a6
fix: load MAUI SvgML inline content
wieslawsoltes Apr 21, 2026
f34ffdb
fix: support literal SvgML text in samples
wieslawsoltes Apr 21, 2026
cade9ae
feat: support scene foreignObject controls
wieslawsoltes Apr 21, 2026
de48179
fix: preserve Uno SvgML text content
wieslawsoltes Apr 21, 2026
56e7af7
fix: transform Uno SvgML text nodes
wieslawsoltes Apr 21, 2026
78b91e2
Revert "fix: transform Uno SvgML text nodes"
wieslawsoltes Apr 22, 2026
f70e696
fix: render Uno SvgML tspan text
wieslawsoltes Apr 22, 2026
fb52e1c
fix: address SvgML foreignObject review issues
wieslawsoltes Apr 22, 2026
24b68eb
chore: target Avalonia demo net10 only
wieslawsoltes Apr 22, 2026
7b9adcd
fix: support dashed SvgML attributes in MAUI
wieslawsoltes Apr 22, 2026
43e6c2c
docs: document SvgML foreignObject controls
wieslawsoltes Apr 22, 2026
f994b59
docs: use default SvgML namespaces
wieslawsoltes Apr 22, 2026
d3f14f3
Bump version to 4.5.0
wieslawsoltes Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<Project>
<PropertyGroup>
<VersionPrefix>4.4.0</VersionPrefix>
<VersionPrefix>4.5.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
<AvaloniaVersionPrefix>12.0.0.4</AvaloniaVersionPrefix>
<AvaloniaVersionPrefix>12.0.0.5</AvaloniaVersionPrefix>
<AvaloniaVersionSuffix>$(VersionSuffix)</AvaloniaVersionSuffix>
<UnoVersionPrefix>6.5.31.5</UnoVersionPrefix>
<UnoVersionPrefix>6.5.31.6</UnoVersionPrefix>
<UnoVersionSuffix>$(VersionSuffix)</UnoVersionSuffix>
<MauiVersionPrefix>10.0.0</MauiVersionPrefix>
<MauiVersionPrefix>10.0.0.1</MauiVersionPrefix>
<MauiVersionSuffix>$(VersionSuffix)</MauiVersionSuffix>
<Authors>Wiesław Šoltés</Authors>
<Company>Wiesław Šoltés</Company>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ and the `Svg.Skia` renderer will provide more complete rendering subsystem imple
- `Svg.Custom` and `Svg.Skia` now include typed `pointer-events` handling plus geometry-aware topmost hit testing.
- `Svg.Skia` now includes a shared interaction dispatcher, shared animation clock/controller, and host-driven animation playback APIs.
- `Svg.Controls.Skia.Avalonia` and `Svg.Controls.Skia.Uno` now expose animation backend selection, playback rate, frame interval, and resolved-backend diagnostics.
- `SvgML.Avalonia`, `SvgML.Maui`, and `SvgML.Uno` now live in the same repository, so inline Avalonia, .NET MAUI, and Uno XAML-authored SVG trees build against the local `Svg.Skia` sources and ship from the same release pipeline.
- `SvgML.Avalonia`, `SvgML.Maui`, and `SvgML.Uno` now live in the same repository, so inline Avalonia, .NET MAUI, and Uno XAML-authored SVG trees, including native controls hosted through SVG `foreignObject`, build against the local `Svg.Skia` sources and ship from the same release pipeline.
- Avalonia adds an optional `NativeComposition` animation backend with fallback to `RenderLoop` or `DispatcherTimer` when retained composition is unavailable.
- `tests/Svg.Skia.Benchmarks` adds a local BenchmarkDotNet harness for the shared animation renderer, and `samples/TestApp` exposes backend and playback controls for manual verification.

Expand Down
2 changes: 1 addition & 1 deletion build/SvgML.Weaving.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<!-- Only the Avalonia package opts into CLR-name weaving. -->
<!-- SvgML UI packages opt into CLR-name weaving when their XAML compiler needs SVG names. -->
<SvgMLSelfWeaverEnabled Condition="'$(SvgMLSelfWeaverEnabled)' == ''">false</SvgMLSelfWeaverEnabled>
<ProduceReferenceAssembly Condition="'$(SvgMLSelfWeaverEnabled)' == 'true'">false</ProduceReferenceAssembly>
<SvgMLSelfWeaverTargetFramework Condition="'$(SvgMLSelfWeaverTargetFramework)' == ''">net10.0</SvgMLSelfWeaverTargetFramework>
Expand Down
83 changes: 83 additions & 0 deletions plan/svgml-foreignobject-inline-controls-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# SvgML `foreignObject` Inline Controls Update

Update date: 2026-04-21

## Goal

Use SVG `foreignObject` as the idiomatic SvgML host for native inline controls on Avalonia, Uno, and .NET MAUI.

`InlineUIContainer` has been removed; `foreignObject` is the single native-control host element.

This also covers non-inline SVG scene placement: a `foreignObject` with a native child can be authored anywhere a graphical SVG element is allowed, including directly under `svg` and inside transformed grouping containers.

## SVG Basis

- MDN describes `foreignObject` as the SVG element for including content from another namespace, most commonly HTML in browsers: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/foreignObject
- SVG 1.1 defines `x`, `y`, `width`, and `height` as the rectangular region into which the foreign content is placed: https://www.w3.org/TR/SVG11/extend.html#ForeignObjectElement
- SVG 1.1 also specifies that `x`/`y` default to `0`, and zero `width` or `height` disables rendering.

SvgML maps those rules onto native UI hosting:

- non-inline `foreignObject` uses its SVG rectangular bounds
- inline `foreignObject` reserves text-flow space from explicit `width`/`height` or the measured native control size
- missing non-inline `width`/`height` are written from the native desired size when a child control exists

## Architecture

### Shared model

`foreignObject` implements the hosted-control contract:

- `HostedControl` returns the platform-native child
- `GetHostedControlSize()` returns explicit SVG size when set, otherwise native desired size
- a generated stable mapping id is emitted when the author did not set `id`

When a `foreignObject` is authored inside SVG text, it serializes as a `tspan` placeholder with:

- generated or explicit id for retained-scene mapping
- invisible placeholder glyph
- `font-size` from resolved slot height
- `textLength` from resolved slot width
- `lengthAdjust="spacingAndGlyphs"`

This lets the SVG text engine reserve inline advance while the real native control is arranged by the platform overlay.

### Scene graph

`SvgForeignObject` is retained as a non-rendering scene node, but the scene compiler now assigns geometry bounds from `x`, `y`, `width`, and `height`.

This is required for non-inline hosted controls because the native child is not serialized into the SVG document and therefore does not contribute child geometry.

### Platform overlays

All platforms now use one hosted-control layout contract:

- enumerate hosted controls from the SvgML source tree
- detect inline placement by walking from the hosted element to an owning `text_base`
- compute inline slot positions from the source text tree
- use retained-scene `foreignObject` bounds for normal SVG-scene placement
- fall back to the source `foreignObject` slot (`x`, `y`, `width`, `height`) transformed through the element's total SVG transform when retained bounds are not available
- transform SVG picture-space bounds into platform control coordinates
- measure and arrange the native child in that slot

Avalonia hosts controls as logical/visual children of the root `svg`.

Uno hosts controls through retained popups positioned from the transformed SVG slot.

MAUI hosts controls in an `AbsoluteLayout` overlay above the Skia drawing surface.

## XML Namespaces

SvgML assemblies expose the `SvgML` CLR namespace through `https://github.com/svgml` where the XAML stack supports public assembly-level XML namespace definitions.

Avalonia also keeps the existing `https://github.com/avaloniaui` mapping so SvgML elements can be used unprefixed in Avalonia markup that already has Avalonia as the default namespace.

Uno uses the WinUI/Uno-supported `using:SvgML` XAML namespace form because Uno's `XmlnsDefinitionAttribute` is not publicly usable by application/library code.

## Samples

The Avalonia, Uno, and MAUI demos now demonstrate both inline controls and scene controls using `foreignObject`.

Scene controls show root-level placement and controls hosted inside translated SVG groups, with native buttons, text editors, sliders, and checkbox content arranged from SVG geometry.

Uno sample markup relies on measured native size because Uno XAML does not currently convert literal `SvgUnit` values for `foreignObject` attributes.
5 changes: 5 additions & 0 deletions plan/svgml-inline-ui-controls-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SvgML Inline UI Controls Plan

Superseded by `svgml-foreignobject-inline-controls-update.md`.

The initial implementation plan used a custom `InlineUIContainer` element as the public inline native-control host. The current design removes that custom element and uses SVG `foreignObject` as the single idiomatic public API for Avalonia, Uno, and MAUI.
179 changes: 166 additions & 13 deletions samples/SvgML.Avalonia.Demo/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="500"
mc:Ignorable="d" d:DesignWidth="960" d:DesignHeight="680"
x:Class="SvgML.Avalonia.Demo.MainWindow"
Width="800" Height="500"
Width="960" Height="680"
Title="SvgML.Avalonia Demo">

<Window.Styles>
Expand Down Expand Up @@ -52,16 +52,169 @@

</Window.Resources>

<StackPanel Spacing="20" VerticalAlignment="Center">
<Viewbox Width="100" Height="100">
<ContentPresenter Content="{DynamicResource SvgIcon}" />
</Viewbox>
<Viewbox Width="100" Height="100">
<ContentPresenter Content="{DynamicResource SvgIcon}" />
</Viewbox>
<Viewbox Width="100" Height="100">
<ContentPresenter Content="{DynamicResource SvgIcon}" />
</Viewbox>
</StackPanel>
<TabControl Margin="16" SelectedIndex="1">
<TabItem Header="Animated Resource">
<ScrollViewer>
<StackPanel Margin="24" Spacing="20">
<TextBlock Text="Animated resource reuse"
FontSize="26"
FontWeight="SemiBold" />
<TextBlock MaxWidth="760"
TextWrapping="Wrap"
Text="The same SvgML resource is instantiated three times. Each ContentPresenter gets an independent animated SVG tree because the resource is marked x:Shared=False." />
<StackPanel Orientation="Horizontal"
Spacing="24"
HorizontalAlignment="Center">
<Viewbox Width="120" Height="120">
<ContentPresenter Content="{DynamicResource SvgIcon}" />
</Viewbox>
<Viewbox Width="120" Height="120">
<ContentPresenter Content="{DynamicResource SvgIcon}" />
</Viewbox>
<Viewbox Width="120" Height="120">
<ContentPresenter Content="{DynamicResource SvgIcon}" />
</Viewbox>
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>

<TabItem Header="Hosted Controls">
<ScrollViewer>
<StackPanel Margin="24" Spacing="16" MaxWidth="980">
<TextBlock Text="Inline foreignObject controls"
FontSize="26"
FontWeight="SemiBold" />
<TextBlock TextWrapping="Wrap"
Text="SVG foreignObject reserves space inside SVG text and hosts a real Avalonia control there, so the button and editor stay part of the text flow instead of being manually overlaid." />

<Border Padding="20"
CornerRadius="16"
BorderBrush="#330F172A"
BorderThickness="1"
Background="#FCFCFD">
<StackPanel Spacing="16">
<TextBlock Text="foreignObject controls inside SvgML text"
FontSize="18"
FontWeight="SemiBold" />

<svg Height="340"
Stretch="Uniform"
viewBox="0 0 520 260">
<rect x="0" y="0" width="520" height="260" fill="#F8FAFC" />
<rect x="18" y="18" width="484" height="224" fill="#FFFFFF" opacity="0.92" />
<text x="32" y="52" fill="#0F172A" style="font-size:24px;">Release review</text>
<text x="32" y="94" fill="#334155" style="font-size:16px;">
<tspan>Approve </tspan>
<foreignObject width="110" height="34">
<Button Content="Publish"
MinWidth="110"
Padding="16,6" />
</foreignObject>
<tspan> to send the revision live.</tspan>
</text>
<text x="32" y="138" fill="#334155" style="font-size:16px;">
<tspan>Owner </tspan>
<foreignObject width="180" height="34">
<TextBox Width="180"
Text="Design systems" />
</foreignObject>
<tspan> can edit the label in flow.</tspan>
</text>
<text x="32" y="182" fill="#334155" style="font-size:16px;">
<tspan>Preview </tspan>
<foreignObject width="118" height="34">
<Button Content="Open sample"
MinWidth="118"
Padding="16,6" />
</foreignObject>
<tspan> before the review is submitted.</tspan>
</text>
</svg>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>

<TabItem Header="Scene Controls">
<ScrollViewer>
<StackPanel Margin="24" Spacing="16" MaxWidth="980">
<TextBlock Text="foreignObject controls in the SVG scene"
FontSize="26"
FontWeight="SemiBold" />
<TextBlock TextWrapping="Wrap"
Text="SVG foreignObject can host native Avalonia controls anywhere a graphical SVG element is allowed, not only inside text. The hosted controls below are positioned by the SVG scene rectangle and inherit group placement." />

<Border Padding="20"
CornerRadius="16"
BorderBrush="#330F172A"
BorderThickness="1"
Background="#FCFCFD">
<StackPanel Spacing="16">
<TextBlock Text="Native controls placed by SVG x/y/width/height"
FontSize="18"
FontWeight="SemiBold" />

<svg Height="420"
Stretch="Uniform"
viewBox="0 0 640 360">
<defs>
<linearGradient id="scene-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#E0F2FE;" />
<stop offset="100%" style="stop-color:#ECFCCB;" />
</linearGradient>
</defs>
<rect x="0" y="0" width="640" height="360" rx="26" fill="url(#scene-gradient)" />
<rect x="26" y="26" width="588" height="308" rx="22" fill="#FFFFFF" opacity="0.82" />

<g transform="translate(54 58)">
<rect x="0" y="0" width="230" height="138" rx="18" fill="#F8FAFC" stroke="#CBD5E1" />
<text x="20" y="34" fill="#0F172A" style="font-size:18px;font-weight:700;">Publish lane</text>
<text x="20" y="62" fill="#475569" style="font-size:12px;">Button is hosted by a group-local rectangle.</text>
<foreignObject x="20" y="78" width="168" height="42">
<Button Content="Run export"
Padding="18,8"
HorizontalAlignment="Stretch" />
</foreignObject>
</g>

<g transform="translate(342 58)">
<rect x="0" y="0" width="244" height="138" rx="18" fill="#F8FAFC" stroke="#CBD5E1" />
<text x="20" y="34" fill="#0F172A" style="font-size:18px;font-weight:700;">Data source</text>
<text x="20" y="62" fill="#475569" style="font-size:12px;">The editor is part of the SVG scene flow.</text>
<foreignObject x="20" y="78" width="196" height="42">
<TextBox Text="northwind.csv"
HorizontalAlignment="Stretch" />
</foreignObject>
</g>

<path d="M82 240 H558" stroke="#CBD5E1" stroke-width="2" stroke-linecap="round" />
<text x="82" y="232" fill="#334155" style="font-size:16px;font-weight:700;">Opacity budget</text>
<foreignObject x="214" y="214" width="230" height="46">
<Slider Minimum="0"
Maximum="100"
Value="62" />
</foreignObject>

<g transform="translate(94 286)">
<rect x="0" y="0" width="452" height="48" rx="24" fill="#0F172A" opacity="0.08" />
<text x="24" y="31" fill="#0F172A" style="font-size:14px;">Root-level host:</text>
<foreignObject x="146" y="7" width="154" height="34">
<CheckBox Content="Ready"
IsChecked="True" />
</foreignObject>
<foreignObject x="310" y="7" width="118" height="34">
<Button Content="Details"
Padding="14,5" />
</foreignObject>
</g>
</svg>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>

</Window>
2 changes: 1 addition & 1 deletion samples/SvgML.Avalonia.Demo/SvgML.Avalonia.Demo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
Expand Down
13 changes: 8 additions & 5 deletions samples/SvgML.Maui.Demo/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ namespace SvgML.Maui.Demo;

public partial class App : Application
{
public App()
{
InitializeComponent();
public App()
{
InitializeComponent();
}

MainPage = new AppShell();
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
}
18 changes: 14 additions & 4 deletions samples/SvgML.Maui.Demo/AppShell.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@
Shell.FlyoutBehavior="Disabled"
Title="SvgML.Maui Demo">

<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
<TabBar>
<ShellContent
Title="Bound Fill"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
<ShellContent
Title="Hosted Controls"
ContentTemplate="{DataTemplate local:InlineControlsPage}"
Route="InlineControlsPage" />
<ShellContent
Title="Scene Controls"
ContentTemplate="{DataTemplate local:SceneControlsPage}"
Route="SceneControlsPage" />
</TabBar>

</Shell>
8 changes: 4 additions & 4 deletions samples/SvgML.Maui.Demo/AppShell.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
public AppShell()
{
InitializeComponent();
}
}
Loading
Loading