Skip to content

Add SvgML foreignObject inline control hosting#496

Merged
wieslawsoltes merged 21 commits into
masterfrom
feature/svgml-foreignobject-inline-controls
Apr 22, 2026
Merged

Add SvgML foreignObject inline control hosting#496
wieslawsoltes merged 21 commits into
masterfrom
feature/svgml-foreignobject-inline-controls

Conversation

@wieslawsoltes
Copy link
Copy Markdown
Owner

PR Summary: SvgML foreignObject Inline Controls

Overview

This PR adds native UI control hosting for SvgML through the SVG foreignObject element across Avalonia, Uno, and MAUI. foreignObject is now the single public API for hosted controls, including inline usage inside SVG text flow. The implementation follows SVG authoring conventions instead of exposing a custom InlineUIContainer element.

The feature lets SvgML authors place platform controls directly inside SVG markup. For normal SVG layout, foreignObject is measured and mapped as an SVG rectangular element. For inline text layout, SvgML reserves text advance in the SVG output and overlays the native platform control at the corresponding transformed slot.

Architecture

The shared SvgML model now treats foreignObject as a hosted-control element with generated x, y, width, and height properties. Platform-specific partials expose a Child property typed to each UI stack: Avalonia Control, Uno UIElement, and MAUI View.

The shared foreignObject writer has two output modes:

  • Positioned mode writes a real SVG foreignObject with measured width and height when explicit sizing is missing.
  • Inline text mode writes a generated placeholder tspan with textLength and lengthAdjust so SVG text layout reserves the correct inline advance. The native child is then hosted by the platform overlay layer at the measured placeholder bounds.

Element mapping was extended with generated mapping IDs so anonymous inline foreignObject elements can still be matched back to retained scene nodes. Scene graph compilation now computes bounds for SvgForeignObject from SVG units and exposes a text-bounds measurement helper used by platform mapping fallback logic.

The content text node is now public and elements supports string additions, which fixes XAML text-content handling differences between Avalonia and Uno. Uno samples use explicit <content Content="..."/> nodes where Uno XAML does not preserve mixed text in the custom SvgML element collection.

Platform Changes

Avalonia now synchronizes hosted control presenters from the SvgML tree, arranges them from retained scene bounds, and keeps foreignObject children in the Avalonia logical and visual tree. The SvgML namespace also includes https://github.com/svgml with the svgml prefix.

Uno now uses a layout root containing a drawing surface plus hosted presenters. SVG drawing is rendered by an internal SKCanvasElement, while foreignObject controls are arranged as native Uno elements above it. Inline controls use text-layout slot calculation and SVG-to-control transform mapping so they stay aligned with SVG text.

MAUI now uses a ContentView-based SvgML element stack with a drawing surface and absolute hosted-control layout. UseSvgML() roots the additional text and foreignObject types needed by the samples and linker.

Samples

The Avalonia, Uno, and MAUI demo apps include hosted-control examples showing foreignObject controls embedded inside SvgML text. The samples demonstrate buttons and editable text fields reserving inline space in SVG text rather than being manually overlaid.

The Uno hosted-controls sample was corrected to render both SVG text and native controls. The issue was Uno XAML not materializing literal mixed text as SvgML content nodes, so the sample now uses explicit content elements for text runs.

Commit Breakdown

  • 64ea100e7 docs: plan SvgML foreignObject controls
  • af997bc13 feat(svgml): add foreignObject inline model
  • f3c29fbbf feat(svgml): host foreignObject controls in Avalonia
  • bfb70d7e3 feat(svgml): host foreignObject controls in Uno
  • 87a044fb3 feat(svgml): host foreignObject controls in MAUI
  • 7dc59d9c2 samples: demonstrate SvgML foreignObject controls

Verification

Ran the repository workflow after the implementation:

dotnet format Svg.Skia.slnx --no-restore
dotnet build Svg.Skia.slnx -c Release --no-restore
dotnet test Svg.Skia.slnx -c Release --no-build
git diff --check

The Release build and test suite pass. The build still reports existing NuGet vulnerability warnings for packages already present in the solution.

The Uno desktop demo was also launched manually and the hosted-controls tab was visually checked to confirm that SVG text and inline Uno controls render together.

Notes

externals/SVG remains dirty in the local worktree and was intentionally not committed. It appears unrelated to the SvgML foreignObject work.

This PR intentionally keeps the public hosted-control API on foreignObject; no custom InlineUIContainer remains in source outside historical planning text.

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: 7dc59d9c24

