fix(ui): pad container children like leaves, and blit without copying - #103
Merged
Conversation
Two reported problems, one about how the toolkit LOOKS and one about what it costs to draw. #96 a container child ignored the parent's st_insets while a leaf child honoured them. A leaf goes through NSStackView's own arranged-subview layout, which applies edgeInsets; a container (hstack, vstack, divider) is pinned to the stack's leading/trailing anchors so nested rows fill the width, and those pins used the BARE anchors. So a heading sat 12px in and the row beneath it did not — a label-and-value row is the basic unit of an inspector panel, so every such panel was misaligned by exactly its own padding. The pins now carry the inset as their constant, and they are tagged so set_edge_insets can update them later: styles are normally applied AFTER the tree is built, so the constraints already exist when an inset arrives. Measured on the reported shape, a 400px panel with 12px sides: the row was 400 wide and is now 376, the same content box the leaf gets. #102 canvas_draw_image_ptr allocates and copies the whole image on every call. For a one-off image that is right and cheap. For a surface redrawn every frame it is a malloc plus a full-framebuffer memcpy sixty times a second: on a 918x659 viewport that was 61% of the frame, more than reading the frame back off the GPU and about six times the cost of rendering it, with the same pixels copied twice before reaching the screen. canvas_draw_image_borrowed_ptr keeps the caller's buffer instead. The contract is deliberately sharp and documented at the verb: the pixels stay yours and must remain valid until the next canvas_clear, which is exactly what a per-frame surface already guarantees. A caller that cannot promise that keeps using the copying variant. Speed is worthless if the picture is wrong, so the test asserts PIXELS: one image blitted both ways, read back, and required to be identical. It is, on all three backends. ci.sh gains a phase each. Neither needs the driver: both apps read their own state back and quit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Two reported problems: one about how the toolkit looks, one about what it costs to draw.
Closes #96
Closes #102
#96 — a container child ignored the parent's padding
A leaf child goes through
NSStackView's own arranged-subview layout, which appliesedgeInsets. A container child (hstack,vstack,divider) is pinned to the stack's leading/trailing anchors so nested rows fill the width — and those pins used the bare anchors. So a heading sat 12px in and the row beneath it did not.That matters more than it sounds: a label-and-value row is the basic unit of an inspector panel, and a row is an
hstack. Every such panel was misaligned by exactly its own padding.The pins now carry the inset as their constant, and they are tagged so
set_edge_insetscan update them afterwards — styles are normally applied after the tree is built, so the constraints already exist by the time an inset arrives. Without that, an inset set through a stylesheet would move the leaf children and leave every nested row behind.Measured on the reported shape, a 400px panel with 12px sides:
#102 — the per-frame blit copied the whole framebuffer
canvas_draw_image_ptrallocates and copies the entire image on every call. For a one-off image that is correct and cheap. For a surface redrawn every frame it is amallocplus a full-framebuffermemcpysixty times a second — on the reported 918x659 viewport, 61% of the frame, more than reading the frame back off the GPU and roughly six times the cost of rendering it, with the same pixels copied twice before reaching the screen.canvas_draw_image_borrowed_ptr(and the scaled twin) keep the caller's buffer instead of copying it, on all three backends.The contract is deliberately sharp, and documented at the verb rather than buried: the pixels stay yours and must remain valid until the next
canvas_clear, which is the lifetime the retained command list already has — and exactly what a per-frame surface already guarantees, since it owns one buffer and overwrites it in place. A caller that cannot promise that keeps using the copying variant.I did not take the alternative in the issue (an image handle with
create/update/draw). It is the better long-term shape, because it would also let a backend keep aCGImageor texture alive across frames, but it is a larger surface and this removes the measured cost now. Worth its own issue if the handle form is wanted.Verification
Speed is worthless if the picture is wrong, so the test asserts pixels, not timing: one image blitted both ways and read back.
Coverage
Two new
ci.shphases (5e23, 5e24). Neither needs the driver — both apps read their own state back and quit — so they work on every leg regardless of the sandbox's socket rules.🤖 Generated with Claude Code