fix(render): keep an SVG inside the bounds it was given - #204
Merged
Conversation
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
force-pushed
the
fix/svg-icon-bounds
branch
from
August 3, 2026 00:08
2d0d159 to
7cff41e
Compare
kolkov
approved these changes
Aug 3, 2026
kolkov
left a comment
Contributor
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.RenderSVGpositions 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 at707bbf3, sameggversion either side.The change
Two, both making
RenderSVGbehave like every other draw method on the canvas:isVisibleis false;RenderSVGwas 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 onmainand pass here; full suite green (61 packages).