ℹ️ 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 +66 to +68
if (isInline && TryGetInlineHostedControlBounds(entry.Element, entry.Host, out var inlineBounds))
{
return inlineBounds;
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 scene transforms for inline hosted controls

GetHostedControlBounds always prefers TryGetInlineHostedControlBounds for inline foreignObject, but that path derives coordinates from text cursor math (TryLocateInlinePictureBounds) and only applies the picture-to-control matrix, not the element’s scene-node transform chain. When inline controls are inside transformed text/groups (e.g., transform on a parent g/text), the hosted control is arranged at an untransformed position while SVG text is transformed, causing visible misalignment.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. Inline hosted-control slots are now located in text-local coordinates and then transformed through the mapped text container's TotalTransformMatrix before applying the root picture-to-control matrix. Nested text containers fall back to the layout-root transform when their own retained metric is identity, so parent <g>/<text> transforms stay aligned with the rendered SVG text.

Comment on lines +269 to +272
foreach (var child in svgElement.Children.OfType<SvgElement>())
{
var childMetrics = CreateAggregateSceneNodeMetrics(child, metrics);
if (childMetrics is null)
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 Cache aggregate metrics instead of recomputing each subtree

MapElementRecursive invokes TryGetSceneNodeMetrics for every control node, and TryGetSceneNodeMetrics rebuilds aggregates by recursively traversing descendants in CreateAggregateSceneNodeMetrics each time. Because there is no memoization, large SVG trees are re-walked repeatedly (quadratic behavior), which can significantly slow reload/mapping performance introduced by this change.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. svg.Loading.cs now carries an aggregateMetrics cache through recursive element mapping and stores cloned aggregate metrics per SvgElement. Child aggregation reuses the cached subtree result instead of recomputing it for every mapped control, avoiding the repeated full-subtree walks noted here.

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: bf12251764

ℹ️ 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 +115 to +116
bounds = TransformPictureBoundsToControl(pictureBounds, renderInfo.Matrix);
return bounds.Width > 0D && bounds.Height > 0D;
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 scene transforms in inline hosted control bounds

This inline path maps pictureBounds using only the root render matrix, but pictureBounds is derived from text cursor math (TryLocateInlinePictureBounds) and does not include ancestor scene transforms. For inline foreignObject inside transformed <g>/<text>, the native MAUI view is arranged at an untransformed position while the SVG text is transformed, causing visible misalignment. This is fresh evidence of the same class of bug already noted elsewhere: the MAUI implementation has its own unconditional inline override path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. The MAUI inline path now mirrors the Avalonia/Uno correction: it transforms located inline slots by the owning text container's accumulated SVG transform before applying the render matrix, so transformed parent groups/text elements no longer desynchronize the native view from the rendered SVG text.

Comment on lines +110 to +113
Svg.SvgUnitType.Pica => unit.Value * 16D,
Svg.SvgUnitType.Point => unit.Value * 96D / 72D,
_ => 0D
};
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 relative length units in hosted slot sizing

TryGetPositiveLength treats any unit outside the small absolute-unit subset as 0, so explicit foreignObject sizes like %, em, or ex are ignored and GetHostSlotSize silently falls back to measured control size. In inline mode this produces wrong textLength reservation and control bounds, so authored widths/heights are not respected and text/control overlap can occur.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. Hosted slot sizing now resolves SVG length units for foreignObject dimensions and coordinates, including %, em, ex, and absolute CSS units. Percentages use the root viewBox reference when available, with picture bounds as a fallback, and explicit non-positive dimensions now produce an empty host slot instead of falling back to measured native size.

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: cb4ebb92e7

ℹ️ 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 +109 to +112
}

bounds = TransformPictureBoundsToControl(pictureBounds, renderInfo.Matrix);
return bounds.Width > 0D && bounds.Height > 0D;
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 Apply scene transforms to Uno inline hosted bounds

TryGetInlinePictureBounds computes positions from text cursor math in local text coordinates, but this path maps them with only renderInfo.Matrix. When the inline foreignObject is under transformed <g>/<text> ancestors, those scene transforms are not applied, so the native Uno control is arranged at a different position than the rendered SVG text.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. Uno inline bounds now apply the located text container's TotalTransformMatrix before the render matrix, matching the rendered SVG text placement under transformed ancestors.

