git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commita7b10baf47c6851b28fd2909e80f79f524a19683
parent48b0a433b2
authorLucas Galante <[email protected]>
date2026-09-01 10:03
docs: record the scroll-virtualization sweep rationale in the RFC

Why the full-containment cull existed (it predates the clip stack), how
it traveled (the Phase 6q ScrollRegion copies plus RowList — multi-repo
copy drift in widget form), the one-place contract that replaced it
(intersect + draw under clip + symmetric hit-test), the deliberate
non-migrations (get_row_rect, the separator gate), and the two pipeline
lessons from the app migrations (the clip must reach every stage that
renders row content; sliver hit rects re-clamp after visuals are
recorded).

Co-Authored-By: Claude Fable 5 <[email protected]>

 docs/rfc-core-rebuild.md | 41 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 1b57497..991134b 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -2659,6 +2659,47 @@ Constraint respected: **each crate still builds standalone** — the new core is
   already closed every grouping window) stays quiet by construction: no open
   enclosing plate remains for the check to run against.
 
+- **Design note (2026-09-01) — the scroll-virtualization contract, and the ScrollRegion
+  de-duplication sweep.** A class fix, recorded because the class outlived every
+  individual sighting of it. `ScrollBox::get_item_draw_y` returned a row's position
+  only when the row was FULLY inside the viewport, so callers drew nothing for a row
+  straddling the edge — cards/rows visibly vanished mid-scroll. The helper predates
+  the §3.4 clip stack (when there was no way to draw a row "cut", culling whole was
+  the only option), and the contract then traveled: the struct around it was copied
+  into cce-system-interface, and from there into cce-fonts, cce-mail, cce-cloud, and
+  cce-layout-interface as each dissolved its List/ScrollBox embedded base (Phase 6q),
+  plus reimplemented in cce-files' RowList — the multi-repo copy-drift failure mode,
+  in widget form. The bug was then rediscovered and fixed **per app**
+  (cce-system-interface first; cce-cloud@1063422 fixed only the CLICK half), which is
+  exactly the cost the sweep exists to stop paying.
+
+  The sweep (one commit per repo, 2026-09-01): the fixed system-interface copy was
+  lifted verbatim into **`widget::ScrollRegion`** (cce-ui@764d9e7) — the union of all
+  the copies' APIs — so the contract lives in one place: **`get_item_draw_y`/
+  `get_draw_y` return every row that INTERSECTS the viewport; callers draw those rows
+  under a clip (the §3.4 stack, or exact per-quad clamping for flat pipelines), and
+  hit-test the SAME partial rows the draw shows** — visible ⇒ clickable, culled ⇒
+  not; a fix to only one half just mirrors the bug (1063422's blank-band click gate
+  became the sliver-selects-correctly gate with no shape change once the draw side
+  caught up). `ScrollBox::get_item_draw_y` itself moved to the intersection contract.
+  Deliberately NOT migrated: TreeList's `get_row_rect` keeps full containment — it
+  places a floating overlay that draws over the well unclipped, and an editor hanging
+  half off the list edge is worse than one that waits — and TreeList's row-bottom
+  separator gate, which is spatial (the separator would land outside the well), not a
+  cull.
+
+  Two pipeline lessons from the app migrations, for anyone adding a scrolled list:
+  the clip must survive to EVERY stage that renders row content, or partial rows
+  bleed instead of cut. cce-mail's hand-emitted row labels ride a boundless labels
+  drain — they drew whole into the menubar until given viewport bounds; cce-cloud's
+  span assembly dropped the paint walk's merged clip on the floor (`bounds: None`)
+  until it was threaded through to the glyph pass. And where widgets keep their full
+  rect while drawing cut (cce-layout-interface's row buttons above other controls),
+  the stored hit rect is re-clamped to the visible sliver after the visuals are
+  recorded, so the hidden part cannot shadow what's beneath it. New scrolled lists
+  build on `widget::ScrollRegion`; hand-copying it back into an app is how this
+  class got six lives.
+
 Order rationale: each phase is independently valuable and reversible, and no phase requires the
 next to compile. Phase 0 can land immediately regardless of the rest.