Add SvgML foreignObject inline control hosting#496
Conversation
There was a problem hiding this comment.
💡 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".
| if (isInline && TryGetInlineHostedControlBounds(entry.Element, entry.Host, out var inlineBounds)) | ||
| { | ||
| return inlineBounds; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| foreach (var child in svgElement.Children.OfType<SvgElement>()) | ||
| { | ||
| var childMetrics = CreateAggregateSceneNodeMetrics(child, metrics); | ||
| if (childMetrics is null) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| bounds = TransformPictureBoundsToControl(pictureBounds, renderInfo.Matrix); | ||
| return bounds.Width > 0D && bounds.Height > 0D; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| Svg.SvgUnitType.Pica => unit.Value * 16D, | ||
| Svg.SvgUnitType.Point => unit.Value * 96D / 72D, | ||
| _ => 0D | ||
| }; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| } | ||
|
|
||
| bounds = TransformPictureBoundsToControl(pictureBounds, renderInfo.Matrix); | ||
| return bounds.Width > 0D && bounds.Height > 0D; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| private static bool TryGetSceneNodeMetrics(SvgElement svgElement, Dictionary<SvgElement, SceneNodeMetrics> metrics, out SceneNodeMetrics metric) | ||
| { | ||
| var aggregate = CreateAggregateSceneNodeMetrics(svgElement, metrics); | ||
| if (aggregate is null) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| var aggregate = CreateAggregateSceneNodeMetrics(svgElement, metrics, textBoundsFallback); | ||
| if (aggregate is null) | ||
| { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| return element.IsSet(element.font_sizeProperty) | ||
| ? ConvertFontSizeToPixels(element.font_size, inherited) | ||
| : inherited; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| Transformed = measuredBounds, | ||
| Transform = Matrix3x2.Identity, | ||
| TotalTransform = Matrix3x2.Identity |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| var widthValue = TryGetPositiveLength(width, out var explicitWidth) | ||
| ? explicitWidth | ||
| : measured.Width; | ||
| var heightValue = TryGetPositiveLength(height, out var explicitHeight) | ||
| ? explicitHeight | ||
| : measured.Height; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| case XText text when string.IsNullOrWhiteSpace(text.Value): | ||
| text.Remove(); | ||
| changed = true; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
| foreach (var element in document.Descendants().Where(element => element.Name.Namespace == svgNamespace).ToList()) | ||
| { | ||
| foreach (var node in element.Nodes().ToList()) | ||
| { | ||
| switch (node) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
This reverts commit 56e7af7.
There was a problem hiding this comment.
💡 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".
| if (string.IsNullOrWhiteSpace(text)) | ||
| { | ||
| return; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| var picture = root.Picture; | ||
| if (picture is null) | ||
| { | ||
| return 0D; | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
| if (!isInline || bounds.Width <= 0D || bounds.Height <= 0D) | ||
| { | ||
| return !isInline && TryGetForeignObjectHostedControlBounds(entry.Element, entry.Host, out var fallbackBounds) | ||
| ? fallbackBounds |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
| if (!isInline || bounds.Width <= 0D || bounds.Height <= 0D) | ||
| { | ||
| return !isInline && TryGetForeignObjectHostedControlBounds(entry.Element, entry.Host, out var fallbackBounds) | ||
| ? fallbackBounds |
There was a problem hiding this comment.
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 👍 / 👎.
| if (TryGetViewBoxReference(root.viewBox, axis, out var viewBoxReference)) | ||
| { | ||
| return viewBoxReference; | ||
| } | ||
|
|
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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".
| if (!isInline || bounds.Width <= 0D || bounds.Height <= 0D) | ||
| { | ||
| return !isInline && TryGetForeignObjectHostedControlBounds(entry.Element, entry.Host, out var fallbackBounds) | ||
| ? fallbackBounds |
There was a problem hiding this comment.
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 👍 / 👎.
| if (TryGetViewBoxReference(root.viewBox, axis, out var viewBoxReference)) | ||
| { | ||
| return viewBoxReference; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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): |
There was a problem hiding this comment.
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 👍 / 👎.
PR Summary: SvgML foreignObject Inline Controls
Overview
This PR adds native UI control hosting for SvgML through the SVG
foreignObjectelement across Avalonia, Uno, and MAUI.foreignObjectis 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 customInlineUIContainerelement.The feature lets SvgML authors place platform controls directly inside SVG markup. For normal SVG layout,
foreignObjectis 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
foreignObjectas a hosted-control element with generatedx,y,width, andheightproperties. Platform-specific partials expose aChildproperty typed to each UI stack: AvaloniaControl, UnoUIElement, and MAUIView.The shared
foreignObjectwriter has two output modes:foreignObjectwith measured width and height when explicit sizing is missing.tspanwithtextLengthandlengthAdjustso 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
foreignObjectelements can still be matched back to retained scene nodes. Scene graph compilation now computes bounds forSvgForeignObjectfrom SVG units and exposes a text-bounds measurement helper used by platform mapping fallback logic.The
contenttext node is now public andelementssupports 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
foreignObjectchildren in the Avalonia logical and visual tree. The SvgML namespace also includeshttps://github.com/svgmlwith thesvgmlprefix.Uno now uses a layout root containing a drawing surface plus hosted presenters. SVG drawing is rendered by an internal
SKCanvasElement, whileforeignObjectcontrols 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 andforeignObjecttypes needed by the samples and linker.Samples
The Avalonia, Uno, and MAUI demo apps include hosted-control examples showing
foreignObjectcontrols 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
contentnodes, so the sample now uses explicitcontentelements for text runs.Commit Breakdown
64ea100e7 docs: plan SvgML foreignObject controlsaf997bc13 feat(svgml): add foreignObject inline modelf3c29fbbf feat(svgml): host foreignObject controls in Avaloniabfb70d7e3 feat(svgml): host foreignObject controls in Uno87a044fb3 feat(svgml): host foreignObject controls in MAUI7dc59d9c2 samples: demonstrate SvgML foreignObject controlsVerification
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 --checkThe 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/SVGremains 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 customInlineUIContainerremains in source outside historical planning text.