Comment on lines +264 to +267
private static bool TryGetSceneNodeMetrics(SvgElement svgElement, Dictionary<SvgElement, SceneNodeMetrics> metrics, out SceneNodeMetrics metric)
{
var aggregate = CreateAggregateSceneNodeMetrics(svgElement, metrics);
if (aggregate is null)
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 Cache aggregate scene metrics in Maui mapping

MapElementRecursive calls TryGetSceneNodeMetrics for each control node, and this immediately rebuilds aggregate metrics by recursively walking descendants in CreateAggregateSceneNodeMetrics every time. Without memoization, large SVG trees are re-traversed repeatedly (quadratic behavior), which will noticeably degrade reload/mapping performance for documents with many elements.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. MAUI retained-scene mapping now memoizes aggregate SceneNodeMetrics per SvgElement, so subtree metrics are computed once and cloned for later consumers instead of being rebuilt for each mapped element.

Comment on lines +721 to +723
var aggregate = CreateAggregateSceneNodeMetrics(svgElement, metrics, textBoundsFallback);
if (aggregate is null)
{
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 Cache aggregate scene metrics in Uno mapping

The Uno mapping path has the same uncached aggregate traversal pattern: each TryGetSceneNodeMetrics call recomputes subtree aggregates recursively instead of reusing previously computed results. Because this runs once per mapped element, deep/large documents pay repeated full-subtree walks, causing avoidable O(n²) mapping cost.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. Uno retained-scene mapping now uses the same aggregate metrics cache as Avalonia/MAUI. The recursive path stores cloned aggregate metrics per SVG element and reuses them for subsequent lookups, avoiding the O(n^2) subtree traversal pattern.

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: afaa7a601d

ℹ️ 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 +306 to +308
return element.IsSet(element.font_sizeProperty)
? ConvertFontSizeToPixels(element.font_size, inherited)
: inherited;
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 Parse style font settings for MAUI inline bounds

Inline hosted controls in MAUI are positioned from MeasureTextContentWidth/GetTextCenterOffset, but ResolveFontSize only reads font_sizeProperty and falls back to inherited size when the SVG uses style="font-size:..." (same issue for font-family in ResolveFontFamily). In documents that style text via the style attribute—common for imported SVG—this computes incorrect text metrics, so inline foreignObject controls drift horizontally/vertically relative to the rendered text.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. MAUI inline text metric resolution now parses style declarations for font-size and font-family, matching the Avalonia/Uno behavior. Inline coordinate and width measurement now use those styled font values instead of falling back to inherited defaults.

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: f34ffdb8fa

ℹ️ 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".

return false;
}

var parsed = numbers.Parse(value).Number;
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 Resolve x/y/dx/dy using SVG units in inline layout

Inline hosted-control positioning parses text coordinates with numbers.Parse, which drops unit semantics and treats values like 10% or 1.2em as raw user units. The SVG renderer resolves these attributes with proper unit conversion, so controls drift from their reserved text slots whenever non-pixel units are used on x, y, dx, or dy; this should parse as SvgUnit and convert with the same viewport/font context used for rendering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. Avalonia inline positioning now parses the first x/y/dx/dy token as SvgUnit and resolves %, em, ex, and absolute units using the current font size and picture viewport reference before falling back to numeric parsing.

Comment on lines +771 to +773
Transformed = measuredBounds,
Transform = Matrix3x2.Identity,
TotalTransform = Matrix3x2.Identity
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 Apply text transforms in Uno text-bounds fallback

When scene metrics are missing for a SvgTextBase, the fallback stores measuredBounds as both geometry and transformed bounds and hard-codes identity matrices. TryMeasureTextBounds only measures geometry-space text extents, so any element/ancestor transforms are lost in this path; transformed text then reports incorrect control bounds and mapping metadata, which can surface as misplaced overlays or wrong hit regions whenever fallback is exercised.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. The Uno text-bounds fallback now applies the nearest ancestor scene TotalTransform to measured text bounds when retained metrics are missing, so fallback transformed bounds and mapping metadata remain in picture space instead of assuming identity transforms.

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: cade9aea20

ℹ️ 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".

return false;
}

var parsed = numbers.Parse(value).Number;
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 Parse inline coordinates with SvgUnit semantics in MAUI

Inline hosted-control placement currently parses x/y/dx/dy via numbers.Parse, which strips unit semantics and treats values like 50% or 1.2em as raw user units. In inline text this causes controls to be positioned differently from the rendered SVG text whenever non-pixel units are used, because the renderer resolves these attributes with viewport/font-aware unit conversion while this path does not.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. MAUI inline x/y/dx/dy parsing now uses SvgUnit conversion with viewport and font-size context, so percentage and font-relative coordinates align with the renderer's coordinate semantics.

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: de48179749

