Skip to content

Add first-party inline SVG rendering (svg-native) - #693

Open
rit3sh-x wants to merge 8 commits into
DioxusLabs:mainfrom
rit3sh-x:feat/svg-native-support
Open

Add first-party inline SVG rendering (svg-native)#693
rit3sh-x wants to merge 8 commits into
DioxusLabs:mainfrom
rit3sh-x:feat/svg-native-support

Conversation

@rit3sh-x

@rit3sh-x rit3sh-x commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Addresses #448

  • Adds a svg-native feature that parses inline <svg> elements directly into Blitz's DOM/Stylo pipeline.
  • Inline SVG elements are treated as real, styleable DOM nodes, so cascading, inheritance, :hover, and author stylesheets all apply.

WPT results

1 newly passing, 1 newly failing (net 0).

Full diff (2 changed tests)
- Pass => Fail css/compositing/mix-blend-mode/mix-blend-mode-svg.html
+ Fail => Pass css/css-flexbox/svg-root-as-flex-item-002.html

Generated by the WPT workflow.

@rit3sh-x

Copy link
Copy Markdown
Contributor Author

@nicoburns can you review it?

@nicoburns

Copy link
Copy Markdown
Member

I think you have the wrong issue link... copy-paste error?

@rit3sh-x

Copy link
Copy Markdown
Contributor Author

@nicoburns fixed the link, didn't notice my mistake.

@staging-devin-ai-integration

staging-devin-ai-integration Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

