Skip to content

fix(render): keep an SVG inside the bounds it was given - #204

Merged
kolkov merged 1 commit into
gogpu:mainfrom
samyfodil:fix/svg-icon-bounds
Aug 3, 2026
Merged

fix(render): keep an SVG inside the bounds it was given#204
kolkov merged 1 commit into
gogpu:mainfrom
samyfodil:fix/svg-icon-bounds

Conversation

@samyfodil

@samyfodil samyfodil commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

An icon painted over the chrome around it. A 16x16 gear in a tab bar's trailing cluster drew a block from its own origin to the corner of the window:

SceneCanvas.RenderSVG positions the document with a transform built from the bounds it is handed, but never constrains it to them. Anything the document draws past its own viewBox lands on whatever surrounds the icon.

That is not a malformed document. An SVG may draw anywhere and every spec-following renderer clips to the viewport; icon sets rely on it, since a stroke centred on the viewBox edge puts half its width outside. So the usual case is a bleed of a pixel or two — invisible wherever another widget paints afterwards and covers it. It only surfaces on the last icon in a row, or one against the edge of a window, where nothing paints over the spill.

Where it came from

The switch to vector re-emission in #200. The previous path rasterised the document into a bitmap sized from those same bounds, so the bounds were enforced by construction — nothing could escape a raster that size. Emitting geometry straight into the scene removed that guarantee and nothing replaced it. Bisected: clean at cd1f292, reproducing at 707bbf3, same gg version either side.

The change

Two, both making RenderSVG behave like every other draw method on the canvas:

  • Push a clip of the given bounds around the emission, bounding the document the way the raster used to.
  • Honour the current clip. Every sibling returns early when isVisible is false; RenderSVG was the one that did not, so an icon with no pixels on screen still emitted its whole document.

Tests

Both covered, with a document that deliberately draws 4x its viewBox — the visible version of the stroke case. Scene.Bounds() is the union of the shapes and does not narrow for a clip, so the test inspects the emitted commands. Both fail on main and pass here; full suite green (61 packages).

@samyfodil
samyfodil requested a review from kolkov as a code owner August 3, 2026 00:04
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

RenderSVG on the scene canvas positions a document with a transform built from
the bounds it is handed, but never constrains it to them. Anything the document
draws past its own viewBox therefore lands on whatever surrounds the icon.

That is not an unusual document. An SVG may draw anywhere, and every renderer
that follows the spec clips to the viewport; icon sets rely on it, since a
stroke centred on the viewBox edge puts half its width outside. So the common
case is a bleed of a pixel or two, invisible wherever another widget paints
afterwards and covers it. It becomes visible on the last icon in a row, or one
against the edge of a window, where nothing paints over the spill: in a terminal
emulator on this toolkit a 16x16 gear in the tab bar's trailing cluster painted
a block from its own origin to the corner of the window, over the tab bar.

This arrived with the switch to vector re-emission (gogpu#200). The previous path
rasterised the document into a bitmap sized from those same bounds, so the
bounds were enforced by construction — nothing could escape a raster that size.
Emitting geometry into the scene removed that, and nothing took its place.

Two changes, both making RenderSVG behave like its neighbours:

  - Push a clip of the given bounds around the emission, so the document is
    bounded the way the raster used to bound it.
  - Honour the current clip. Every other draw method on this canvas returns
    early when isVisible is false; RenderSVG was the one that did not, so an
    icon with no pixels on screen still emitted its whole document.

Tests cover both, using a document that deliberately draws four times its
viewBox — the visible version of the stroke case. Scene.Bounds() is the union
of the shapes and does not narrow for a clip, so the test checks the emitted
commands instead.
@samyfodil
samyfodil force-pushed the fix/svg-icon-bounds branch from 2d0d159 to 7cff41e Compare August 3, 2026 00:08

@kolkov kolkov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Validated: SVG spec requires viewport clipping (overflow defaults to hidden, W3C REC-SVG11 section 7.9). Skia's pattern is caller-side clip — our SceneCanvas is the caller. PushClip with rect shape has near-zero overhead (2 tags). The isVisible early-out is an additional win that was missing only from RenderSVG.

@kolkov
kolkov merged commit 310936b into gogpu:main Aug 3, 2026
9 checks passed
@kolkov kolkov mentioned this pull request Aug 3, 2026
5 tasks
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.

2 participants