ℹ️ 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 +76 to +81
var widthValue = TryGetPositiveLength(width, out var explicitWidth)
? explicitWidth
: measured.Width;
var heightValue = TryGetPositiveLength(height, out var explicitHeight)
? explicitHeight
: measured.Height;
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 explicit non-positive foreignObject dimensions

When width or height is explicitly set to 0 or a negative value, this code treats it as “not set” and falls back to measured control size, so hosted controls are still arranged (often with fallback sizing) even though SVG semantics require non-positive foreignObject dimensions to disable rendering. This creates a visible mismatch between the SVG output (no valid slot) and the native overlay control placement for Avalonia/MAUI/Uno because this shared file is linked into all three implementations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. foreignObject sizing now distinguishes unset dimensions from explicitly non-positive dimensions. Explicit width or height values resolving to <= 0 produce an empty slot and inline placeholders are skipped, so native hosted controls no longer render where SVG semantics say the foreignObject should not render.

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: 56e7af7540

ℹ️ 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 +85 to +87
case XText text when string.IsNullOrWhiteSpace(text.Value):
text.Remove();
changed = true;
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 meaningful whitespace text nodes in Uno XAML transform

This transform removes every whitespace-only XText node, which also strips intentional spaces between inline SVG nodes (for example, <tspan>foo</tspan> <tspan>bar</tspan>). In those cases the generated markup collapses words together and changes rendered text content, so whitespace-only runs should not be deleted unconditionally.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed by 78b91e29b and f70e696c5. The build-transitive Uno XAML rewrite that removed whitespace text nodes was reverted entirely in 78b91e29b; Uno text content is now handled idiomatically through tspan/textPath text content properties and writer support in f70e696c5, so there is no transform left that strips whitespace-only XText nodes.