(nicoburns: note that I did do a pass on the AI-review before posting it here, so the below isn't completely automated):


Thanks for taking this on. This is an initial AI review pass; a human review will follow. There are several blocking issues that need to be addressed before this can land.

Blocking

1. ALL_DAMAGE drops Stylo's own damage bits — cause of the 19 WPT regressions

The rewrite of ALL_DAMAGE in layout/damage.rs ORs together the four named constants (bits 3–7) but silently drops bits 0–2, which are Stylo's own restyle/repaint damage bits. The old value was 0b0111_1111.

I bisected this locally: reverting only damage.rs on this branch flips css/css-transforms/transform-input-001.html from FAIL back to PASS (the stylo.rs and default.css changes are innocent). This is why the feature-off WPT run regresses by 19 tests. The fix is mechanical — include the low bits in the OR.

2. Paint and hit-testing bypass the cascade entirely

The PR wires SVG presentation attributes into Stylo's cascade (svg/attrs.rs — nice approach), but paint_shape and hit_test then read fill/stroke/stroke-width/fill-opacity/pointer-events as raw DOM attributes, with a hand-rolled ~20-color CSS color parser in render/svg.rs. Consequences, all contradicting the PR description:

  • style="fill:red", author stylesheets, and :hover rules have no effect on painted shapes.
  • Inheritance is broken: <g fill="red"><rect/></g> paints the rect black, since raw_attr only looks at the leaf.
  • CSS transform on inner SVG elements is ignored (only the raw transform attribute is honoured via parse_transform_list).
  • Hit-testing has the same problems, and additionally misparses percentage stroke-width (falls back to 1.0).

Since these values are already cascaded into ComputedValues (fill/stroke are servo-enabled longhands on this Stylo build), paint should read computed style, and the raw color parser should be deleted rather than extended. This also fixes a layering violation: paint currently re-parses attribute strings every frame instead of consuming the resolved outputs of the style phase.

3. SVG_URL_EXTRA_DATA thread_local breaks under parallel style traversal

synthesize_presentational_hints_for_legacy_attributes only gets &self with no path back to BaseDocument, hence the thread_local — but blitz supports parallel style traversal (StyleThreading::Parallel drives traverse_dom on a rayon pool). The thread_local is only set on the thread that calls resolve_stylist, so on rayon worker threads it reads None and SVG presentation attributes are silently, non-deterministically dropped. It's also never cleared, so it leaks the last document's URL data across documents on the same thread.

Suggested fix: use a static/dummy UrlExtraData — the base URL only matters for relative url() values, which this pass doesn't resolve anyway.

4. rebuild_svg_fragments scans the entire document on every layout pass

The per-fragment rebuild is properly damage-gated, but the discovery is not: it iterates the whole node slotmap looking for SvgRoot elements at the end of every layout, so an SVG-free document pays a full O(N) scan per relayout. Box construction already knows exactly when it creates/destroys an SvgRoot, so please keep a registry (e.g. HashSet<NodeId> on BaseDocument) and iterate only that — this also gives propagate_svg_damage an early-out (it currently walks the parent chain to the root on every attribute mutation of any SVG-ns element).

Smaller issues

  • Group opacity is approximated by multiplying each leaf's alpha by its ancestors' opacities (ancestor_opacity). This is wrong compositing for overlapping siblings within a group — they'll blend with each other instead of being composited as a unit. The new tests only cover the non-overlapping case. Worth a // FIXME at minimum.
  • The opacity layer's clip uses the fill bbox, so a stroked shape with opacity < 1 gets its stroke clipped off (bbox excludes stroke width).
  • <use> targeting <symbol>/<svg> establishes an inner viewport but walks children with the outer viewport for percentage resolution instead of Size::new(use_w, use_h) (construct.rs).
  • hit_test only considers Shape nodes — <text> and <foreignObject> are unhittable, and ancestor display/pointer-events on groups aren't consulted.
  • parse_coord silently treats unit suffixes (em, pt, …) as px via the numeric-prefix parse; fine as a first cut but deserves a TODO.
  • anchor_shift in text.rs is duplicated inline in draw_svg_text; identity_ctm() is a pointless wrapper.
  • The CI feature-matrix comment claims "both off doesn't build" — worth verifying that's actually true rather than encoding it as an exclude.
  • A few doc-comment typos: "is resolve to None", "the caller checks that beforea", unbalanced parens in the Image variant doc.

@FireMasterK

Copy link
Copy Markdown

I tried testing this briefly. I was not able to get any SVG to load unfortunately (using dioxus-free-icons) in my app. I did get a lot of warnings like so:

2026-08-12T14:42:55.474125Z  WARN A path contains NaN, ignoring it.
Cargo.toml
dioxus-native = {git = "https://github.com/DioxusLabs/blitz", rev = "f38ce8da61eb57ba441efb864c831801d9a07677", features = ["prelude"]}
blitz-dom = {git = "https://github.com/DioxusLabs/blitz", rev = "f38ce8da61eb57ba441efb864c831801d9a07677", features = ["svg-native"]}
blitz-paint = {git = "https://github.com/DioxusLabs/blitz", rev = "f38ce8da61eb57ba441efb864c831801d9a07677", features = ["svg-native"]}

...

[patch.crates-io]
usvg = { git = "https://github.com/DioxusLabs/resvg", branch = "devin/1785858271-intrinsic-dimensions" }
anyrender = { git = "https://github.com/DioxusLabs/anyrender", branch = "devin/1785858394-usvg-048" }
anyrender_svg = { git = "https://github.com/DioxusLabs/anyrender", branch = "devin/1785858394-usvg-048" }

@FireMasterK

Copy link
Copy Markdown

blitz-paint-svg-computed-paint.patch
Here's a patch for the rendering issues.

@FireMasterK

Copy link
Copy Markdown

blitz-hover-propagation.patch
Here's a patch for fixing hover progragation for :focus.

@rit3sh-x

Copy link
Copy Markdown
Contributor Author

@FireMasterK thanks for patches. I'll try to see where my solutions failed.

@rit3sh-x

Copy link
Copy Markdown
Contributor Author

@nicoburns @FireMasterK pushed some changes with an example too.

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Re-reviewed the latest push — all four blocking items from the previous review are addressed, and I verified locally that css/css-transforms/transform-input-001.html passes again and the new svg tests pass. Two remaining issues:

1. New bug: <g opacity="0"> no longer hides its descendants

In the rewritten paint loop (blitz-paint/src/render/svg.rs), the opacity <= 0.0 branch pushes a Frame { layer_open: false } and continues — but the children of that group are still iterated and painted at full opacity on subsequent loop iterations. I confirmed with a local test: a red rect inside <g opacity="0"> paints solid red. (The previous multiplied-alpha approach handled this case correctly.)

Fix: either skip the whole subtree when a group's opacity is <= 0, or push a layer with alpha 0 so descendants composite to nothing. Worth adding a zero_opacity_group_hides_descendants test alongside the new opacity tests:

#[test]
fn zero_opacity_group_hides_descendants() {
    let px = center_pixel(
        r##"<html><body style="margin:0; background:#ffffff;">
            <svg width="100" height="100" viewBox="0 0 100 100">
                <g opacity="0">
                    <rect x="0" y="0" width="100" height="100" fill="#ff0000"/>
                </g>
            </svg>
        </body></html>"##,
    );
    assert_eq!(px, [255, 255, 255]);
}

2. CSS transform on inner SVG elements is still ignored

walk() in blitz-dom/src/svg/construct.rs still builds each node's CTM from the raw transform attribute only (raw_attr(...).map(parse_transform_list)). The pres-hint path pushes transform into the cascade, so the computed value exists — it's just never read during fragment construction. As a result, style="transform: ..." or a stylesheet transform on <g>/shapes does nothing, while the same value as an attribute works, and a CSS rule can't override the attribute as it should (pres-hints are the weakest cascade origin).

Fix: read the computed transform from the node's primary styles in walk() instead of re-parsing the attribute.

@nicoburns

Copy link
Copy Markdown
Member

Looks like this isn't using servo/stylo#396, so that might explain why it's not making full use of Stylo. Almost all of the SVG attributes should be being pushed into Stylo as presentational attributes and then read back from the Stylo computed style. This will allow for inheritance, and for the SVGs to be styled with CSS.

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

An implementation plan for first-party inline SVG support, and a detailed comparison of this PR against it, is posted at #701 (comparison: #701 (comment)).


Written by Devin

@nicoburns

Copy link
Copy Markdown
Member

Plan above is the one I had worked out with an AI before you expressed interest in the task. May be useful.

@rit3sh-x

Copy link
Copy Markdown
Contributor Author

Looks like this isn't using servo/stylo#396, so that might explain why it's not making full use of Stylo. Almost all of the SVG attributes should be being pushed into Stylo as presentational attributes and then read back from the Stylo computed style. This will allow for inheritance, and for the SVGs to be styled with CSS.

I was planning to do this in parts, the properties I've implemented don't need that branch. I will use it in followup PR.

@rit3sh-x

Copy link
Copy Markdown
Contributor Author

@nicoburns point out any issues you see.

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.

3 participants