ui: contain widget drawing — clip stack, render() widget clip, gfx_read pixel oracle (#823) - #860
Merged
Merged
Conversation
…ad pixel oracle (#823) Closes #823. - lib/ui_draw.eigs: ui_clip_push/ui_clip_pop — a nesting clip stack over gfx_clip; push INTERSECTS with the current clip, pop restores the parent clip instead of clearing it. - lib/ui.eigs render(): every widget draws under a clip of its own rect intersected with its ancestors' — canvas on_paint included (F-DYN-12), and label overflow is now defined: crop at the container edge. Registry opt-out ("clip": 0) for dropdown/combobox open lists, menu, dialog's dim overlay, grid's row-label gutter (follow-up: #859). - All raw gfx_clip set/clear pairs in lib/ui migrated to the stack (a child's clear used to wipe its parent's clip). - src/ext_gfx.c: gfx_read of [x, y] -> [r, g, b] back-buffer readback, the render-decode oracle primitive; nondet input, TAKE/RECORD tape pair like audio_stream_queued. - tests: [63] +14 stubbed containment checks (recorded clip state, planted fault via registry opt-out); new [132] real-pixel oracle (tests/test_ui_containment_gfx.eigs) under SDL_VIDEODRIVER=dummy with its own planted fault. - docs: BUILTINS.md gfx_read row, STDLIB.md clip-stack + label-overflow contract, CHANGELOG; LSP builtin+stdlib indexes regenerated. Validated: full suite green against gfx build twice (3808/3808) and under ASan+UBSan with detect_leaks=1 (3756/3756, leak tally 0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses UI containment gaps by ensuring widget rendering is clipped to each widget’s rectangle (including canvas on_paint) and by adding a real-renderer pixel readback builtin (gfx_read) to enable render-decode “pixel oracle” tests under the SDL dummy driver.
Changes:
- Added a nesting clip stack (
ui_clip_push/ui_clip_pop) and updatedrender()to clip each widget to its rect unless the widget registry opts out ("clip": 0). - Migrated existing lib/ui internal clipping sites from raw
gfx_clipset/clear pairs to the new clip stack, preventing child clears from wiping ancestor clips. - Introduced
gfx_read of [x, y] -> [r, g, b]and added both stubbed and real-pixel containment tests, with runner gating for gfx builds.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_ui.eigs | Adds stubbed-gfx containment assertions by inspecting recorded clip state (with planted-fault validation). |
| tests/test_ui_containment_gfx.eigs | New real-renderer pixel-oracle test using gfx_read under SDL_VIDEODRIVER=dummy (with planted fault). |
| tests/run_all_tests.sh | Adds probe-gated section [132] to run the real-pixel containment oracle only when gfx is available. |
| src/ext_names.h | Registers the new gfx_read builtin in the ext name table. |
| src/ext_gfx.c | Implements builtin_gfx_read using SDL_RenderReadPixels with trace nondet TAKE/RECORD. |
| lib/ui.eigs | Updates render() to wrap widget renders in a clip push/pop (with registry opt-out). |
| lib/ui_w_viz.eigs | Replaces raw gfx_clip usage with ui_clip_push/ui_clip_pop in viz widgets. |
| lib/ui_w_special.eigs | Migrates splitter rendering clips to the clip stack. |
| lib/ui_w_menu.eigs | Marks dropdown and menu registry entries as "clip": 0 to allow out-of-rect rendering. |
| lib/ui_w_input.eigs | Marks combobox registry entry as "clip": 0 for its open list rendering. |
| lib/ui_w_dock.eigs | Migrates dock child clipping to the clip stack. |
| lib/ui_w_dialog.eigs | Marks dialog as "clip": 0 for fullscreen dim overlay rendering. |
| lib/ui_w_data.eigs | Migrates table/tree/item_list/hex_view clips to stack; marks grid "clip": 0 due to documented gutter behavior. |
| lib/ui_w_container.eigs | Migrates scroll_panel clipping to stack. |
| lib/ui_draw.eigs | Introduces the ui_clip_push/ui_clip_pop stack implementation over gfx_clip. |
| docs/STDLIB.md | Documents the clip stack API and the defined label overflow/containment behavior. |
| docs/BUILTINS.md | Documents the new gfx_read builtin, including back-buffer timing and trace behavior. |
| CHANGELOG.md | Adds release notes for gfx_read, clip stack, and contained widget drawing behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
Closes #823.
Two faces of one gap (F-DYN-12): widgets did not contain their own drawing — a canvas
on_paintcould paint over surrounding chrome, and a label wider than its column drew past the panel and off the window, invisible to the stubbed suite (the #599 class).What changed
ui_clip_push/ui_clip_pop(lib/ui_draw.eigs) — a nesting clip stack overgfx_clip: push INTERSECTS with the current clip, pop restores the parent clip instead of clearing it. Exported for custom paint routines.render()clips every widget to its own rect intersected with its ancestors' — canvason_paintincluded, and label overflow is now defined: crop at the container edge (size deliberately withtext_width). Registry opt-out"clip": 0for the four renders that legitimately leave the rect: dropdown/combobox open lists, the floatingmenu,dialog's full-screen dim,grid's documented row-label gutter. Reducing those to menu+dialog (overlay-pass migration per lib/ui: no menu-bar widget — desktop-app shells must hand-roll one from buttons + show_menu #565, gutter folded into the rect) is lib/ui: move dropdown/combobox open lists to the #565 overlay pass; fold grid's row-label gutter into its rect #859.gfx_clipset/clear pairs in lib/ui migrated to the stack — a child'sgfx_clip of nullused to wipe its parent's clip.gfx_read of [x, y]→[r, g, b](src/ext_gfx.c): back-buffer pixel readback viaSDL_RenderReadPixels— the render-decode oracle primitive. Nondeterministic input, so it takes theTRACE_NONDET_TAKE/RECORDtape pair likeaudio_stream_queued. Registered through ext_names.h (linter E003 base and LSP index pick it up by construction).Proof, both halves
"clip": 0makes the probe report no active clip, then restoring restores containment.tests/test_ui_containment_gfx.eigs): the actual SDL software renderer underSDL_VIDEODRIVER=dummydraws an escapingon_paintand an overflowing label;gfx_readdecodes the back buffer — escape pixels must not exist. Its own planted fault flips the probe red. Runs in the CI gfx leg with no display.Validation
make asan+ASAN_OPTIONS=detect_leaks=1: 3756/3756, leak tally 0.--linton all touched lib/tests files: no new warnings.Follow-up filed: #859 (move open lists to the #565 overlay pass; fold grid gutter into its rect).
Closes #823.
🤖 Generated with Claude Code