Comment on lines +79 to +83
foreach (var element in document.Descendants().Where(element => element.Name.Namespace == svgNamespace).ToList())
{
foreach (var node in element.Nodes().ToList())
{
switch (node)
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 Skip converting text inside existing SvgML content elements

The rewrite iterates every SvgML element and converts text nodes into nested <content Content="..."/> elements, including when the current element is already <svgml:content>. That turns valid <svgml:content>text</svgml:content> into a nested structure that no longer represents the original text payload correctly and can break consumer-authored explicit content nodes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed by 78b91e29b and f70e696c5. The XAML preprocessing target that recursively converted text nodes into <content> elements was removed by the revert in 78b91e29b. The replacement implementation in f70e696c5 uses proper Uno content properties for SVG text elements instead of rewriting consumer-authored content nodes.

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: f70e696c54

ℹ️ 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 thread src/SvgML.Avalonia/Manual/elements.cs Outdated
Comment on lines +52 to +54
if (string.IsNullOrWhiteSpace(text))
{
return;
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 whitespace-only text runs when adding string content

elements.Add(string?) currently drops any whitespace-only node, but SVG text spacing can depend on those runs (for example, <tspan>foo</tspan> <tspan>bar</tspan>). When XAML feeds inter-element spaces through this overload, the space is removed and rendered text content changes, so inline text/control layouts can collapse words unexpectedly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. elements.Add(string?) now drops only null or empty strings, not whitespace-only strings, preserving significant inter-element text runs such as the separator in <tspan>foo</tspan> <tspan>bar</tspan>.

return false;
}

var parsed = numbers.Parse(value).Number;
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 Resolve Uno inline text coordinates with SVG unit semantics

Inline hosted-control positioning parses x/y/dx/dy via numbers.Parse, which strips unit semantics and treats values like %/em as raw user units. The renderer resolves these attributes with unit conversion, so controls drift from rendered text whenever non-pixel coordinates are used on text elements.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in fb52e1c67. Uno inline text coordinate parsing now resolves x/y/dx/dy through SvgUnit semantics, including %, em, ex, and absolute units, before falling back to raw numeric parsing.

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: fb52e1c67f

ℹ️ 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 +68 to +72
var picture = root.Picture;
if (picture is null)
{
return 0D;
}
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 Resolve hosted % lengths against the active SVG viewport

ResolveHostedControlLengthReference falls back to root.Picture and returns 0 when that picture is not available yet. During reload the tree is serialized before SKSvg.Load populates Picture, so inline foreignObject sizes like width="50%"/height="50%" can collapse to zero on the first pass; the placeholder tspan is then skipped and the hosted control no longer matches text layout. The same root-based fallback also resolves % against the root cull rect instead of the element’s current viewport (for nested <svg> viewports), so inline slot widths/heights can be wrong even after load.

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: 7b9adcd7c9

ℹ️ 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 +73 to +76
if (!isInline || bounds.Width <= 0D || bounds.Height <= 0D)
{
return !isInline && TryGetForeignObjectHostedControlBounds(entry.Element, entry.Host, out var fallbackBounds)
? fallbackBounds
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 Skip foreignObject fallback when scene mapping is missing

This branch always executes the non-inline fallback (!isInline is always true for scene foreignObject), so controls are arranged from authored x/y/width/height even when retained scene bounds are empty. In practice, foreignObject under non-rendered contexts (for example display="none" or elements not participating in the rendered scene) can still produce a visible native hosted control, diverging from the SVG output. The fallback should only be used when the element is confirmed renderable (or when scene bounds are unavailable for a renderable node), not unconditionally for every non-inline host.

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: 43e6c2c728

ℹ️ 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 +92 to +95
if (!isInline || bounds.Width <= 0D || bounds.Height <= 0D)
{
return !isInline && TryGetForeignObjectHostedControlBounds(entry.Element, entry.Host, out var fallbackBounds)
? fallbackBounds
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 Gate non-inline hosted fallback on renderable scene data

This branch always takes the authored foreignObject fallback for non-inline hosts (!isInline), even when retained-scene mapping produced empty bounds. In cases like display="none" (or other non-rendered subtrees), GetControlBounds is empty but TryGetForeignObjectHostedControlBounds still arranges a visible native view from raw x/y/width/height, so the overlay can appear and remain interactive while SVG renders nothing. The fallback should only run when the element is confirmed renderable rather than for every non-inline host with missing scene bounds.

Useful? React with 👍 / 👎.

Comment on lines +59 to +63
if (TryGetViewBoxReference(root.viewBox, axis, out var viewBoxReference))
{
return viewBoxReference;
}

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 Resolve hosted percent lengths from the active viewport

ResolveHostedControlLengthReference always resolves % lengths from the root SVG (root.viewBox/root.Picture) instead of the element’s active viewport, so nested <svg> viewports compute incorrect slot sizes for foreignObject width/height percentages. This same root-only logic also returns 0 when root.Picture is still null during initial reload, which can collapse explicit percentage sizes and skip inline slot reservation on first pass.

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: f994b59c8d

ℹ️ 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 +87 to +90
if (!isInline || bounds.Width <= 0D || bounds.Height <= 0D)
{
return !isInline && TryGetForeignObjectHostedControlBounds(entry.Element, entry.Host, out var fallbackBounds)
? fallbackBounds
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 Gate Uno foreignObject fallback on rendered scene bounds

This branch always takes the authored foreignObject fallback for non-inline hosts, even when retained-scene mapping returned empty bounds. When the SVG node is not actually rendered (for example under display="none" or other non-rendered contexts), GetControlBounds is empty but TryGetForeignObjectHostedControlBounds can still return a positive rect from raw x/y/width/height, causing a visible interactive native control while SVG renders nothing.

Useful? React with 👍 / 👎.

Comment on lines +67 to +69
if (TryGetViewBoxReference(root.viewBox, axis, out var viewBoxReference))
{
return viewBoxReference;
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 Resolve Uno hosted % lengths from the active viewport

Percentage foreignObject sizes are resolved against root.viewBox here, so nested <svg> viewports compute hosted slot sizes from the outer root instead of the element’s active viewport. That makes %-based width/height incorrect for embedded SVG fragments and can misplace/resize hosted controls relative to the rendered picture.

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: d3f14f32d2

ℹ️ 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".

case element child:
element = child;
return true;
case string text when !string.IsNullOrWhiteSpace(text):
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 whitespace text nodes in Uno mixed-content parser

The Uno runtime mixed-content path still drops whitespace-only text nodes because TryCreateElement only accepts strings that are not IsNullOrWhiteSpace. This collapses intentional separators in cases like <tspan>foo</tspan> <tspan>bar</tspan> (and breaks xml:space="preserve" behavior), so rendered text/content flow can change even though the source XAML includes explicit spacing. Fresh evidence here is the runtime SvgContentCollection filter, which bypasses the previously discussed build-transitive rewrite path.

Useful? React with 👍 / 👎.

@wieslawsoltes wieslawsoltes merged commit 978559b into master Apr 22, 2026
23 checks passed
@wieslawsoltes wieslawsoltes deleted the feature/svgml-foreignobject-inline-controls branch April 22, 2026 20:32
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