Visitar URL original
Native inline SVG foundations behind the svg-native feature by nicoburns · Pull Request #1541 · DioxusLabs/blitz · GitHub
Skip to content

Native inline SVG foundations behind the svg-native feature - #1541

Open
nicoburns wants to merge 8 commits into
mainfrom
devin/1794504624-native-svg-foundations
Open

nicoburns wants to merge 8 commits into
mainfrom
devin/1794504624-native-svg-foundations

Conversation

@nicoburns

@nicoburns nicoburns commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Milestone 1 (foundations) of native SVG rendering. Each SVG backend is now its own cargo feature: svg-usvg is the existing path (inline <svg> serialized and re-parsed by usvg, plus <img src="x.svg"> and CSS background SVGs), with svg kept as an alias; svg-native keeps an inline <svg> subtree in the DOM as live nodes, stylo styles them, and blitz-paint draws the shapes from their computed styles. The native feature compiles without usvg (SvgIntrinsicDimensions has its own SvgViewBox/SvgTransform types, and CI clippy-checks blitz-dom and blitz-paint with only svg-native). When both features are compiled, DocumentConfig { native_svg: Some(true), .. } picks the native renderer per document; the default is usvg, so nothing changes for anyone until the flag is set. With one backend compiled the flag is ignored. <img>/background SVGs stay on usvg either way. dioxus-native forwards svg-native; the WPT runner and the screenshot example compile both backends and switch with BLITZ_NATIVE_SVG=1. Nothing about the usvg workaround PRs (#587, #1267, #1320, #1266, #1510) is touched.

What the native path does:

  • is_html_element / is_svg_element / is_html_element_in_html_document now check the element namespace (ns!(html) / ns!(svg)) instead of always answering "HTML". The UA sheet gets svg| rules for overflow on inner viewports, transform-origin, and non-rendered title/desc/metadata/script/style. (The @namespace svg declaration has to sit with the other @namespace rules at the top of the sheet: declared after a style rule it is invalid and every svg| rule silently drops, which is what the first version of this PR did.) Resource containers (defs, gradients, clipPath, mask, marker, pattern, symbol, filter) are deliberately not display: none: stylo wouldn't style their children, and later milestones need those computed styles. The painter skips them by tag instead.
  • Presentation attributes are turned into presentation hints with stylo's own property parser (stylo/svg_attrs.rs), so the cascade order is right for free (attribute < author CSS). Each attribute is parsed into one stack-allocated SourcePropertyDeclaration that is reused across the element's attributes and drained into its declaration block, so there is no per-attribute allocation. Geometry attributes are mapped only on the elements they belong to (cx/cy on circle/ellipse, r on circle, rx/ry on rect/ellipse, x/y/width/height on rect/image/foreignObject/svg, d on path); elsewhere they stay plain attributes. d goes through SVGPathData::parse_bytes, so a malformed suffix keeps the valid prefix as SVG requires. transform="..." is composed into a single matrix(...) declaration. Out-of-range values (negative rx, r) are dropped so the property stays auto/initial rather than clamped.
  • The <svg> root is a replaced box with SpecialElementData::SvgRoot(SvgIntrinsicDimensions) computed from width/height/viewBox/preserveAspectRatio (the same struct the usvg path uses, now built from attributes instead of an xmltree). No boxes are constructed for descendants.
  • Damage: a native <svg> root is a boundary. Descendant damage is reduced to REPAINT when it reaches the root (the HTML page is never relaid out because a circle moved); only the root's own damage (sizing attributes, style) is layout damage for the ancestors. In restyle_for_attribute_change, only the root's width/height/viewBox/preserveAspectRatio are layout-read in native mode; other SVG attribute changes mark repaint damage. The old "any SVG attribute reconstructs the whole SVG" behaviour is kept for usvg mode.
  • blitz-paint/src/render/svg.rs walks the live subtree and paints rect/circle/ellipse/line/polyline/polygon/path, g/a, and nested <svg> viewports, honouring transform, fill/fill-rule/fill-opacity, stroke with width/caps/joins/dasharray/offset/opacity, group opacity, visibility/display, currentColor, and overflow clipping on inner viewports.

Known gaps in what is here: transform-origin percentages and keywords on SVG content resolve against an empty reference box (there is no transform-box support yet), so transform-origin: center rotates about the user-space origin while lengths work; presentation-hint URLs (fill="url(#g)") are parsed against a placeholder base URL until paint servers land; <switch> paints its first child without evaluating conditional attributes. The namespace-aware is_svg_element/is_html_element and the shared UA rules (svg|title etc. hidden, transform-origin: 0 0 on SVG content) apply to computed styles in usvg mode too; CI's WPT diff shows no rendering change from that.

Not in this PR (later milestones; these render as nothing in native mode, so leave the flag off if you need them): <use>, paint servers (gradients/patterns), markers, clip-path/mask on SVG content, filters, <text>, <image>, <foreignObject> content, hit testing of SVG shapes and pathLength. There is no per-<svg> fallback to usvg (decided against in the scope discussion).

WPT svg/** (1520 tests, 15,881 subtests, PR head): main (usvg) 6036 subtests pass, this PR with usvg 6045, this PR native 6281. On the shapes/styling/painting/render/coordinate-systems/geometry/struct-reftests subset (561 tests, 2115 subtests): 944 / 950 / 1180, native +269 / −39 subtests vs usvg on the same build. The native losses (35 tests) are in features listed above (markers, <use>, text, clip-path/mask, pathLength) plus svg/styling/nested-svg-sizing-* (nested <svg> width/height given as auto, calc(), em/rem, % or sizing keywords: the native painter only resolves plain lengths and percentages for nested viewports). CI runs the default (usvg) mode, so the diff below is the usvg-mode change; a second native pass over the SVG directories in CI is a follow-up.

The usvg backend's rendering is unchanged: presentation hints are only synthesized for documents that render SVG natively (otherwise the outer <svg>'s transform attribute would apply twice), and the SVG UA sheet's svg:not(:root) { overflow: hidden } lives in a separate NATIVE_SVG_CSS UA sheet added only to those documents (as overflow: clip, so the replaced <svg> is not a scroll container and keeps its automatic minimum size as a flex item), since usvg transforms the outer <svg>'s content rather than its CSS box. Three svg/animations SMIL tests are quarantined (animation-events): now that type selectors match SVG elements, they wait for endEvent instead of throwing.

Tests: tests/blitz-tests/tests/native_svg.rs (both features compiled, switched per document) covers toggle/fallback behaviour, namespace matching, presentation attributes (incl. element-specific geometry and partial d), transforms, intrinsic sizing, currentColor, group opacity, nested-viewport clipping, resource containers (styled but not painted) and repaint-on-attribute-change; layout/damage.rs has unit tests for the root boundary. Also includes the needless_bool fixes the current stable clippy raises on main. cargo fmt, the clippy invocations (incl. the two new svg-native-only ones), cargo test --workspace, cargo doc and cargo deny ... check licenses pass.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/5b57528a8ccb4275ab16b1320f65bea5
Link to Bot: https://dioxus.staging.devinenterprise.com/org/dioxus-engineering/bots/3904245e951a4a7fa4631b37b308d89b
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/5b57528a8ccb4275ab16b1320f65bea5?variant=devin-insiders


Painting

Painting opened this PR for Nico

Implemented in Build native SVG foundations in blitz-dom · Review with Devin


WPT results

Subtests: 15 newly passing, 0 newly failing (net +15).

Subtest changes by area (11 areas)
+ css           | +6 (+6 / -0) | 59.69% -> 59.70% (+0.00%) | 119623 -> 119629 / 200400
+   css-display | +1 (+1 / -0) | 53.99% -> 54.14% (+0.15%) |    372 ->    373 /    689
+   selectors   | +5 (+5 / -0) | 48.65% -> 48.75% (+0.10%) |   2528 ->   2533 /   5196
+     old-tests | +5 (+5 / -0) | 90.25% -> 92.06% (+1.81%) |    250 ->    255 /    277
+ svg           | +9 (+9 / -0) | 38.01% -> 38.06% (+0.06%) |   6036 ->   6045 /  15881
+   animations  | +1 (+1 / -0) |  4.32% ->  4.59% (+0.27%) |     16 ->     17 /    370
+   geometry    | +4 (+4 / -0) | 46.28% -> 46.91% (+0.63%) |    292 ->    296 /    631
+     parsing   | +4 (+4 / -0) | 48.38% -> 49.68% (+1.30%) |    149 ->    153 /    308
+   interact    | +2 (+2 / -0) | 47.42% -> 47.44% (+0.02%) |   4398 ->   4400 /   9275
+     scripted  | +2 (+2 / -0) | 47.55% -> 47.57% (+0.02%) |   4387 ->   4389 /   9227
+   styling     | +2 (+2 / -0) | 32.06% -> 32.48% (+0.42%) |    151 ->    153 /    471
Full diff (14 changed tests)
+ FAIL => PASS    [1/1]  +1  css/css-display/display-contents-svg-elements.html
+ FAIL => PASS    [1/1]  +1  css/selectors/old-tests/css3-modsel-173a.xml
+ FAIL => PASS    [1/1]  +1  css/selectors/old-tests/css3-modsel-173b.xml
+ FAIL => PASS    [1/1]  +1  css/selectors/old-tests/css3-modsel-174a.xml
+ FAIL => PASS    [1/1]  +1  css/selectors/old-tests/css3-modsel-92.xml
+ FAIL => PASS    [1/1]  +1  css/selectors/old-tests/css3-modsel-93.xml
! FAIL => SKIP    [0/1]  +0  svg/animations/beginelement-instance-time-1.html
+ FAIL => PASS    [2/2]  +1  svg/animations/historical.html
! FAIL => SKIP    [0/1]  +0  svg/animations/scripted/animatetransform-type-missing-value-default.html
! FAIL => SKIP    [0/1]  +0  svg/animations/scripted/keypoints-attribute-trailing-semi.html
+ FAIL => FAIL  [17/34]  +2  svg/geometry/parsing/height-computed.svg
+ FAIL => FAIL  [17/34]  +2  svg/geometry/parsing/width-computed.svg
+ FAIL => FAIL  [23/39]  +2  svg/interact/scripted/tabindex-focus-flag.svg
+ FAIL => PASS    [8/8]  +2  svg/styling/css-selectors-case-sensitivity.html

Generated by the WPT workflow.

Inline <svg> subtrees can stay live DOM nodes styled by stylo and be painted
from the DOM instead of being serialized and re-parsed with usvg. Off by
default; the usvg path is unchanged when the switch is off.

- stylo: namespace-aware is_html_element / is_svg_element, SVG UA sheet
- presentation attributes parsed with stylo's property parser; geometry
  attributes only on their elements, d via SVGPathData::parse_bytes,
  transform lists composed into a CSS matrix()
- native <svg> root: intrinsic size from width/height/viewBox, no box
  construction for descendants, child damage stops at the root
- blitz-paint: paint basic shapes, <g>, nested <svg> from computed styles
ALLOW_ALL_NUMERIC_VALUES is for SMIL; a negative rx/r must be dropped
(computing to auto), not accepted.
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access that start with 'Devin'.

  • Disable automatic comment, CI, and merge conflict monitoring

The group opacity layer was pushed with an identity transform while its
bounds were the root's padding box in the pre-transform coordinate space,
so groups scrolled or offset from the origin were clipped away.

The `@namespace svg` declaration sat after the first style rule in the UA
stylesheet, which makes it invalid, so every `svg|...` UA rule was dropped
and nested <svg> viewports never got `overflow: hidden`. Move it to the
top with the other namespace declarations, and clip nested viewports in
the outer coordinate system.

The screenshot example honours `BLITZ_NATIVE_SVG=1` like the WPT runner.
svg_presentation_hint returned a fresh Vec<PropertyDeclaration> for every
attribute. It now parses straight into a SourcePropertyDeclaration (stylo's
stack-allocated per-declaration buffer) owned by the caller and reused
across all attributes of an element; the caller drains it into the
element's PropertyDeclarationBlock.
Per the scope discussion, each SVG backend gets its own cargo feature.
`svg-usvg` is the existing usvg path for inline <svg> and SVG images, with
`svg` kept as an alias; `svg-native` is the live-DOM path from this PR and
compiles without usvg (`SvgIntrinsicDimensions` now uses its own
`SvgViewBox` / `SvgTransform` types instead of usvg's). With both compiled,
`DocumentConfig::native_svg` picks the inline renderer per document
(default usvg); with one compiled the flag is ignored.

dioxus-native, the WPT runner, blitz-tests and the examples forward the new
feature, and CI gets clippy passes over blitz-dom and blitz-paint with only
`svg-native` so usvg types cannot creep back into the native path.

Also fixes the `needless_bool` lints the current stable clippy raises on
main, and two lints the svg-native-only build exposed.
@staging-devin-ai-integration staging-devin-ai-integration Bot changed the title Native inline SVG foundations behind DocumentConfig::native_svg Native inline SVG foundations behind the svg-native feature Nov 12, 2026
CI's WPT run (default usvg backend) regressed in three ways once both
backends were compiled in:

- SVG presentation hints applied in usvg mode too, so the outer <svg>'s
  `transform` attribute became a CSS transform on the box on top of usvg's
  own transform of the content. The hints are now only synthesized when the
  document renders SVG natively (the flag is mirrored on `NodeTree`, which
  is all styling has access to).
- The UA sheet's `svg:not(:root) { overflow: hidden }` finally took effect
  (the `@namespace` fix) and clipped the outer <svg>'s box, cutting off
  content usvg had transformed outside it. That rule moves to
  `NATIVE_SVG_CSS`, a UA sheet added only to native-SVG documents.
- `document.querySelector('animateMotion')` now finds SVG elements (type
  selectors match them case-sensitively), so three SMIL tests wait for
  events Blitz never fires instead of throwing: quarantined as
  `animation-events`.
svg_path_to_bezpath mapped stylo's ArcSweep::Cw (the SVG sweep flag 1) to
kurbo's sweep = false, so every arc in a path() clip-path or a native SVG
<path> was drawn on the wrong side. Found by the resvg suite (shapes/path/M-A*).
- Presentation attributes use the SVG 2 allowlist instead of mapping any
  attribute that names a CSS property (padding, background, position, ...).
- A path arc with coincident end points is omitted and one with a zero
  radius is a line, instead of dropping the whole path.
- The native UA rule for the outer <svg> is `overflow: clip`, so a replaced
  element is not a scroll container and keeps its automatic minimum size
  as a flex item.
- Group opacity layers are only bounded by the root box when the root clips.
- <switch> paints its first child element.
- <line> x1/y1/x2/y2 resolve percentages and units.
- The damage boundary tests run in an svg-native-only build too.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant