GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
docs/rfc-core-rebuild.md (201.3K)
1 # RFC: cce-ui core rebuild
2
3 **Status:** Draft / proposal
4 **Scope:** The core of `cce-ui` — tree ownership, layout, paint, clipping, and animation.
5 **Appetite:** Breaking changes are acceptable. Migration is incremental, behind a stable
6 `Application` trait, one widget/app at a time. Every crate must continue to build standalone.
7
8 ---
9
10 ## 1. Why
11
12 `cce-ui` works, but it is not one system — it is three half-systems overlaid, with nothing
13 arbitrating between them. Concretely, from an audit of the current code:
14
15 - **Tree ownership is tripled.** A child lives in a container's own
16 `Vec<*mut dyn Element>`, in `ctx.layout_tree.children`, *and* in `ctx.widget_registry`,
17 kept in sync by hand in every `add_child`/`clear_children`. All three are raw
18 `*mut dyn Element`. `Drop` (`core.rs` `clear_widget_references`) clears focus/context-menu
19 refs but **not** the registry or parent/child maps, so stale pointers can linger.
20 - **Rendering has three paths with no single owner:** the app's top-level `widgets: Vec<Box<dyn Element>>`
21 iteration, parent→child `all_rounded_quads` recursion, and immediate-mode
22 `render_widget`/`SectionContext`. Nothing enforces that a widget is drawn by exactly one.
23 *(This is the direct cause of the breadcrumb "black rectangle": the breadcrumb was sized
24 full-width by its container and drawn a second time under the dropdown.)*
25 - **Layout is smeared across five mechanisms:** `LayoutStrategy::allocate` (a child-driven
26 bump cursor), `Container::layout`, immediate-mode builders that literally *render twice to
27 measure*, `Backplate` (which clips children but does not lay them out), and hand-written
28 `set_rect` with absolute screen coordinates in page code. There is no measure→arrange pass
29 and no owner of any given rect.
30 - **No clip/transform abstraction.** Rect clipping is hand-copied rect-intersection in every
31 container's `all_quads`/`all_rounded_quads`/`text_*`. The GPU has no `set_scissor_rect`;
32 the only GPU clip is a per-fragment circular test in `shader.wgsl`.
33 - **Animation barely exists.** One real helper (`hover_animation`) — copy-pasted into a
34 second, *dead* implementation in `UiContext` (`tick_hover`/`get_hover_quad`, zero callers).
35 Everything else (button hover/press, `network_opacity`) is an **instant boolean flip or a
36 static multiplier, not interpolated**. No `Animated<T>`, tween, spring, or easing library.
37 "Keep animating" is a bool hand-propagated up the `tick` chain — miss one link and the
38 animation silently freezes.
39
40 Every fragility we have hit is a symptom of this. The `Element` trait has grown to ~90
41 methods spanning layout, paint, hit-testing, clipboard, tree expand/collapse, and ~12
42 `as_*_controller` downcast escape hatches — a god-trait that makes each of the above worse.
43
44 ### What is already good (keep it)
45
46 - **The frame loop is sound.** Demand-driven redraw with a single `redraw` dirty bool, gated
47 by a Wayland frame-callback vsync (`frame_callback_pending`). It idles correctly when
48 nothing changes. `tick(dt)` plumbing (clamped `dt`, ~60 Hz dispatch) already exists.
49 - **Immediate-per-frame tessellation from a retained tree** is a reasonable bones: widgets are
50 long-lived, geometry is re-emitted each frame into one shared vertex buffer and one shader
51 pipeline. We are not throwing this out.
52 - **The primitive tessellators** (rounded rects with per-corner radii, vectors with caps,
53 arcs, circles, relief primitives) are solid and reusable as-is.
54
55 ---
56
57 ## 2. Goals / non-goals
58
59 **Goals**
60 1. **Solid** — one source of truth for the widget tree; no raw pointers; no manual multi-store
61 sync; no dangling-pointer class.
62 2. **Flexible** — one real two-phase layout pass (measure → arrange) *separate from paint*,
63 with genuine flex/grow, so layout can be recomputed without re-running paint.
64 3. **Dynamic** — a first-class animation primitive; hover/press/opacity/slide/scale/collapse
65 as interpolated values; the loop automatically keeps frames coming while anything is live.
66 4. **Fast** — GPU scissor clipping, no per-frame debug I/O, and a path to per-subtree geometry
67 caching later.
68
69 **Non-goals (for this RFC)**
70 - Changing the Wayland/wgpu/glyphon backend, the `calloop` loop, or the frame-callback vsync.
71 - Changing the KDL config system or IPC.
72 - A big-bang rewrite. This lands incrementally behind the existing `Application` trait.
73 - Per-subtree tessellation caching — designed-for, but deferred (see §9).
74
75 ---
76
77 ## 3. Target architecture
78
79 A retained scene graph with a clean separation of concerns, borrowing the proven
80 Flutter/GPUI/Taffy split of *tree · layout · paint*:
81
82 ```
83 ┌─────────────────────────────────────────────┐
84 update → │ Arena (owns all nodes, keyed by NodeId) │
85 │ Node { parent, children, widget, style, │
86 │ layout_out, anim_state, dirty } │
87 └───────────────┬─────────────────────────────┘
88 │
89 ┌─────────────────────┼──────────────────────┬───────────────┐
90 ▼ ▼ ▼ ▼
91 Layout pass Paint pass Input pass Anim tick
92 (measure→arrange) (emit DisplayList) (hit-test by (advance
93 → LayoutOut rects under clip/xform layout rect+z) Animated<T>)
94 │ │
95 │ ▼
96 │ DisplayList → existing tessellators → one vertex buffer
97 └── taffy (recommended) or hand-rolled solver
98 ```
99
100 ### 3.1 Node arena — one source of truth
101
102 Replace `Vec<*mut dyn Element>` + `layout_tree` + `widget_registry` with a single arena
103 (a `slotmap`/generational-index store). No raw pointers cross frames; code passes
104 `&Arena` / `&mut Arena` + `NodeId`.
105
106 ```rust
107 pub struct NodeId(/* slotmap key: generational index */);
108
109 pub struct Node {
110 pub parent: Option<NodeId>,
111 pub children: Vec<NodeId>,
112 pub widget: Box<dyn Widget>, // the payload (see §3.5)
113 pub style: Style, // layout inputs (flex/size/padding/…)
114 pub layout_out: LayoutOut, // computed rect+transform (written by layout pass)
115 pub anim: AnimSet, // this node's live Animated<T> values
116 pub dirty: Dirty, // LAYOUT | PAINT bitflags
117 }
118
119 pub struct Arena { nodes: SlotMap<NodeId, Node>, root: NodeId, /* free lists, dirty set */ }
120 ```
121
122 Why an arena and not `Rc<RefCell<>>` or keeping raw pointers:
123 - Generational keys make use-after-free a `None` lookup, not UB. The entire dangling-pointer
124 class disappears.
125 - One store means no hand-sync of three collections; `add_child`/`remove` touch one place.
126 - It sidesteps the borrow-checker tree problem: recursion passes `NodeId` and re-borrows the
127 arena, and layout operates on `style`/`layout_out` (not the `widget` payload) so it never
128 needs `&mut` to two nodes at once. `SlotMap::get_disjoint_mut` covers the rare cases that do.
129
130 Widget **identity** becomes `NodeId` uniformly. Today identity is split: parent/child links
131 key on `WidgetId` while `hit_test`/focus/`highlight_color` key on the raw `self` address cast
132 to `usize`. Unify on `NodeId`.
133
134 ### 3.2 Layout pass — measure then arrange, separate from paint
135
136 Introduce a real two-phase pass that runs *before* paint and writes `LayoutOut` per node:
137
138 - **measure(constraints) → Size** bubbles intrinsic sizes child→parent. Leaves (text, icons)
139 measure their content — text via a glyphon measurement hook so wrapping is correct.
140 - **arrange(final_rect)** flows final positions parent→child, writing absolute (or
141 transform-local, see §3.4) rects into `layout_out`.
142
143 **Decision: hand-roll the solver** (taffy was considered and declined). We own a compact
144 measure/arrange engine inside `cce-ui`: `measure(constraints) -> Size` bubbles intrinsic sizes
145 child→parent (leaves measure content — text via a glyphon hook), `arrange(final_rect)` flows
146 final positions parent→child. Start with the layout primitives cce-ui actually needs —
147 row/column with gap + padding, flex grow/shrink, main/cross alignment, and fixed/intrinsic
148 sizing — rather than a full CSS flexbox/grid clone. This deletes `LayoutStrategy`, the
149 `allocate` bump-cursor, and the render-twice-to-measure `SectionContext` pattern outright.
150
151 Rationale for hand-rolling over a dependency: full control over the exact box model (no
152 impedance-matching a general CSS engine to our primitives), no external version churn, and a
153 solver scoped to what the DE uses. The cost is that we implement and test grow/shrink/wrap
154 ourselves — acceptable given the constrained widget set.
155
156 The key property either way: **layout is computed independently of paint.** That is what makes
157 the breadcrumb bug structurally impossible (one owner writes each rect) and what makes animated
158 layout cheap (re-arrange without re-emitting paint).
159
160 ### 3.3 Paint pass — one path, a display list
161
162 Collapse the three render paths into one. Paint walks the arena in z-order and each node emits
163 primitives into a single `DisplayList`, given its computed `layout_out` and a `PaintCtx`:
164
165 ```rust
166 pub enum Prim {
167 RoundedRect { rect: Rect, radii: CornerRadii, color: Rgba, corners: Corners },
168 Vector { a: Vec2, b: Vec2, thickness: f32, color: Rgba, cap: LineCap },
169 Circle { center: Vec2, r: f32, color: Rgba },
170 Arc { .. }, Text { .. },
171 }
172 pub struct DisplayList { prims: Vec<(ZIndex, Prim)>, /* under active clip/xform */ }
173 ```
174
175 The `DisplayList` is then fed to the **existing tessellators** (`push_rounded_rect_vertices_corners`,
176 `vector_vertices`, `circle_vertices`, glyphon text) → the existing single vertex buffer + shader.
177 The backend `render()` stays; only its *input* changes from "call `view`/`view_rounded_quads`/
178 `overlay_quads` on the app" to "walk the arena into one display list." The top-level `widgets`
179 Vec iteration and the `render_widget`/`SectionContext` immediate path both go away.
180
181 ### 3.4 Clip + transform stack
182
183 `PaintCtx` carries a clip stack and a transform (translate+scale is enough for v1; a full 2×3
184 affine is a small extension):
185
186 ```rust
187 impl PaintCtx {
188 fn with_clip(&mut self, rect: Rect, f: impl FnOnce(&mut PaintCtx));
189 fn with_transform(&mut self, xform: Affine2, f: impl FnOnce(&mut PaintCtx));
190 }
191 ```
192
193 - Rect clips lower to GPU **`set_scissor_rect`** (a real render-pass feature we currently do
194 not use), deleting the hand-copied rect-intersection in every container.
195 - Keep the shader's circular clip for the cases that need it.
196 - The transform gives **slide / scale / collapse transitions for free** and lets children use
197 local coordinates instead of everyone storing absolute screen coords.
198
199 ### 3.5 Trait split — kill the god-trait
200
201 Replace the ~90-method `Element` with narrow traits, each a single concern:
202
203 ```rust
204 /// The payload. Most widgets implement only this + Paint.
205 pub trait Widget: 'static {
206 fn style(&self) -> Style { Style::default() } // layout inputs
207 fn measure(&self, c: Constraints, ctx: &MeasureCtx) -> Option<Size> { None } // leaves only
208 }
209 pub trait Paint { fn paint(&self, layout: &LayoutOut, ctx: &mut PaintCtx); }
210 pub trait Input {
211 fn hit(&self, layout: &LayoutOut, p: Vec2) -> bool { layout.rect.contains(p) } // default!
212 fn on_event(&mut self, ev: &Event, ctx: &mut EventCtx) -> EventStatus { EventStatus::Ignored }
213 }
214 ```
215
216 - Hit-testing gets a correct default from `layout_out.rect` + z-order, so the manual
217 `hit_test`-by-`self`-address code across widgets disappears.
218 - The 12 `as_*_controller` downcasts are replaced by typed messages / commands through
219 `EventCtx` (an app-defined message channel), not runtime `Any` casts.
220 - `Container` stops being special: a container is just a `Widget` with children and a flex
221 `Style`; it no longer hand-clips or hand-lays-out.
222
223 ### 3.6 Animation — first-class
224
225 ```rust
226 pub struct Animated<T> { current: T, target: T, motion: Motion } // Motion: Tween(easing,dur) | Spring(k,damp)
227 impl<T: Lerp> Animated<T> {
228 fn set_target(&mut self, t: T);
229 fn tick(&mut self, dt: f32) -> bool; // returns true while still moving
230 fn value(&self) -> T;
231 }
232 ```
233
234 - Hover/press/focus become `Animated<f32>` 0→1 factors, not bools. Buttons interpolate color
235 by `lerp(idle, hover, factor)` instead of `if hovered { a } else { b }`.
236 - `network_opacity` becomes an `Animated<f32>` that actually tweens.
237 - **The loop keeps frames coming automatically:** the arena tracks "any node has a live
238 animation." `tick` OR-reduces over the arena's animated nodes and sets `redraw`, so the
239 hand-propagated `tick`-bool chain (and its silent-freeze failure mode) is gone.
240 - Fold `hover_animation` (and delete its dead `UiContext` twin) into this; the global
241 thread-local singleton — which today allows only one animated highlight at a time — becomes
242 per-node state, so multiple highlights animate independently.
243
244 ---
245
246 ## 4. How this kills the classes of bug we have
247
248 | Bug class | Fixed by |
249 |---|---|
250 | Dangling / stale `*mut dyn Element` | §3.1 arena, generational keys, single store |
251 | Tree desync (Vec vs layout_tree vs registry) | §3.1 one store |
252 | Same widget drawn twice at different rects (breadcrumb) | §3.2 single layout owner + §3.3 single paint path |
253 | Overflow bleeding past clip regions | §3.4 GPU scissor + clip stack |
254 | Animation silently freezes (missed tick-bool link) | §3.6 arena-driven auto frame requests |
255 | One-highlight-at-a-time hover | §3.6 per-node animation state |
256 | Per-frame debug I/O in hot path | §7 quick win |
257
258 ---
259
260 ## 5. Impact on the `Application` trait & clients
261
262 The client-facing `Application` trait shape stays as stable as possible so apps migrate one at
263 a time. Two viable migration shapes:
264
265 - **Adapter (recommended):** the old `Element` widgets keep working via a compatibility shim
266 that wraps each in a `Node` and forwards `all_rounded_quads`/`text_items` into a `DisplayList`.
267 New/migrated widgets implement the narrow traits directly. Both coexist until the last old
268 widget is gone.
269 - The `view`/`view_rounded_quads`/`overlay_quads` methods become thin shims over the arena walk
270 during the transition, then are removed.
271
272 Constraint respected: **each crate still builds standalone** — the new core is entirely inside
273 `cce-ui`; clients depend on it by path exactly as today. No `[workspace.dependencies]`.
274
275 ---
276
277 ## 6. Migration plan (staged; every stage leaves the tree building)
278
279 - **Phase 0 — Quick wins (independent of the rebuild).** Fix breadcrumb double-render; remove
280 the per-frame `eprintln!` text reconstruction in `render()`; add `set_scissor_rect` for the
281 existing clip call sites; delete the dead `tick_hover`/`get_hover_quad` twin. *Ships value now.*
282 - **Phase 1 — Arena + identity.** Introduce `Arena`, `NodeId`, `Node`; port `add_child`/tree
283 ops onto it behind the adapter; unify identity on `NodeId`. No visual change.
284 - **1a — Arena data structure: DONE.** `cce-ui/src/scene/arena.rs` — a generational forest
285 arena, generic over payload, added additively (`pub mod scene;`) with nothing wired into the
286 live path yet. `NodeId` carries a `NonZeroU32` generation so a handle to a removed node reads
287 back as `None` (use-after-free → missed lookup, not UB). Tree ops (`insert`, `append_child`,
288 `detach`, `remove_subtree`, `get_pair_mut`, `subtree`/`ancestors` iterators, cycle rejection)
289 with 14 headless unit tests, including one that owns and trees real `dyn Element` payloads.
290 Full `cce-ui` suite: 83 passing.
291 - **1b — Adapter: DONE.** `cce-ui/src/scene/tree.rs` — `WidgetTree`, the arena-backed
292 replacement for `UiContext`'s two stores (`widget_registry` + `layout_tree`), keyed by a
293 `WidgetId → NodeId` index so the public `WidgetId` API is preserved. Consolidates both maps
294 into one generational store; link-only-before-register is modeled as `Entry.ptr: None`. 10
295 headless tests against real `dyn Element` payloads (register/overwrite, symmetric+deduped
296 link, reparent, link-before-register, symmetric detach, clear_children, clear_all, removal
297 staleness, registered-iteration). Not yet wired into `UiContext`.
298 - **1b — Live swap: DONE (compile + tests; runtime verification pending).**
299 `UiContext.{widget_registry, layout_tree}` are replaced by a single `tree: WidgetTree`. All
300 access routed through it: the 5 public methods, the internal direct field reads in
301 `context.rs` (event routing, `clear_dirty`, `rebuild_spatial_grid`, `is_widget_visible`,
302 `tick`, `is_coordinate_covered`, `is_movable_backplate_at`, `is_widget_at`), the widget-layer
303 defaults in `widget/mod.rs`, and the direct pokes in `keybinds_control.rs`/`multi_control.rs`
304 (now `link_ids`) and `plate.rs`/`parameters_bg.rs` (now `tree.set_parent(id, None)`). `cce-ui`
305 builds clean; **93 tests pass**; all 7 app crates that use the API build unchanged. Still to
306 do: **`make run`** cce-files + cce-designer to confirm the symmetric-tree change (below) is
307 behavior-safe in paint/event handling.
308 - Original swap notes below retained for reference.
309
310 Replace `UiContext.{widget_registry, layout_tree}`
311 with a `WidgetTree`, and route through it: the 5 public methods apps depend on
312 (`register_widget`, `link_ids`, `clear_hierarchy`, plus internally `clear_children_ids`,
313 `unlink_child`), the internal direct field reads in `context.rs` (event routing, `mark_dirty`
314 walk, `is_widget_visible`, `clear_dirty`, `rebuild_spatial_grid`, coverage/hit tests), and the
315 widget-layer defaults in `widget/mod.rs` (`parent`/`set_parent`/`children`/`add_child`/
316 `mark_dirty`) plus the direct pokes in `keybinds_control.rs`, `multi_control.rs`, `plate.rs`,
317 `parameters_bg.rs`.
318 - **One deliberate behavior change to verify at runtime:** the legacy maps are left
319 *asymmetric* in a few spots (`set_parent(Some)` writes only `parents`; `plate`/`parameters_bg`
320 detach via `parents.remove` only). `WidgetTree` keeps parent/child links symmetric, so
321 `children()` — read by paint recursion and event propagation — becomes self-consistent. This
322 is almost certainly a latent-bug fix, but it must be confirmed against the running apps
323 (cce-files, cce-designer, cce-graph, cce-system-settings) before landing.
324 - App compatibility: only `register_widget`/`link_ids`/`clear_hierarchy` have app callers;
325 `get_widget`/`get_widget_mut`/`unlink_child` have **zero callers** workspace-wide and can be
326 dropped or kept as thin shims.
327 - **Phase 2 — Layout pass.** Add `Style` + measure/arrange (taffy). Migrate containers to
328 emit `Style` instead of `LayoutStrategy`; delete `allocate` and render-twice measurement as
329 containers move over.
330 - **`LayoutStrategy`, `allocate` and render-twice: DELETED 2026-10-08.** The last users were
331 cce-system-interface's pages (`PageLayoutBuilder` over `AdaptiveGrid`), cce-files' browse
332 page (`ColumnLayout`) and the gallery's container-layout exhibit. A section's slot never
333 depended on its own height, so `layout::section::PageFlow` places it first and the
334 section draws once, in place (`each_section_is_drawn_once`); the browse page is a
335 `scene::layout` column; `ContainerLayout` keeps only `layout` / `measure`. Section
336 CONTENT is still immediate-mode `SectionContext` — porting each page's content to box
337 trees is not done. Every settings page, cce-files (relief on and off, save mode) and the
338 gallery drew identically to the pixel before and after, live readings aside.
339 - **Phase 3 — Paint unification.** Introduce `DisplayList` + `PaintCtx` (clip/transform);
340 route the backend `render()` through the arena walk; retire the top-level `widgets` Vec and
341 `render_widget`/`SectionContext`.
342 - **Phase 4 — Animation.** Land `Animated<T>` + arena-driven frame requests; convert
343 hover/press/`network_opacity`; consolidate `hover_animation`.
344 - **Phase 5 — Trait split & cleanup.** Split `Element` into `Widget`/`Paint`/`Input`; remove
345 `as_*_controller` downcasts; migrate remaining widgets; delete the compatibility shim.
346 - **Approach correction (resolved by experiment).** A *non-breaking supertrait carve-out* of
347 `Element` (`trait Element: Paint + …`) turns out to be impossible in Rust here. The structural
348 methods the passes need (`rect`, `children`, `set_rect`) are overridden in dozens of widgets
349 across cce-ui **and** the app crates (`rect` 31+8, `children` 23+2, `set_rect` 42+4): moving
350 them off `Element` breaks every override, and merely *declaring* them on a supertrait makes
351 every `elem.children()`/`elem.rect()` call site ambiguous (a supertrait method is always in
352 scope on the subtrait). Nor does a blanket "view" `impl<T: Element> Paint for T` let
353 `&dyn Element` coerce to `&dyn Paint` — that coercion exists only for real supertraits. So the
354 split follows the **adapter** path from §5, not a supertrait split: narrow traits independent
355 of `Element`, with `Adapted<W>` bridging a narrow-trait widget into the `*mut dyn Element`
356 tree. The compatibility-shim bullet is thus *this* adapter (there was never a discrete legacy
357 shim to delete — the earlier migration hung hooks directly on `Element`).
358 - **5a — Layout + Paint concerns + adapter: DONE (compile + tests; no runtime surface yet).**
359 `cce-ui/src/widget/model.rs` — the independent `Layout` (`layout_style` / `intrinsic_size` /
360 `layout_children`) and `Paint` (`color` / `paint` / `clips_children`, where `paint` takes the
361 laid-out rect rather than reading a stored one) traits, plus `Adapted<W>`: a wrapper that
362 carries the `Widget` base and forwards the `Element` layout/paint methods to `W`'s narrow
363 traits. A headless test builds a pure narrow-trait tree (a `Col` container + two `Dot` leaves,
364 none of which implement `Element`), wraps each in `Adapted`, and drives it through the
365 *existing* `scene::bridge` layout pass and `scene::painter` paint pass — asserting both the
366 computed rects and the painted quads. Purely additive: no existing widget or app changes, all
367 137 cce-ui tests pass. Runtime verification is N/A until a real widget is migrated onto the
368 adapter (nothing in a running app uses it yet).
369 - **5b — Input concern: DONE (compile + tests; no runtime surface yet).** `widget/model.rs` —
370 the `Input` trait (`hit` / `on_event`, both against the laid-out rect) plus adapter
371 forwarding with the RFC's centralizations: `Adapted::handle_event` hit-gates pointer-
372 positioned events (`MouseButton`/`MouseWheel`) once, so narrow widgets never carry the
373 per-widget "check hit_test first" boilerplate every legacy `mouse_input` override does;
374 unconsumed `PointerMove` falls back to the legacy hover bookkeeping, so `base.hovered` and
375 the synthesized `MouseEnter`/`MouseLeave` (which re-enter `handle_event` and reach
376 `on_event`) keep working; `hit_test` keeps the occlusion (`is_coordinate_covered`) check
377 while delegating the geometric test to `Input::hit`. A headless test drives a narrow
378 `Clicker` through the *real* `UiContext::propagate_event` router: in-rect click consumed +
379 counted, out-of-rect click gated out, hover enter/leave transitions observed on both the
380 narrow widget and the base flag. 138 cce-ui tests pass.
381 - **5c — First real widget migrated: `ProgressBar`. DONE (runtime-verified, pixel-identical).**
382 `widget/display/progress_bar.rs` now implements only `Layout` + `Paint` + `Input`;
383 `ProgressBar::new` returns `Adapted<ProgressBar>`, so both construction sites
384 (`cce-ui` demo, `cce-test-interface`, incl. `.with_label`) compile unchanged. The migration
385 forced the adapter to absorb the legacy surface external render loops actually read, all
386 added to `Adapted` in this step: an `all_rounded_quads` **reverse bridge** (the widget's
387 `Paint::paint` output converted back to legacy tuples — cce-test-interface renders via this),
388 `Paint::corner_style` → `corner_radius`/`rounded_corners` (for style-property painters like
389 the demo's `widget_vertices`; transitional, dies with those paths), the detached-label
390 convention (`set_rect` inflation + `with_label` builder + content-rect inset),
391 `preferred_height` ← `intrinsic_size`, `highlight_quad → None` (narrow widgets own their
392 pixels), and `type_name` reporting the *inner* type (layout.rs string-matches
393 `"ProgressBar"` for span-full sizing). **Runtime verification:** ran cce-test-interface and
394 the demo on the live compositor (via `ccectl center-window` + `grim`); an A/B pixel diff of
395 the demo against the pre-migration build showed the two frames identical except a 19×20
396 compositor corner artifact — zero differing pixels at any widget. 141 tests pass.
397 - **5d — Leaf sweep: `Separator`, `StatusDot`, `UsageBar`. DONE (runtime-verified via the
398 settings app's render stream).** New adapter machinery this round: `Deref`/`DerefMut` to the
399 wrapped widget (call sites keep `dot.set_status(..)` / `bar.value`); per-prim reverse
400 bridges (`Prim::Quad` → `extra_quads`, `RoundedRect` → `all_rounded_quads`, `Circle`/`Arc` →
401 `extra_circles`/`extra_arcs`) so apps reading BOTH `all_quads` and `all_rounded_quads` draw
402 each prim exactly once; `Input::blocks_backplate_drag`; and the mirrored-by-value-builder
403 pattern (`Adapted<UsageBar>::with_colors`) since builders can't flow through `Deref`.
404 `Separator` had no `Widget` base (public x/y/w/h fields) — its rect now lives on the adapter
405 base, and cce-status-interface's rotation loop was updated to transpose via `rect`/`set_rect`.
406 **One deliberate behavior fix:** legacy `StatusDot` emitted **zero** geometry on every render
407 path (probe-confirmed — `render_widget` reads only `all_quads`/`all_rounded_quads`, both
408 empty for it), so the Processes-page dots were invisible; the narrow `Paint` default emits
409 the color quad, and the dots now render (verified in the live app's render dump: 10×10 rects
410 in exact status colors). UsageBar verified byte-identical in the same dump (bg+fill rects at
411 the exact `with_colors` colors). 147 tests pass; status-interface, system-settings, and
412 test-interface all build.
413 - **5e — First interactive widgets: `Checkbox` + `Toggle`. DONE (verified end-to-end with an
414 injected live click).** New machinery: the legacy polling/value surface on `Input`
415 (`take_click`/`take_change`/`value_string`/`set_value_string`/`value` — kept there to avoid
416 a fourth bound; dies with RFC §3.5 typed messages); `Input::opens_context_menu` (the adapter
417 routes a hit right-press to `UiContext::handle_right_click`, which ctx-less `on_event`
418 can't); `Layout::inline_label` (Checkbox/Toggle draw the label inside their rect — no
419 `set_rect` inflation/content inset, matching the legacy `label_offset` type-name special
420 cases); `Paint::{solid_border, widget_font, sync_label}` (transitional forwards);
421 prim-derived `text_labels` for inline-label widgets (one paint source feeds every text
422 path) with a base-label fallback replica for detached ones; `as_any` now exposes the *inner*
423 widget so legacy `downcast_mut::<Checkbox>()` sites keep working; `Drop` on `Adapted`
424 clears the global focus/context-menu refs (bounds moved onto the struct for this);
425 `Debug`/`Clone` derives; and an inherent `Adapted::set_label` that shadows
426 `Control::set_label` (which writes only the base and left self-painted labels stale —
427 caught by a test; `Control` impls override to route here). Both widgets track
428 `hovered`/`focused` from the forwarded `MouseEnter`/`MouseLeave`/`FocusIn`/`FocusOut`
429 events — the state that becomes `Animated<f32>` in §3.6. In-crate consumers updated
430 (`json_layout` direct Element calls, `multi_control` enum variant, `parameters_bg` field);
431 app repos updated (system-settings network+notifications, data-editor, layout-interface
432 field types — construction sites unchanged). **Verification:** cce-test-interface pixel-
433 diffed 0 against the pre-migration baseline, and a `wlrctl`-injected click on the live
434 compositor flipped the Toggle's bordered half on-screen — the full input path through the
435 adapter exercised for real. 152 tests pass.
436 - **5f — `Button` (widest-radius widget: 19 app files + 8 in-crate). DONE.** The press/release
437 contract forced an adapter refinement: **presses stay hit-gated, releases now flow ungated**
438 — a press-tracking widget must see the release wherever the cursor ended up to commit
439 (in-rect → `take_click` + `on_click_cb`) or cancel, exactly the legacy `mouse_input`
440 contract (pinned by a router-level test incl. out-of-rect cancel). Also added:
441 `Layout::layout_ignore` and `Input::set_selected` forwards. The model ports the per-kind
442 color matrix (Primary/Reset/ListRow/CopyIcon × pressed/hovered/selected + bg overrides),
443 SVG icon quads, per-kind label justification/fonts, and the Phase 2b `intrinsic_size`; the
444 9 by-value builders are mirrored on `Adapted<Button>` (`with_label` comes from the generic
445 + `sync_label`). In-crate consumers fixed (multi_control, keybinds_control, ramp, treelist,
446 parameters_bg fields; json_layout + demo now call `take_click` on the box instead of
447 concrete downcasts); ~11 app repos updated (field types + raw-cast→`as_ptr_mut` cleanups).
448 **Verification:** full workspace (minus compositor, which doesn't use widgets) builds;
449 154 cce-ui tests + cce-cloud's json_layout hover-simulation test pass; test-interface
450 pixel-diffs cursor-only vs the 5e baseline; a live hover A/B against the stashed legacy
451 build showed the identical fill pixel (the inert hover on that page is pre-existing app
452 behavior, not a regression).
453 - **5g — `Label`. DONE.** Text lives on the model, emitted as a `Text` prim; the adapter's
454 prim bridge serves every legacy text path. Added the generic synced `Element::set_text`
455 override on `Adapted` (same trap as `set_label`: the trait method wrote only the base and
456 left the painted text stale — live-updating labels like system-info's CPU readouts hit it
457 constantly). Builders mirrored; three app repos' field types updated. Workspace builds;
458 155 tests pass; test-interface diff vs the post-Button baseline has a 0x0 bbox at 2%
459 threshold (sub-perceptual blend noise only).
460 - **5h — `Slider` + `RangeSlider`, and the event-capability layer. DONE.** Introduced the
461 RFC §3.5 **`EventCtx`** (`on_event(&mut self, event, &mut EventCtx)`): content rect, widget
462 id, `request_focus()` (readout edit mode), and a transitional `ui: Option<&mut UiContext>`
463 for the legacy scroll-gesture gating. Added `Input` drag hooks
464 (`draggable`/`is_dragging`/`drag_begin`/`drag_update`/`drag_end`, rect-carrying — hosts
465 drive drags by direct call) and — critically — **direct-dispatch overrides**: hosts call
466 `mouse_input`/`mouse_wheel`/`keyboard_input`/`focus`/`unfocus` directly on widgets, and
467 without adapter overrides those hit the inert Element defaults (a latent 5e/5f regression:
468 treelist's add-key button and parameters_bg checkboxes were deaf on that path — now routed
469 into `handle_event`). `Layout::inflates_label_rect` distinguishes ProgressBar-style rect
470 inflation from Slider-style label-eats-into-rect. `text_labels` is now prim-derived PLUS
471 base fallback (sliders paint readout text AND have a detached label). **Found the hard
472 way:** hosts under-size labeled sliders, so legacy content height went NEGATIVE and the
473 flipped quads still rasterized — `content_rect` must not clamp at zero or tracks vanish
474 (pixel-diffed to 0 vs baseline after the fix). Slider geometry consolidated into one
475 `geom()` helper (legacy re-derived it in five places). 154 tests pass; workspace builds.
476 - **5i — Display leaves + `Spinbox`: InfoBox, FontPreview, Sidebar, Splitter, Panel, Spinbox.
477 DONE.** New adapter machinery: `Input::drag_reposition` (self-moving widgets — Panel,
478 Splitter — return a new origin; the adapter applies it to the base rect the model can't
479 reach), `Input::set_drag_bounds`, and `Layout::detached_label_inset` (the legacy
480 `Control::control_label` +4px x-offset that the default `text_labels` path lacked — caught
481 as a 4px label shift in the pixel diff, fixed to a 1×1-pixel residual). Spinbox ports the
482 sub-zone hover (-/+ buttons) into `PointerMove` handling, display-click edit mode with
483 cursor placement + `request_focus`, decimals/unit value formatting, and drops its vestigial
484 raw-pointer parent/children fields. Skipped for later: `PreviewState` (needs per-label fonts
485 on the `Text` prim), `StatusBar` (bigger custom surface), Float3/LayoutPreview (time).
486 156 tests pass; workspace builds; test-interface pixel-diff vs the 5h baseline: 1 pixel.
487 - **5j — `InteractiveListItem`. DONE (verified in the live settings render dump: service-row
488 titles/subtitles + themed overlays through the prim bridge).** Button-pattern press/release
489 with themed selected/hover/press overlays. `StatusBar` was surveyed and DEFERRED: it is
490 parent-coupled (reads its parent's backplate state/rect/radius at paint time and owns
491 `parent`/`set_parent`) — that belongs with the container/children design, not the leaf
492 recipe. Also still pending from the leaf tier: Float3, LayoutPreview (time), PreviewState
493 (needs per-label fonts on `Prim::Text`).
494 - **5k — Controller capabilities + first controller widgets: `Breadcrumb`, `Node`. DONE.**
495 The controller tier was blocked because `Element` is implemented exactly once (for
496 `Adapted<W>`), so a migrated widget couldn't re-expose its `as_*_controller` downcasts.
497 Resolution: transitional **capability hooks on `Input`** (`menu/graph/spreadsheet/path/
498 param/geom _controller[_mut]`, default `None`) that the adapter forwards the `Element`
499 downcast pairs to — a controller widget returns `Some(self)`. This is the pragmatic half of
500 the §3.5 "typed messages" bullet: the controller *traits* are already the typed surface;
501 what dies with `Element` is reaching them through the god-trait (end state: hold the
502 concrete `Adapted<W>` or a `&dyn XController` directly). Also added:
503 `EventCtx::open_context_menu` (Breadcrumb records the right-clicked segment *before* the
504 shared menu opens — `opens_context_menu` can't express work-before-menu), an
505 `Input::copy_path` forward (context menu "Copy Path"), and an `Adapted::on_cursor_moved`
506 override — another direct-dispatch entry (cce-files drives breadcrumb hover through it)
507 routing the raw move to `on_event` with the base hover bookkeeping as fallback.
508 `ScrollController` was deleted outright: zero implementors and zero live callers
509 workspace-wide (only a dead cce-designer helper, removed there). **Breadcrumb** (first
510 controller widget, live in cce-files + cce-designer) verified on the live compositor:
511 idle/hover captures pixel-identical to the stashed legacy build at the widget, and a
512 breadcrumb-segment click navigates correctly through the direct-dispatch
513 `mouse_input → on_event → path_click` chain. **Node** (ParamController + GeomController,
514 self-moving drag with grid snap via `drag_reposition`) has no live constructors in any app
515 repo — compile + router-level tests only. 160 tests pass; all 19 client crates build.
516 - **5l — `Spreadsheet` + the tick/scroll adapter surface. DONE (live-verified in the
517 designer: startup and pane-open captures diff 4px/32px vs the stashed legacy build, all in
518 a one-pixel bottom-edge blend strip).** New `Input` surface: `tick(dt, rect)` +
519 `wants_tick` (inertial scroll — hosts broadcast `Element::tick` per frame; §3.6
520 `Animated<T>` eventually replaces this), `scrollable` (→ `is_scrollable`), and
521 `draggable` now takes the laid-out rect (scroll widgets are draggable only while content
522 overflows). **`Adapted` now owns real visibility**: the `Widget` base carries none and the
523 legacy `Element` defaults are no-ops, so each hideable widget stored its own flag; the
524 adapter stores it once, gating `hit_test` (hosts broadcast wheel/press dispatch and rely
525 on hidden widgets rejecting the hit), direct-dispatch `keyboard_input` (hiding a pane
526 doesn't unfocus it), and the `text_labels` bridge. Deliberate fix: `set_visible` on
527 migrated widgets now works instead of being silently ignored. Spreadsheet's 7×-duplicated
528 scroll/thumb math collapsed into one `geom()` helper; its `paint` deliberately does NOT
529 emit the translucent `PARAM_BG` background (the designer draws widget backgrounds itself
530 from `color()` + `corner_style` — emitting it again would double-blend).
531 - **5m — `Graph` + the legacy dual-geometry escape hatch. DONE (designer startup pixel-diffs
532 ZERO; cce-graph diffs to an empty 2%-threshold bbox; cce-files' Graph view verified
533 visually + getter-contract unit test — its pixel A/B was blocked by the live session's
534 terminal covering the capture region).** Graph's three hosts consume DIFFERENT getters
535 (designer: plain `extra_quads` + `extra_circles`; cce-files `render_widget`:
536 `all_rounded_quads` with highlight-only `all_quads`; cce-graph: the scene path's
537 `paint_self`). The model keeps one geometry generator; `paint` emits the rounded view, and
538 transitional `Paint` hooks serve the rest: `serves_legacy_plain_quads`/`legacy_plain_quads`
539 (verbatim through `extra_quads`, with the adapter emptying `all_quads` to preserve the
540 no-double-draw contract) and `text_bounds` (node names clip to the widget rect — the
541 adapter now overrides `text_labels_with_[font_and_]bounds`, replicating the
542 scroll-ancestor walk when the hook is `None`). Also: `Input::hit` override for the legacy
543 edge-exclusive hit test; ctrl-wheel zoom reads `ctrl_pressed` through `EventCtx.ui`;
544 the `Element::paint` register_hovered pre-pass is dropped (shared hover highlight is
545 suppressed for all adapted widgets). Discovered en route: the designer's CONTENT pane IS a
546 real `Graph` (index 1), and `ContentBg` is a standalone grid background whose
547 GraphController impl is mostly stubs — it is NOT a Graph wrapper.
548 - **5n — The container concern + first containers: `Switcher`, `ContentBg`. DONE
549 (render-dump-verified: cce-system-settings — whose every page lives under the Switcher —
550 A/B'd byte-identical across four pages against the stashed legacy build, modulo live
551 system data).** The children/tree design, resolved transitional-first: tree links already
552 live in `ctx.tree` (Phase 1b), so a container needs only (a) its own child-pointer Vec
553 (ctx-less `set_rect` arrangement — the same reason legacy containers kept one) and (b) the
554 subtree plumbing every legacy container hand-copied. (a) stays in the model behind new
555 `Layout` hooks (`has_container_children`/`container_children`/`child_added`/
556 `children_cleared`/`parent_changed`/`adjust_rect`/`arrange_children`/
557 `layout_children_ctx`/`child_visible`); (b) moved into the ADAPTER once, filtered by the
558 `child_visible` policy: plain-quad aggregation with the shared rounded-bg-skip rule,
559 rounded recursion (the Element default this override had been shadowing — a latent
560 container blocker), per-kind text aggregation, `get_text_items`/`prepare_text`/`tick`/
561 popover recursion, container-style `add_child` (parents the child back), the ctx-carrying
562 child layout pass, and `is_child_visible`. `Input` gains `hits_through_children` and
563 `gates_presses` (event-proxying containers must see every press — Switcher unfocuses its
564 child on an outside click). Event proxying itself stays in the model via `EventCtx::ui`,
565 bug-for-bug (including Switcher's double `mouse_input` dispatch while a popover is open).
566 `ContentBg` turned out to be a leaf and rode the ordinary recipe. NOT for this path:
567 deep-composition containers (Page/Plate embed `Layer`; SectionContainer embeds
568 `Container`; Paginator embeds ButtonStrip + `Vec<Page>`) — embedding means migrating the
569 base struct inverts the dependency; those dissolve when their hosts move to the scene
570 walk (Phase 6), not through `Adapted`.
571 - **5o — `MenuBar` migrated; standalone `Menu` DELETED (zero constructors workspace-wide —
572 dead code).** MenuBar keeps its legacy `ButtonStrip` EMBEDDED in the model (owned by
573 value, driven through `Element` calls; events reach it via `EventCtx::ui`;
574 `arrange_children` parents it back to the adapter). New adapter surface: `Paint::popover`
575 / `draw_popover` (own dropdowns — the container recursion only covered child popovers),
576 `Layout::z_order`, `Layout::tracked_parent` (serves `Element::parent` from the model's
577 field — legacy parent-chain styling walks use a DUMMY ctx that tree lookups can't
578 answer), `Input::set_modifiers` / `visibility_changed` / `is_focused` (conditional focus:
579 the bar holds the global slot only while something is open) + `EventCtx::release_focus`,
580 the `as_page_selector` capability pair, and `Paint::corner_style` now takes the laid-out
581 rect (corners computed against the parent backplate's edges). Dropped, flagged: the
582 never-read glyphon buffer caches and the vertical-mode dynamic `rect()` (`with_vertical`
583 has no callers). Verified: 168 tests (full open→click→close roundtrip through real
584 adapter dispatch), all four hosts run, live A/B on cce-test-interface pixel-equivalent
585 (the strip has zero diffs above the 8% threshold; the File-click-opens-nothing behavior
586 there is byte-identical pre-existing app behavior). Designer A/B (was pending on screen
587 contention): DONE — startup, params-pane dropdown clicks, circular-pane mode, and a
588 View→Circular-Pane `trigger_menu_click` roundtrip all pixel-equivalent vs the f1523ab^
589 baseline (all residual diffs are composited-cursor + bottom-status-strip artifacts).
590 Driven deterministically via the designer's embedded HTTP API on :3000
591 (`{"action":"toggle_circular_pane"}`, `{"action":"menu_click","widget_idx":8,
592 "menu_idx":2,"item_idx":2}`); the params-pane Circular-Pane dropdown not opening on
593 click is pre-existing app behavior, identical in both builds.
594 - **5p — `Dropdown` (first popover widget through the 5o `Paint::popover`/`draw_popover`
595 surface). DONE (render-dump + live-A/B verified).** Slider label convention
596 (`inflates_label_rect=false`), `Control::control_label`'s +4px via
597 `detached_label_inset`, `gates_presses=false` (an open dropdown must see the outside
598 press that closes it), `opens_context_menu`, dynamic `z_order` (100 while open), and
599 focus parity bug-for-bug: `FocusIn` re-claims the global slot (direct `focus()` callers —
600 test-interface), `FocusOut` closes without releasing it. New adapter surface:
601 `Layout::intrinsic_measure_width` + an `Element::measure` override on `Adapted`
602 (identical to the `Element` default unless a widget opts in — preserves `auto_width`
603 measuring, which cce-system-settings sizes its page dropdown through). Parity decisions,
604 flagged: the public `parent` field stays direct-write-only (legacy `set_parent` never
605 wrote it — Ramp's dummy-ctx `set_parent` calls were silently discarded, so the Ramp
606 popover clamp and fade-blend parent color were dormant in production and stay dormant);
607 the backplate-concentric corner walk starts from a `parent_changed`-tracked pointer and
608 hops field-based legacy `parent(&dummy)` impls (exact for cce-graph's
609 Dropdown→Plate→Backplate chain; deep tree-only chains lose the adjustment); the row-rect
610 hit expansion is dropped, consistent with every migrated control. Verified: 169 tests,
611 full workspace builds, cce-system-settings fonts/notifications render dumps
612 content-identical (only the detached label's emission order shifts — adapter appends it
613 after the widget's prims), cce-graph startup/open/close live A/B **byte-identical**
614 (AE=0 open state on a clean run; run-to-run compositor translucency noise ~15k AE dwarfs
615 any residual), cce-fonts + cce-test-interface smoke-run. App sweep: 8 repos (graph,
616 text-editor, fonts, layout-interface, system-settings, data-editor, files,
617 test-interface) — field types to `Adapted<Dropdown>`, raw casts to `.as_ptr_mut()`,
618 two `&mut Dropdown` fn params in cce-files pages.
619 - **5q — `TextBox` (widest-surface leaf; the clipboard/selection tier). DONE (render-dump
620 + live-A/B verified, data-editor byte-identical).** New adapter surface: the `Input`
621 clipboard quintet (`cut_selection`/`copy_selection`/`paste_from_clipboard`/`select_all`/
622 `clear_text` — defaults replicate the whole-value `Element` defaults so earlier
623 migrations keep their shipped behavior), `Paint::prepare_text` (TextBox's glyph shaping
624 is load-bearing: `map_x_to_idx` reads the measured advances), `Layout::hit_row_rect`
625 (restores the legacy row-substituted, side-label-inset hit geometry — cce-files'
626 save-name box relies on row hits; earlier migrations' drop of it stands, opt-in),
627 `Layout::adjust_row_rect` + `Layout::rect_assigned` (the width/max-width clamp on both
628 rect paths; ungated scroll re-clamp on every `set_rect`), `Input::tracks_base_focus`
629 (legacy TextBox's `focus()` never set the base flag — its detached label must not color
630 as focused), and **`Paint::legacy_focus_highlight`**: the shared focus-highlight overlay
631 the adapter suppresses for every migrated widget is re-enabled per-widget — legacy
632 TextBox kept the `Element` default, and the focused editor's primary-tint wash
633 (data-editor's teal editing surface) is real legacy behavior. Found the honest way: the
634 first A/B came back 1.4M pixels apart; after restoring the overlay (replicated
635 byte-for-byte in `Adapted::highlight_quad` + the `all_quads`/`paint_self` inclusion
636 points), data-editor startup AND focused-editor states are **byte-identical (AE=0)**.
637 The asymmetric legacy render split is preserved (non-rounded `extra_quads`: full-width
638 background + disabled special-case; rounded `all_rounded_quads`: side-label inset, no
639 disabled branch). Flagged approximations: releases re-check plain-rect containment
640 (legacy hit-gated them through the row-substituted test); wheel is now hit-gated by the
641 adapter (legacy hosts dispatched it to the hovered widget themselves). Verified: 169
642 tests, workspace builds, settings fonts/processes dumps content-identical (processes
643 modulo live PID/CPU data), cce-data-editor live A/B byte-identical. Sweep: 9 app repos
644 (authenticator, data-editor, display-manager, email, files, fonts, layout-interface,
645 system-settings, text-editor) + 5 in-crate embedders (treelist, scrolling_list,
646 keybinds_control, multi_control's `InstancedWidget` variant, parameters_bg).
647 - **5r — `Paginator` (value-embedded container: ButtonStrip + Vec<Page>). DONE (live-A/B
648 verified, cce-layout-interface byte-identical across four states).** The model owns both
649 embedded legacy widgets by value and proxies events to them (strip first, draining its
650 click into the selection; then the selected page while not hidden); the container
651 concern serves them via `container_children`/`child_visible` (strip always, selected
652 page only), and `arrange_children` is the legacy `set_rect` body. New adapter surface:
653 **`Layout::register_embedded_children(host_id, ctx)`** — legacy `tick`/`layout`
654 re-registered the strip + pages into the ctx registry every frame, and that registration
655 is load-bearing (the spatial grid is rebuilt from registered widgets; the registered
656 strip is what blocks backplate drags over the sidebar —
657 `backplate::tests::test_paginator_blocks_backplate_drag`); the adapter calls it from
658 `Element::tick` and `Element::layout`, the legacy cadence. **`Paint::
659 aggregates_child_extra_quads`** — legacy container `extra_quads` served the CHILDREN's
660 chrome only, while the widget's own background quad lived in `all_quads` alone;
661 cce-mail and cce-layout-interface render the tab column through `extra_quads` over
662 their own backgrounds (emitting the bg there would double-blend), and cce-test-interface
663 renders through `all_quads` (dropping the bg there would blank it). The adapter's
664 `all_quads` now draws own prims from a shared `own_plain_quads()` helper instead of
665 `extra_quads()` (identical for every prior migration) so the two views never
666 double-serve. **`Paint::forwarded_highlight(ctx)`** — legacy `highlight_quad` forwarded
667 to the strip's (the hovered-tab tint cce-layout-interface draws by calling
668 `highlight_quad` directly); served only through that getter, kept out of
669 `all_quads`/`paint_self` (gated on `legacy_focus_highlight` now — where the strip's own
670 aggregation already carries it, as legacy container `all_quads` overrides did).
671 `PageSelector` + `MenuController` ride the existing `Input` capability hooks
672 (cce-test-interface reaches `sidebar_w` through an `as_page_selector()` downcast on
673 `dyn Element`). Ported bug-for-bug though unused workspace-wide: the `pages` container
674 surface (`add_widget_to_page`/`set_pages`/…) — every app manages page content itself
675 keyed on `selected_page()`. Verified: 171 tests, all four consumer apps build,
676 cce-layout-interface live A/B **byte-identical (AE=0)** on idle, File-tab hover,
677 Page-tab click, and Page-selected hover (exercises extra_quads aggregation, highlight
678 forwarding, labels, and the click→selection→page-switch path);
679 cce-test-interface A/B equals its launch-to-launch noise exactly (same 7.5k-px bbox —
680 animated waveform phase; the `all_quads` gallery path contributes zero residual).
681 Sweep: 3 app repos re-typed to `Adapted<Paginator>` (email, files, layout-interface);
682 test-interface's `Box<dyn Element>` gallery needed no change.
683 - **5s — `ParametersBg` (the designer's parameter panel; the last real container). DONE
684 (live-A/B verified, cce-designer pixel-equivalent across four states).** The model caches
685 its laid-out rect via `Layout::rect_assigned` (all row geometry derives from it — the
686 TextBox pattern; `arrange_children` is the legacy child-stacking tail, visible-gated by
687 the adapter exactly as legacy gated it), keeps the value-owned per-row widget vecs
688 (mostly `Adapted<W>` already) plus the raw-pointer `children` list on the 5n container
689 hooks, and ports the full bespoke event surface into `on_event` arms: the every-press
690 dispatch chain (`gates_presses=false` — the panel consumes every left press, scrollbar
691 thumb drag math, popover-first ordering, per-row-type dispatch with value commit-back,
692 the inline emacs-flavored code editor) and the host-driven drag surface on the `Input`
693 drag hooks. New adapter surface: **`Paint::serves_legacy_labels` /
694 `legacy_labels_with_font_and_bounds(rect, ctx)`** — the text sibling of the 5m
695 dual-geometry hatch: the standard bridge gives every own label ONE font and ONE clip
696 rect, but this panel assigns them PER LABEL (viewport clip everywhere, code-box clip +
697 monospace inside a code row); served as a full replacement, children included. Also
698 **`EventCtx::widget_addr()`** — the wheel arm's occlusion check
699 (`is_coordinate_covered`) keys on the adapter's address, which `on_event` couldn't
700 reach. Reuses 5m's plain-quad hatch for the designer's raw `extra_quads` render path
701 (row chrome, section borders, code cursor, scrollbar — with the panel's translucent
702 PARAM_BG plate deliberately NOT emitted: the designer draws it from
703 `color()`/`corner_style`, the 5l double-blend trap). `window_runner`'s
704 `get_child_widget_for_quad` downcast keeps working unchanged (`as_any` exposes the
705 inner type; the 9 pub sub-widget fields stay pub). Flagged approximation: the legacy
706 scrollbar-press called `self.focus()` (base flag only — nothing reads it; the highlight
707 keys on the ctx focus slot and the designer tracks panes by index); dropped.
708 Verified: 175 tests (4 new: controller roundtrip + row layout, checkbox/code-editor
709 commit flows, hatch split, overflow scrolling), cce-designer builds with ZERO app
710 changes (`Box::new(ParametersBg::new())` coerces), live A/B across startup /
711 wheel-scrolled pane / dropdown-row click / second click identical except the bottom
712 status strip — calibrated as launch-to-launch live-data noise (same 1447×21 bbox,
713 77 px between two launches of the SAME baseline vs 81 px old-vs-new). The wheel state
714 changed 107k px within a build and matched across builds, so the event path is
715 genuinely exercised. (The params-pane Circular-Pane dropdown not opening on click is
716 the pre-existing app behavior recorded in 5o, identical in both builds.)
717 - **5t — the deferred leaves: `Float3`, `LayoutPreview`, `PreviewState`, `StatusBar`.
718 DONE (live-A/B verified: cce-files byte-identical ×3 states, cce-designer params pane
719 byte-identical ×3 states, cce-system-settings byte-identical whole-window).**
720 Consumer survey first (it reshaped the work): every external `Float3` grep hit is the
721 MATH type (designer `GAttribute::Float3` / wgpu `Float32x3`) — the widget's only
722 consumer is ParametersBg, whose pub-field reach (`values`/`mins`/`maxs`/`edit_buffer`/
723 `editing_idx`) flows through `Deref` unchanged; LayoutPreview has ZERO consumers
724 (definition + re-exports only); PreviewState is cce-files' preview pane; StatusBar has
725 six construction sites across five apps plus the demo.
726 **Float3**: rect cached via `rect_assigned` (`get_row_rects` is pub API with no rect
727 param), readout-edit + track-drag into `on_event`/the drag hooks, the readout click's
728 legacy `focus::set_focused(self)` rides `EventCtx::request_focus`, commit-on-unfocus via
729 the FocusOut arm. **LayoutPreview**: mechanical (paint = quads + text; the duplicated
730 SimNode match collapsed into one helper). **PreviewState**: the widget the 5s labels
731 hatch was built for — canvas quads flow from `paint`, canvas labels (per-label
732 monospace for content lines) through `serves_legacy_labels`; plain `text_labels` stays
733 EMPTY like legacy (emitting the text as prims too would double-render under container
734 aggregation — the legacy scene path showed no text either, preserved). The adapter
735 gains a blanket `impl Default for Adapted<W: Default>` (cce-files constructs it via
736 `Default`); the app's struct-literal update became field mutation (the private cached
737 rect can't ride functional-update syntax — and now survives updates instead of zeroing
738 until the next layout pass). **StatusBar**: the MenuBar parent-coupling pattern
739 (tracked parent, backplate-aware color/text-color/blur, corners-against-parent at the
740 parent's radius) plus ONE new hook — **`Paint::text_items()`**: pre-shaped glyphon
741 buffers for the legacy `get_text_items` path, which prim-derived text cannot serve (it
742 returns borrows of widget-owned buffers); cce-status-interface drives the bar by hand
743 (`prepare_text` → `get_text_items` into its own paint) and data-editor/system-settings
744 host it as a Backplate child. `inline_label` keeps `Element::set_text`'s base-label
745 write from leaking a detached label. Deliberately preserved asymmetry: NO
746 `widget_font`, so the container text path keeps rendering the bar's text in the
747 default font while the buffer path uses the statusbar font, exactly as legacy.
748 Verified: 177 tests (Float3 readout/drag flow; StatusBar manual-host pipeline —
749 covering the path of the one app, cce-status-interface, not A/B'd live: it is the
750 user's session status bar). Sweep: 4 app repos (files: field + literal→mutation;
751 data-editor: field + one raw `*mut StatusBar` cast → `as_ptr_mut()`; system-settings +
752 status-interface: field types).
753 - **Phase 5 widget migration COMPLETE.** Everything remaining on `impl Element` is
754 embedded-base machinery by design: the containers
755 (Layer/Container/Page/Plate/Backplate/ScrollBox/List/…) dissolve via Phase 6 scene
756 adoption instead. Then delete `Element` + `Adapted` once the last widget is across, at
757 which point the `as_*_controller` pairs and the `Input` capability hooks die together
758 (callers hold concrete types or `&dyn XController`).
759 - **Phase 6 — Per-app migration.** Move each `cce-*` app onto the new core; delete legacy paths
760 once the last app is across. **Definition of done per app:** the whole frame — geometry AND
761 text — is one `Application::display_list()` (+ `display_list_text()`), layout runs through
762 the scene solver where the app has a real tree, events reach widgets through routed dispatch
763 rather than hand-rolled per-widget loops, and no embedded-base container
764 (Layer/Page/Plate/Backplate/ScrollBox/List) is load-bearing. Seven apps already feed
765 geometry through `display_list()` (colors, data-editor, files, fonts, graph, text-editor,
766 system-settings) — their remaining gaps are text, layout, and events.
767 - **6a — display-list text. DONE (live-A/B verified via cce-notifier).** `Prim::Text` now
768 carries `font: Option<String>` + `bounds: Option<[f32;4]>` (`PaintCtx::text_with`; plain
769 `text` emits None/None), and the backend renders a list's Text prims through the glyphon
770 pass — shaped via the shared `get_text_buffer` cache, clipped to the item clip ∩ the prim
771 bounds, held in `EngineState::dl_text_items` so the `TextArea`s can borrow the buffers.
772 **Opt-in via `Application::display_list_text()` (default false)**: the seven Phase 3
773 adopters' lists already carry Text prims that those apps ALSO push as `TextItem`s —
774 rendering both would double-draw; each app flips the flag when it stops pushing its own.
775 `Application::view`/`text_items` gained no-op defaults so a fully migrated app implements
776 neither. Known limitation (scoped out, not a bug): the legacy `text_areas`
777 popover-occlusion clip is not applied to display-list text yet — a popover plate does not
778 hide list text beneath it (text draws after all geometry); apps with popovers keep their
779 own text path until that lands. The 5s/5t labels hatches become expressible as prims once
780 hosts consume lists directly.
781 - **6b — `cce-notifier` (first app fully on one path). DONE (live-A/B on a private D-Bus
782 session: text/accent pixel-identical; 60-px residual is compositor translucency noise in
783 the alpha-0.9 background region).** The whole frame is one display list (accent quad +
784 three `text_with` prims in the configured bundled family); deleted: the app-side
785 `FontSystem`, the `TextItem` cache, `rebuild_layout`, and the scale/rebuild bookkeeping.
786 Non-interactive, so no event surface. This is the reference shape for a minimal Phase 6
787 app.
788 - **6c — `cce-wallpaper` + `cce-screenaver` across; `display_list` gains `(size, scale)`.
789 DONE (wallpaper live-A/B AE=0; screensaver background-fill verified live, sim quads are
790 the same mechanical loop).** The Phase 6 frame entry point now receives the frame's
791 logical size and HiDPI scale like `view` did (fullscreen apps size geometry from it);
792 mechanical sweep across the eight implementors. Both apps' dead `TextItem` caches
793 deleted.
794 - **6d — the paint walk carries per-widget fonts + clip rects. DONE (179 tests; cce-graph
795 live A/B shows zero structural diff — all residual below the 8% translucency-noise
796 amplitude).** `Adapted::paint_self` no longer forwards `Paint::paint`'s plain Text prims:
797 it re-emits the geometry verbatim (through the ctx so the walk's offset/clip apply
798 once) and serves text as `text_with` prims from the SAME views the standard text bridges
799 use — `own_labels_with_font_and_bounds` (prim text + detached base label, `widget_font`,
800 `text_bounds` or the scroll-ancestor clip) or the 5s per-label hatch verbatim (caveat
801 noted in-code: the hatch contract includes raw container children). This makes a
802 `paint_tree` display list's text renderable-correct for migrated widgets, which is the
803 precondition for the seven adopters flipping `display_list_text`. Found and recorded on
804 the way: the LEGACY `Element::paint_self` default drains the child-aggregating
805 `text_labels` for legacy containers, so scene-path text double-emits under the walk for
806 trees that still contain Layer/Page/etc. — invisible today (text prims unrendered
807 without the opt-in), but it means an app can only flip `display_list_text` once its
808 tree is embedded-base-free. Consistent with the dissolution plan; revisit per app.
809 - **6e — `cce-colors` flips `display_list_text` (first of the seven adopters). DONE
810 (live-A/B'd; slider drag re-verified via wlrctl).** The whole frame is one
811 `display_list()` — the rebuild check moved off the deleted `view`/`view_rounded_quads`
812 overrides, `rebuild_layout` keeps the flat PageContent text tuples and the list emits
813 them as `text_with` prims; deleted: the app-side `FontSystem`, the `TextItem`
814 assembly, and the `CCE_LEGACY_PAINT` fallback. The A/B exposed a PRE-EXISTING runtime
815 bug this fixes: the app shaped its `TextItem`s with its own
816 `create_font_system_with_system_fonts()`, whose fontdb face IDs don't resolve in the
817 engine's render `FontSystem` — every `font: None` label (slider names, channel
818 values, hex readout) was INVISIBLE at runtime in the baseline (only the bundled-font
819 button labels survived). Shaping through the engine's FontSystem (the dl-text path)
820 is what makes the text render at all. Note for the remaining adopters: an app-side
821 `FontSystem` is not just dead weight, it is a live font-resolution hazard — check
822 each app's text for the same silent invisibility before trusting its baseline
823 capture. cce-colors' safety: no popovers, no paint_tree (flat-list bridge), so
824 neither the popover-occlusion gap nor the 6d embedded-base double-emit applies; its
825 root `Backplate` remains for legacy layout/events (full definition-of-done still
826 pending events + layout).
827 - **6f — `cce-files` flips `display_list_text`. DONE (live-A/B pixel-identical, AE=0;
828 dropdown popover + breadcrumb context menu re-verified live).** Same mechanical shape
829 as 6e: rebuild check into `display_list()`, `rebuild_layout`'s text tuples emitted as
830 `text_with` prims, `view*`/`CCE_LEGACY_PAINT`/TextItem assembly deleted. Two deltas
831 from colors: the app-side `FontSystem` STAYS (TextBox/List `prepare_text` measurement
832 still needs it — it's `create_font_system()`, bundled-only, so no 6e invisibility
833 hazard), and popover occlusion needed nothing from the engine — cce-files folds
834 popover/context-menu/dialog occlusion into each text's clip bounds app-side
835 (`occlude_against`, both axes), and those bounds ride along as prim bounds. That's
836 the general pattern for flat-list-bridge apps with popovers: the engine's missing
837 dl-text occlusion pass only blocks apps that rely on the DEFAULT `text_areas`
838 popover clamp (`ui_context().active_popovers`), e.g. widget-tree apps whose popovers
839 register through `register_popover`.
840 - **6g — popover occlusion for display-list text lands; `cce-system-settings` flips
841 `display_list_text`. DONE (live-A/B pixel-identical, AE=0; page-dropdown popup, page
842 switch, service-list scroll, live process refresh exercised).** Engine:
843 `popover_occlusion_clamp` extracted from the default `text_areas` mapping and applied
844 to `dl_text_items` in `render()`, driven by `ui_context().active_popovers` — the
845 display_list_text known limitation is gone; apps whose popovers register through
846 `register_popover` (which `render_widget` does for any open `popover_rect`) can flip.
847 App: same mechanical shape as 6e/6f, with the app-side `FontSystem` kept for
848 button-label width measurement (centering) and the search-match highlight rect, and
849 the wheel fast-path mutating tuple y/bounds in place exactly as it did TextItems.
850 Popovers/context menu were never main-surface here: they draw on the engine's
851 xdg-popup surface via the `render_popovers` collector, which is orthogonal to the
852 flip.
853 - **6h — `cce-text-editor` flips `display_list_text`; `Paint::text_font` lands. DONE
854 (live-verified: frame matches baseline modulo a uniform ~2px baseline shift from
855 engine line-height shaping; File-menu popup + occlusion of editor text beneath it;
856 click cursor placement identical to baseline).** The FIRST app rendering scene-walk
857 text prims — its tree (Adapted Dropdown + Adapted TextBox, no embedded-base
858 containers) was exactly the 6d-safe shape, unblocked by the 6g occlusion clamp.
859 Engine: `own_labels_with_font_and_bounds` split into a parameterized helper — the
860 tuple getters keep `widget_font` for every label (unmigrated apps byte-identical),
861 while the walk view (`own_labels_for_walk`) attaches the new `Paint::text_font`
862 (default = `widget_font`) to prim-derived labels; the detached base label keeps
863 `widget_font`. TextBox overrides `text_font`: a customized `font_family`/`font_size`
864 serves the bare family name so the value text draws in the widget's own font at the
865 label's size (the control-font string's size suffix would otherwise override it) —
866 this is what keeps the editor monospace. App: view()'s side effects (registration,
867 initial focus, relayout, popover registration) moved into `display_list`; chrome
868 text emitted as prims in the system mono family; `rebuild_text_items` + hand-rolled
869 Buffer shaping deleted; the app `FontSystem` stays for `editor.prepare_text` (glyph
870 advances — cursor↔pixel mapping).
871 - **6i — the 6d trap is FIXED in the walk; `cce-data-editor` flips `display_list_text`.
872 DONE (live-verified: full-frame parity modulo the uniform ~2px engine line-height
873 shift; row selection → inline value editor + statusbar update; choice-dropdown
874 popover renders and occludes rows beneath).** Engine, two changes that make a
875 `paint_tree` list's text emit exactly once — the embedded-base-dissolution
876 precondition is GONE for the flip step: (1) the legacy `Element::paint_self` default
877 emits text only for LEAVES — the legacy container `text_labels` overrides
878 (Backplate/Layer/Page/SplitBox/Plate) aggregate their children's labels, which the
879 walk reaches itself; a legacy container with OWN text overrides `paint_self` (Plate
880 now serves its label this way; SectionContainer still aggregates from internal
881 non-child widgets and needs the same treatment if it is ever walked). (2) the walk's
882 `renders_own_subtree` branch (TreeList) emits the subtree's text from the recursive
883 `text_labels_with_font_and_bounds` — the walk never descends there, so the aggregate
884 is that subtree's text once, with the tuple pipeline's fonts/bounds. App: the old
885 `view()` body (registration, focus, relayout, inline-editor placement, popover
886 registration) moved into `display_list`; `rebuild_text_items` shrank to a
887 widget-state refresh (rebuild_tree, prepare_text, statusbar text); the toolbar file
888 label is a prim; `add_element_labels` and the TextItem cache deleted (−240 lines).
889 - **6j — `cce-graph` flips `display_list_text`. DONE (live-verified: static A/B
890 residual is only the uniform ~2px engine line-height text shift; node selection,
891 View-menu popover occluding the node beneath, control-panel toggle with its
892 multi-line info text via the 6i container fix).** Same recipe as 6i; the app
893 `FontSystem` deleted outright (no prepare_text dependency). Also fixed a latent
894 Phase 3 loss found on the way: the loaded-image pixel quads and selection borders
895 were pushed into `view()`'s plain quads, which the backend DISCARDS when
896 `display_list` returns Some — images had only rendered under `CCE_LEGACY_PAINT`
897 since the Phase 3 adoption; they now emit into the display list itself.
898 - **6k — `Application::load_system_fonts` lands; cce-fonts opts in. DONE (live-verified:
899 baseline previews Berkeley Mono (bundled) but drew Adwaita Mono (system-only) BLANK —
900 the app's core purpose was broken for installed fonts; with the opt-in Adwaita renders
901 as its own face).** The render-FontSystem design settled as a bool `Application` hook,
902 consulted once at GPU init: the engine's `WgpuAdapter` FontSystem loads system fonts
903 additively (bundled first, so fontdb face IDs stay aligned with every
904 `create_font_system*` database — the alignment that makes app-side-shaped buffers
905 rasterizable engine-side). This was the same face-ID-mismatch class as the 6e
906 cce-colors bug, and it predates Phase 6 entirely.
907 - **6l — `TextAttrs` lands; `cce-fonts` flips `display_list_text`. ALL TEN display_list
908 adopters are now fully on the single paint path. DONE (live-verified: style popover
909 renders with labels on top and occludes the text beneath via the ui_context-only
910 registration; selecting Italic re-renders the alphabet in the italic face).** Engine:
911 `Prim::Text` gains `attrs: TextAttrs { italic, weight }` (toolkit-plain — no glyphon
912 types in the scene layer), emitted by `PaintCtx::text_attrs`, shaped by
913 `get_text_buffer_attrs` (cache key includes them). App: same recipe, plus the popover
914 drawn INTO the list (replacing `overlay_quads`) with labels bounded to the popover
915 rect, and the open popover registered in `ui_context` ONLY — a global registration
916 would spawn an empty xdg popup (no `render_popovers` here). Restored two more Phase 3
917 view()-quad losses (panel borders, alphabet box) and fixed the alphabet's premature
918 wrapping (legacy passed a LOGICAL width to `set_size` on a physical-unit buffer).
919 - **6m — first container dissolution: `cce-data-editor`'s root Backplate. DONE
920 (live-verified: A/B residual 43px over the 8% threshold — translucency noise;
921 selection/editor/statusbar interactions exercised; held window drag not headlessly
922 drivable, covered by the new unit test).** The root-Backplate dissolution recipe,
923 now established: (1) the plate becomes prims replicating `Backplate::color()`/
924 `corner_radius()` (page-low bg at active backplate opacity, config radius); (2)
925 top-level widgets register directly in `ui_context`, parentless, and the paint walk
926 runs per top-level widget in the old child order (composite widgets keep their own
927 children — the splitter still owns its panes); (3) window dragging answers via the
928 new `UiContext::drag_allowed_at` — the `is_movable_backplate_at` candidate walk
929 minus the registered-movable-Backplate requirement, because the surface itself is
930 the movable plate once the Backplate is gone.
931 - **6n — `cce-graph`'s root Backplate dissolved; two engine input holes fixed. DONE
932 (live-verified: A/B residual 14px over the 8% threshold; View menu → Control Panel
933 toggle → panel renders through the walk).** The 6m recipe applied (plate prims,
934 parentless top-level registration, walk in old child order, `drag_allowed_at`);
935 popovers moved to the 6l ui_context-only registration (the global registration
936 spawned a render-only xdg popup double-drawing the menu; `render_popovers` override
937 deleted). Found en route, both pre-existing: (1) a press inside an OPEN popover's
938 plate could start a window move and swallow the click when the widget beneath does
939 not block dragging (Graph's edge-exclusive canvas hit) — both drag questions now
940 veto via `point_in_active_popover`; (2) the engine's render-only popups took input
941 with their default full input region — they now carry an EMPTY input region.
942 Verification lesson recorded: the compositor drops pointer focus after each click
943 (Leave with no re-Enter on in-window motion), so headless click sequences MUST
944 re-park the pointer (`wlrctl pointer move -10000 -10000`) before every click — a
945 skipped re-park looks exactly like an input regression.
946 - **6o — graph's two Plates dissolved: `cce-graph` is EMBEDDED-BASE-FREE (the first app
947 to get there via dissolution; text-editor was born free). DONE (dropdown row A/B'd to
948 the solver's exact rects after switching to the bridge's own sizing entry,
949 `Element::intrinsic_size` — a `measure` call returns display-label widths instead;
950 control-panel area A/B: AE=0).** The transparent dropdown-row Plate (pure layout
951 shim) became direct placement; the draggable control panel became app state + prims
952 replicating Plate's exact visual (config plate color / drag tint, plate opacity,
953 negative-alpha blur flag, border, radius) with the label walked standalone via
954 Plate's centered-first-child rule. Panel drag reimplemented properly app-side —
955 NB the legacy `Plate::on_cursor_moved` forwarded drags only to CHILDREN, so the old
956 panel's own drag likely never moved it (manual drag check pending).
957 - **6p — cce-fonts' root Backplate + all three Plates dissolved. DONE (live-verified:
958 family select, style popover + occlusion, Oblique re-render, select-mode bar).** The
959 6m/6o recipes at full width, plus the first APP-OWNED EVENT DISPATCH: a
960 `dispatch_widgets` list replicating the Plates' forwarding — popover-first press
961 pass, unfocus-on-missed-press, `drag_update` forwarding for dragging children
962 (scrollbar thumbs), panel-grouped order. The A/B surfaced another legacy
963 double-draw: `Plate::paint_self` aggregated child plain-quads while the walk painted
964 the child again, double-compositing the ScrollBox background (~22 units darker) —
965 the dissolved single-draw is the correct rendering. Remaining in fonts: `ScrollBox`
966 (mid-panel scroll state/scrollbar) and `List` (browse-list scroll/frame) — the last
967 two embedded-base types in the app.
968 - **6q — fonts' ScrollBox + List dissolved: cce-fonts is FULLY EMBEDDED-BASE-FREE.
969 DONE (live-verified: wheel scroll, scrollbar track-jump lands proportionally, family
970 click after deep scroll + scroll_to_index, alphabet re-render in the new family).**
971 Both were pure scroll frames in this app (List with columns=None; the rows are
972 standalone Buttons), so they reduce to one app-owned `ScrollRegion` (~150 lines):
973 scroll state, wheel, thumb-grab/track-jump/drag, hover-scoped keyboard scrolling,
974 item-y math with List's silently adjusted item height (max(24, list font + 14)), and
975 bg/track/thumb prims. The bg is a single list_bg layer — the legacy leaf-walk
976 stacked rounded + plain copies (the translucent double-compositing class again);
977 residual A/B delta is a 2px bottom-edge strip.
978 - **6r — colors' and files' root Backplates dissolved, BOTH A/B'd to AE=0.** These are
979 the flat-pipeline (render_widget) apps, and their dissolution surfaced the legacy
980 aggregate's GLOBAL tuple-order contract: `render_widget(root)` emitted every
981 descendant's PLAIN quads first (via `all_quads` aggregation), then the root's
982 rounded bg, then every descendant's ROUNDED quads — so the root's translucent plate
983 WASHES over the plain content (colors' muted slider gradients depend on it; a
984 naive plate-first order renders saturated). Replication: per-child `render_widget`,
985 partition the tuples by radius, and interleave [plain…, plate, rounded…]. Wheel in
986 colors propagates per-slider; both apps answer dragging via `drag_allowed_at`.
987 Files' view-dropdown popover + breadcrumb context menu re-verified live.
988 - **6s — settings' root Backplate + StatusBar dissolved; `Dropdown::set_corner_frame`
989 lands. DONE (WINDOW_PC tuple stream byte-identical; pixels AE=0; live: dropdown
990 popover → page switch to Processes with statusbar text following, wheel scroll,
991 service-list render).** Settings needed what colors/files didn't: its plate radius
992 is a hardcoded 12, so the legacy aggregate's corner RESOLUTION mattered — a child
993 plain quad flush with a window corner picks up the plate radius there (the
994 `render_widget` extra-corners logic against the ROOT rect). The hand assembly
995 replicates the full aggregate: child plain quads (window-clipped, root-corner-
996 resolved), plate, child rounded quads, root-clamped text, in the old child order.
997 Two parent couplings surfaced (the widgets read their Backplate ancestor):
998 (1) StatusBar — bg falls back from the backplate-statusbar theme color to
999 STATUS_BG, bottom corners round at the PARENT's radius, text color/font are
1000 backplate-specific; it dissolves outright (pure chrome in this app) into tuples +
1001 a `status_text` String. (2) Dropdown — the backplate-concentric corner cut walks
1002 for a Backplate ancestor and silently degrades to a plain rounded box when the
1003 walk finds nothing; new `Dropdown::set_corner_frame((rect, radius, corners))`
1004 hands it the frame explicitly and takes precedence. Also found (pre-existing,
1005 reproduced on the pre-6s baseline): the engine xdg-popup positioner anchors at
1006 the widget's BOTTOM edge regardless of the app's open-upward popover rect, so
1007 settings' page popover displays below the window while clicks land on the
1008 app-side (invisible, in-window) popover rect — the engine popup path's last
1009 consumer; fix when settings' popovers move to the 6l ui_context-only pattern.
1010 The root's `with_border` was never rendered (a rounded Backplate emits no plain
1011 bg quad; the border branch fires only on plain bg quads) — dropped, not ported.
1012 - **6t — settings' popovers + context menu draw INTO the frame;
1013 `Application::draws_own_popovers` lands. DONE (live-verified: dropdown popover
1014 in-window with page geometry occluded beneath, item click switches pages both
1015 ways, spinbox right-click context menu at cursor with dl-text occluded beneath,
1016 dismissal).** This fixes the 6s finding at the source: the engine's render-only
1017 xdg popup anchored at the widget's bottom edge regardless of the app's open-upward
1018 popover rect, so settings' page popover displayed BELOW the window while clicks
1019 landed on the app-side in-window rect. The app now runs the same
1020 `render_popovers` collector into its own tuple stream (appended above window/
1021 page/search content; kept out of the scrollable vecs so the wheel fast-path can't
1022 shift popover content) and deletes the override. Page-widget popovers
1023 (notifications/fonts menus) shift by −scroll_y — the subtraction the popup
1024 positioner used to apply — keeping display aligned with hit-testing under scroll.
1025 Engine: `draws_own_popovers` (default false) gates BOTH popup spawn triggers (the
1026 global popover registry and global context-menu visibility), and under the flag
1027 `render()` adds the visible context menu's rect to the dl-text occlusion overlays
1028 (the menu is engine-global state, not a `ui_context` popover; its own labels are
1029 exempt via bounds == rect). Remaining popup-path consumers: cce-data-editor and
1030 cce-text-editor (`render_popovers` overrides) — the popup surface, `ActivePopup`,
1031 `PopoverCollector`, and this flag all go away once they draw their own.
1032 - **6u — settings' Switcher + Page dissolved; the System page comes back from the
1033 dead. DONE (audio A/B: window tuple stream byte-identical, pixels AE=0;
1034 live-verified across six pages — spinboxes, context menu, page switching, wheel
1035 + fallback, System governor dropdown + scroll, notifications dropdown, fonts
1036 textbox focus).** The top two tree layers reduce to app state: the active page
1037 was always `app.current_page`, page scroll was already `scroll_y`, so what was
1038 load-bearing was Page's scrollbar child, its out-of-bounds event gate, keyboard
1039 scrolling, and being the propagate root. `dispatch_page_event` replicates the
1040 routing (OOB gate with scrollbar-drag bypass; scrollbar first with the y-unshift,
1041 then sections in reverse child order; PointerMove visits all, others stop at the
1042 first handler) against the app-held SectionContainer clones; the scrollbar is an
1043 app field whose quads collect into the window assembly's plain slot with the
1044 legacy one-frame-stale content height. Found on the way: the System page's
1045 widget-tree render path — the only page not on immediate-mode — SEGFAULTED at
1046 launch on the pre-6u baseline (raw-pointer one-time section/label tree; the
1047 use-after-free class this rebuild exists to kill). A complete immediate-mode
1048 view for it existed in the file, never wired to the `AppPage` impl; 6u flips it
1049 (labels → `sec.text`, InfoBoxes advance the section cursor, menus linked into
1050 the clone sections like every other page). Pre-existing, deferred to the
1051 List/ScrollBox dissolution: the processes lists' inner wheel is dead; the page
1052 scrollbar's right half sits in the compositor's 8px edge-resize zone.
1053 - **6v — settings' List/ScrollBox dissolved (all five lists). DONE (A/B render
1054 dumps on processes/packages/radios: rect streams byte-identical minus one
1055 duplicated pair per list, see below; live-verified — per-list wheel, page-scroll
1056 fallback, scrollbar track-jump that sticks, focus tint, package row click →
1057 selection + info fetch, scroll state surviving watcher rebuilds).** Every
1058 settings list was a pure scroll frame (`List` with `columns: None`; the pages
1059 draw the rows), so the recipe is fonts' 6q `ScrollRegion` ported app-side
1060 (`cce-settings/src/scroll_region.rs`) with the List-flavored visuals (1px
1061 focus/hover-tinted rounded border + inset bg) and the List-mirroring API the
1062 pages already used. Routing: `AppPage` grows `extra_dispatch_roots` — the
1063 `InteractiveListItem` rows dispatch directly as propagate roots (`Adapted`'s
1064 press/wheel hit-gate makes misses fall through, so root order is immaterial) —
1065 plus `handle_mouse_wheel` (after widget dispatch, before the manual page-scroll
1066 fallback: the legacy "inner ScrollBoxes take the wheel first" slot) and
1067 `handle_key_input` (hover/focus-scoped, before the page's scroll-key fallback);
1068 regions ride the existing pointer down/move/up hooks (audio's slider-drag slots).
1069 This FIXES the 6u-deferred dead inner wheel, and two latent visuals of the
1070 columns=None List path: `List::extra_quads`' early return never removed the
1071 ScrollBox bg quad, so `render_widget` emitted the border+bg pair TWICE (plain
1072 bg through the solid-border branch + `all_rounded_quads`) — the 6p double-
1073 composite class — with the scrollbar track/thumb sandwiched UNDER the second
1074 translucent bg wash. Single-drawn now; the inner scrollbars are visible for the
1075 first time. Only other A/B delta: item-label clip bounds relax by ScrollBox's
1076 4px text inset (rows are fully-visible-culled, nothing renders in that band).
1077 Replication trap for other apps: the region's `focused` is a local bool
1078 (press-inside sets, press-miss clears) standing in for the global
1079 `focus::set_focused(scroll_box)` — ctrl-nav can no longer land on a list, and
1080 the focused border tint shows through the translucent bg as a green wash
1081 (legacy did this too, darker under its doubled bg). Spot-check PASSED
1082 (2026-07-13, via ccectl held-drag injection, cce 881c2b1): held thumb drag
1083 scrolls and tracks mid-drag; arrow scrolls one row; PageUp/PageDown page both
1084 directions over a hovered list. The green focused wash appeared as documented.
1085 - **6w — settings' SectionContainer dissolved; cce-system-settings is
1086 embedded-base-FREE. DONE (A/B render dumps: all nine pages byte-identical
1087 modulo live data — the sections never painted; live-verified — notifications
1088 spinbox + menu open/select with in-frame occlusion, audio spinbox round trip
1089 through pactl and the watcher, processes filter-box click-to-focus, services
1090 list wheel).** The per-rebuild section clones were pure event/focus plumbing:
1091 propagate roots whose `container` children were the pages' widgets, plus the
1092 ctrl-nav focus targets. `AppPage::section_widgets()` (one widget-pointer group
1093 per section, old count/order) replaces `get_section_containers` +
1094 `link_children` + `clear_children`; the widgets dispatch directly as propagate
1095 roots flattened in the legacy order, and section-level keyboard focus is an
1096 app-side index, single-slot with the global widget focus exactly as when both
1097 lived in `FOCUSED_WIDGET` (entry → section 0; ctrl+j/k cycle; ctrl+i descends
1098 to the section's first widget — the legacy walk went through the
1099 header/container intermediates; ctrl+u ascends from a widget to its section;
1100 a focus-taking click and page switches drop the highlight). Also killed a
1101 latent use-after-free of exactly the class this rebuild targets: the focused
1102 section clone was dropped and reallocated EVERY rebuild while the global
1103 focus pointer kept aiming at it — it survived only because same-size Vec
1104 reallocation tends to reuse the freed block. Ctrl-nav spot-check PASSED
1105 (2026-07-13, nested rig) — and it took two fixes to get there. (1) cce 114ef89:
1106 injected key-down/key-up now updates an injected xkb mask and pushes a
1107 modifiers event (OR'd over the device state), so ctrl/shift/alt/super combos
1108 land like hardware. (2) The check then caught a REAL cce-ui bug (ec511f4):
1109 handle_key preferred event.utf8, which xkb control-transforms while Ctrl is
1110 held (ctrl+j arrived as Character("\n")) — settings' ctrl-nav could never
1111 have fired from real hardware either. With the keysym preferred under Ctrl,
1112 ctrl+j/k cycle the section highlight and ctrl+i/u descend/ascend
1113 (audio-page border diffs). ~~NB accounts/storage take `_sec_focused` and
1114 render no highlight — ctrl-nav is invisible on those two pages.~~ FIXED
1115 2026-08-13 (cce-system-interface@`573bf43`): both pages now thread
1116 `sec_focused` into their `view()` and pass it per section instead of a
1117 hardcoded `false`. Live-verified — storage cycles the highlight
1118 Local Storage → Memory on successive ctrl+j, accounts highlights its single
1119 section (42,513 px changed in a rectangle-outline distribution: dense at the
1120 well's top and bottom edges, constant down the sides). All 14 pages now
1121 consume `sec_focused`; none takes it as `_sec_focused`.
1122 Re-verified on the LIVE session (2026-07-13, after the compositor restarted
1123 onto the 114ef89 binary — the restart was the only reason this had been
1124 nested-rig-only): ctrl+j → j → k and ctrl+i → u on the audio page, both
1125 round-trips returning pixel-identical frames (0 AE diff), highlight
1126 visually confirmed cycling Output→Input. Nothing pending on ctrl-nav.
1127 - **6x — data-editor + text-editor off the engine popup path; the render-only xdg
1128 popup machinery is DELETED. DONE (live-verified: text-editor File menu open +
1129 item click; data-editor recent-files menu → config.kdl load, tree context menu
1130 with occlusion + Copy Key through the clipboard; settings page dropdown +
1131 page switch unaffected after losing its gate).** Both apps now collect their
1132 ui_context-registered popovers via `PopoverCollector` and emit them last in
1133 the display list (data-editor appends the global context menu too), labels
1134 bounded to the overlay rect — the 6l/6t recipe; registration is
1135 ui_context-only. With the last consumers across, the engine sheds the whole
1136 popup path: `ActivePopup` (wgpu surface + viewport + vertex buffer per
1137 popover), the xdg positioner/spawn/despawn block in the event loop, the popup
1138 render pass, the popup-surface pointer-coordinate translation, the
1139 `PopupHandler` + `delegate_xdg_popup` plumbing, and the
1140 `Application::render_popovers` + `draws_own_popovers` hooks (settings'
1141 override removed; the context-menu dl-text occlusion rect is now
1142 unconditional). Every popover in the workspace is app-drawn, in-frame, where
1143 it hit-tests — the 6s below-window-popover class of positioner bugs is
1144 unrepresentable. NOTE: the compositor-side dismissal in `PopupHandler::done`
1145 (unfocus popovers + hide context menu when the popup was dismissed) went with
1146 it — in-frame apps already own dismissal (press-outside), same as
1147 fonts/settings. The global `widget::popovers` registry is now write-only
1148 (apps still clear/register into it) — delete it with the legacy paths.
1149 Drive-by: cce-designer had not compiled since 6k (direct `WgpuAdapter::new`
1150 call missing the new `load_system_fonts` bool) — fixed.
1151 - **6y — files' SplitBoxes + BrowseContainer/NetworkContainer dissolved. DONE
1152 (browse page A/B: zero >8%-amplitude pixel diffs; network page's only diff is
1153 a removed paint bug, see below; live-verified — row select, double-click
1154 navigation, view-dropdown page switch, divider hover tint via hover-on/off
1155 crop diff, preview populate).** The split reduces to an app-owned `SplitPane`
1156 (frac + divider drag/hover + divider quad — the SplitBox two-child horizontal
1157 math verbatim); the pane containers were pure layout shims whose child copies
1158 the pages have always re-rendered on top (the Phase 0 double-paint), so the
1159 window assembly now emits only the divider quad and the preview pane
1160 (`render_widget` at the right pane rect, text clamped to the pane like the
1161 legacy SplitBox bounds clamp). Killed on the way: the left pane's under-copy
1162 double-compositing every translucent quad, including the NetworkContainer's
1163 full-width breadcrumb-copy strip that visibly leaked behind the graph page's
1164 top bar — the exact class the Phase 0 stopgap patched for Browse only.
1165 Verification trap for the log: `wlrctl` pointer warps land as Enter WITHOUT
1166 Motion — nudge (`move 2 2`) after warping or app hover state never updates
1167 (cost an hour chasing a "broken" divider tint that was fine). Held divider
1168 drag spot-check PASSED (2026-07-13, ccectl pointer-press/release): frac
1169 tracks the held drag, panes re-lay out, window stays put. The Enter-without-
1170 Motion trap turned out to be a REAL RUNNER GAP, not just an injection quirk:
1171 the backend's Enter arm set the cursor icon but never fed the enter position
1172 to `handle_pointer_move`, so a press straight after crossing into the window
1173 hit the movable-backplate check with stale hover and moved the WINDOW instead
1174 of grabbing the divider. Fixed in cce-ui 9e23229 (Enter now routes like
1175 Motion); re-verified no-nudge cross+press drags the divider.
1176 - **6z — files' List dissolved; cce-files is embedded-base-FREE. DONE
1177 (live-verified: row click select with preview/details update, double-click
1178 navigation, breadcrumb navigation, wheel scroll with selection retained,
1179 hover tint, item count; 3 new RowList unit tests).** The column-mode List
1180 flavor ports verbatim to the app-owned `RowList`
1181 (`cce-files/src/row_list.rs`): column_bounds (Flex/Absolute/RightOffset),
1182 row virtualization + hit math, 400ms double-click, the scrollbar, and the
1183 cell layout (icon column, primary/secondary tints, char-estimate truncation,
1184 viewport-inset clip bounds). The in-List search box became a standalone
1185 BrowseState TextBox; the open/close shortcuts and SearchChanged plumbing
1186 move app-side (close returns the empty SearchChanged the legacy
1187 just_changed flag produced). Two fixes: the 6v sandwich again
1188 (render_widget emitted scrollbar + row overlays UNDER the rounded bg), and
1189 a NEW DISSOLUTION TRAP for the checklist — a dissolved widget no longer
1190 blocks window drags via its registered `blocks_backplate_drag`, so
1191 `is_movable_backplate_at` must veto its rect app-side; without it every row
1192 press became a compositor window-move grab and the app saw only the release
1193 (looked exactly like a dead click). Kept legacy: the view's
1194 scroll-into-view snaps the wheel back while the selected row would leave
1195 the viewport. Search typing not headlessly drivable — user spot-check
1196 pending.
1197 - **6aa — data-editor's SplitBox dissolved. DONE (live-verified: empty-state
1198 pixels identical modulo the cursor sprite; config load via the File menu,
1199 tree wheel, tree row select with the inline value editor, divider hover tint
1200 via crop diff).** The 6y `SplitPane` recipe on the scene-walk app: panes
1201 positioned directly from the pane rects and walked as separate roots, the
1202 divider quad emitted in the splitter's old walk slot. Removes the app's last
1203 raw-pointer child container and retires the Phase 2b
1204 `scene::bridge::layout_subtree` showcase that drove the split (the layout
1205 engine's app-facing debut now waits for the routed-events/scene-layout
1206 phase). TreeList intentionally NOT dissolved: at ~1.8k lines of tree
1207 expansion/inline-edit/annotation logic it is a self-contained walked widget
1208 (renders_own_subtree) whose internal ScrollBox never leaks — porting it
1209 app-side buys no hazard reduction; it converts to narrow traits with the
1210 `Element` deletion instead. Held divider drag spot-check PASSED (2026-07-13,
1211 ccectl injection): press at the divider grabbed it (app log: press 482 →
1212 release 332), split tracked the full 150px, and the grab stole keyboard
1213 focus from the raw editor exactly as designed.
1214 - **6ab — cce-text-editor on routed events + scene-solver layout: the FIRST app
1215 fully on the target architecture, end to end. DONE (live-verified: menu-open
1216 pixels match the pre-change capture at 0.13% = cursor sprite; menu item
1217 click through the routed release; editor click focus; the
1218 focused-border-after-outside-click oddity reproduced byte-identically on
1219 the stashed pre-change binary — pre-existing).** Layout: the frame is a
1220 plain `Arena<LayoutBox>` tree solved by `scene::layout::compute_layout` —
1221 no Element in the loop, the solver used directly by the app (stretched
1222 column [top bar fixed 42 / content grow padded 10 / status fixed 30], menu
1223 a fixed leaf, editor growing) — and it reproduces the legacy hand-math
1224 rects exactly, clamps included. Events: each handler builds one `Event` and
1225 routes it through `UiContext::propagate_event` per root; the router owns
1226 press hit-gating, Enter/Leave synthesis, drag-target recording, and
1227 KeyInput-to-focused delivery, leaving the app take_change plumbing and
1228 app-level shortcuts only. This is the shape the remaining widget-tree apps
1229 (data-editor foremost) migrate toward, and the pattern the demo
1230 (`cce-ui/src/main.rs`) should teach.
1231 - **6ac — data-editor on routed events + scene-solver layout; the
1232 routed-events/scene-layout item is COMPLETE. DONE (live-verified:
1233 empty-state pixels match 6aa at 0.06% (cursor + caret); config load through
1234 the routed menu; tree row select → inline choice editor + raw-span sync;
1235 choice popover open/select; tree wheel).** Layout: chrome + panes are one
1236 solver tree (stretched column [menubar fixed 42 + File-menu leaf / content
1237 row grow with pad 10, gap = divider width, panes growing by the SplitPane
1238 fractions / statusbar fixed 30]) reproducing the 6aa hand rects exactly;
1239 the SplitPane keeps divider input state, its frame derived from the solved
1240 panes; the inline value editors stay hand-positioned (they float over tree
1241 rows). Events: all 30 direct dispatch call sites route one `Event` through
1242 `propagate_event` per root with the plumbing intact. THE ROUTING TRAP worth
1243 remembering: the router delivers KeyInput to the ctx-focused widget FIRST
1244 on every propagate call, so a legacy non-short-circuited keyboard chain
1245 would deliver a typed key to the focused widget once per call site
1246 (N-time character insertion) — short-circuit the chain on first handled,
1247 and re-gate any plumbing that keyed off WHICH call returned true onto
1248 widget state instead (Enter→ApplyValue now checks the value editor was
1249 editing when the key arrived). Keyboard flows not headlessly drivable —
1250 user spot-check (typing, Enter-apply, tree search, keybind recording).
1251 - **6ad — the demo rewritten as `DemoApp`, the reference `Application`. DONE
1252 (live-verified: button click, toggle with app-state re-assert, slider wheel
1253 nudge, dropdown popover open/select with the occlusion clamp visibly
1254 working, all through routed dispatch).** `src/main.rs` had never been the
1255 "reference Application" the docs claimed — it was a 1925-line fossil
1256 predating the engine entirely: a raw Wayland client with its own
1257 CompositorHandler/SeatHandler impls, its own wgpu state, and hand-copied
1258 tessellators. Replaced by ~450 teaching-commented lines on the full target
1259 architecture: display-list frame + display_list_text, solver-driven layout
1260 (with `shrink` demonstrated for min-width rows), routed events with the
1261 KeyInput short-circuit rule and state-gated `drain_widget_changes`
1262 plumbing, ui_context-only popover registration with the in-frame draw, the
1263 dissolved-root window plate, and `drag_allowed_at` window dragging. API
1264 footgun surfaced for the log: `Slider::set_value` takes the NORMALIZED
1265 0..1 value (`with_range` only scales `get_scaled_value`) — passing a
1266 ranged value silently clamps to 1.0.
1267 - **6ae — legacy deletion, part 1: the global `widget::popovers` registry is
1268 DELETED. DONE (write-only since 6x; the mod, its `render_widget` write, and
1269 the four apps' `clear()` calls are gone; settings' popover renders
1270 byte-identically after).** Part 1 also produced a CORRECTED precondition
1271 map for the rest of the deletion — the endgame list had been assuming "the
1272 last app is across," and it is not:
1273 - The `view*`/`text_items` paths CANNOT be deleted yet: SIX apps still
1274 implement them — cce-test-interface (2.1k), cce-authenticator (0.9k),
1275 cce-display-manager (1.5k), cce-mail (2.5k), cce-layout-interface
1276 (3.8k), cce-status-interface (4.2k). Each needs its own Phase-6-style
1277 migration (display-list flip at minimum; dissolutions as found).
1278 Suggested order: smallest/least-critical first (test-interface,
1279 authenticator — NOTE it may be the lock screen, verify carefully),
1280 status-interface last (layer-shell, always-running).
1281 - The per-widget text getters are additionally load-bearing for the walk's
1282 legacy branches (`renders_own_subtree`, container `text_labels`
1283 aggregation) and the migrated apps' hand-rolled window aggregates
1284 (settings' `collect_window_child`, files' assembly) — they go when those
1285 consumers move to `paint_self`-only trees.
1286 - `Element` + `Adapted` go last, after both of the above; TreeList
1287 converts to narrow traits then.
1288 - **6af — cce-test-interface across (1 of 6). DONE (A/B: zero >8%-amplitude
1289 pixel diffs; live-verified — full gallery render, page-dropdown popover
1290 in-frame, page switch updating the MenuBar title and the status prim).**
1291 The recipe for the remaining five: move the `view()` +
1292 `view_rounded_quads()` bodies into `display_list()` in the engine wrapper's
1293 order (ROUNDED first, then plain, then popover rects — the wrapper reversed
1294 the intuitive order and apps' visuals bake it in), and re-emit the
1295 `rebuild_text_items` assembly as `Prim::Text` built fresh per frame,
1296 deleting the cache + its invalidation call sites + the app FontSystem +
1297 any `text_areas` override (its extra areas become prims).
1298 `custom_vertices` stays. Remaining queue: authenticator (verify carefully —
1299 lock screen), display-manager, email, layout-interface, status-interface.
1300 - **6ag — cce-authenticator across (2 of 6); the flip FIXED runtime-invisible
1301 text. DONE (live-verified --standalone + CCE_AUTH_SIMULATE: full dialog text
1302 renders, zero font-ID warnings — was hundreds per frame — fingerprint-scan
1303 click drives the animated glow + hint).** It is an xdg-toplevel polkit auth
1304 dialog (NOT a session lock — safe to run; needs `--standalone` +
1305 `CCE_AUTH_SIMULATE=1` to show a window without a live polkit request, and it
1306 auto-exits ~3s in simulate mode so capture fast). The single `view()` (both
1307 geometry and text) → `display_list()`; the `text_items` assembly → prims;
1308 app FontSystem / make_text_buffer / text_items field+getter deleted. The
1309 6e face-ID class again, and worse here — the app used
1310 `create_font_system_with_system_fonts()`, so EVERY label was invisible at
1311 runtime; the migration is the fix. General lesson reinforced: any
1312 legacy-path app with its own FontSystem is a latent-invisible-text
1313 candidate — don't trust its baseline capture.
1314 - **6ah — cce-display-manager across (3 of 6). DONE (A/B: 131px AE = 0.004%
1315 caret blink, zero >8%-amplitude diffs; live-verified --greeter renders
1316 identically).** The greetd login greeter — run the GUI standalone with
1317 `--greeter` (daemon mode needs root/greetd). Bundled fonts, byte-match flip.
1318 view() + view_rounded_quads() → display_list() (rounded then plain, both
1319 skipping the card); rebuild_text_items → prims; FontSystem / make_text_buffer
1320 / info_buffer / text_items machinery deleted. The card — a soft radial-glow
1321 blob drawn with the circular clip disabled — STAYS in custom_vertices
1322 (escape-hatch layer, on top, untouched). REPO HAZARD hit here: these crates
1323 live under ~/Dropbox, and a Dropbox sync reverted the edited main.rs to disk
1324 AFTER build+test but BEFORE the commit landed (a harness-interrupted commit,
1325 exit 144, left the tree clean at the old file) — had to re-apply and commit
1326 immediately. Verify `git log`/`grep display_list` actually stuck before
1327 moving on.
1328 - **6ai — cce-mail across (4 of 6); the flip FIXED invisible list/detail text.
1329 DONE (live-verified: inbox list of 3 emails, detail placeholder, and on click
1330 the full detail view — subject/From/To/Date/body + Reply/Delete/Mark-Unread
1331 toolbar).** view() body (all plain quads) → display_list() via a small
1332 `__EmailQuadSink` shim mapping the ported quads.push/extend to PaintCtx::quad;
1333 rebuild_text_items → emit_text_prims(&mut pc). The 6e face-ID class AGAIN
1334 (create_font_system_with_system_fonts): the list rows, detail metadata/body,
1335 and placeholder were all invisible — only the paginator tabs showed. Fix:
1336 switch the app FontSystem to bundled create_font_system() (KEPT for the
1337 TextBoxes' prepare_text measurement — now matching the engine render FS) and
1338 render via prims. Note the AE-vs-baseline metric is misleading for these
1339 invisible-text fixes (tiny % of dark-on-dark pixels change) — judge by
1340 whether text APPEARS, not by AE.
1341 - **6aj — cce-layout-interface across (5 of 6); needed + consumes the new
1342 boxed-text prim; the flip FIXED invisible text. DONE (live-verified: the
1343 whole properties/geometry/alignment/add-elements UI renders where the
1344 baseline showed nothing; 104→0 font-ID warnings).** This app forced the
1345 boxed-text feature (previous commit): its canvas Element::Text boxes need
1346 word-wrap + h/v alignment, unrepresentable as a plain Text prim. view() +
1347 view_vectors() → display_list() (a __LayoutQuadSink shim for the
1348 quads.push/extend body; vectors → PaintCtx::vector); rebuild_text_items →
1349 text-prim tuples carrying an optional TextLayout, emitted via text_with /
1350 text_boxed. Same 6e face-ID class (create_font_system_with_system_fonts);
1351 fixed by bundled FS + prims. VERIFY GAP: the canvas boxed prim itself
1352 (wrap/align on the page) could not be pinned headlessly — a placed text box
1353 defaults to page x=40, behind the ~540px properties panel, and there is no
1354 virtual keyboard to type a clear coordinate; it renders via the identical
1355 dl_text_items path as the confirmed-visible text. User spot-check: place +
1356 drag a text box onto open page, confirm wrap + alignment.
1357 - **6ak — cce-status-interface across (6 of 6, LAST legacy-path app). DONE
1358 (live-verified: an isolated `--module clock` instance renders "Friday, July
1359 10, 2026 … PM" with its rounded background pill through the display list).**
1360 The persistent layer-shell bar. view() + view_rounded_quads() bodies move
1361 into display_list() (rounded boxes, then status-bar bg / module rects /
1362 separators as prims, in the wrapper's ROUNDED-then-plain order); module text
1363 becomes fresh Prim::Text each frame. overlay_quads() stays a separate on-top
1364 pass (tray hover highlights over text). The status bar's OWN text is never
1365 set in this app (get_text_items was a no-op), so its text_items()/custom
1366 text_areas() overrides are deleted; self.font_system is kept only for the
1367 modules' measure-then-position shaping. Mechanism: modules build a
1368 StyledLabel to measure width, then emit via the new StyledLabel::into_prim
1369 (cce-ui 005a53f) through a draw_label helper — the vertical bar's centered
1370 per-char text rides the boxed-text TextLayout. Bundled create_font_system(),
1371 so NO 6e invisible-text hazard — a byte-match flip. Full multi-module A/B was
1372 avoided (the no-arg binary is a launcher daemon that would spawn a bar
1373 conflicting with the user's live one); a single isolated module segment was
1374 the test surface.
1375 - **All six legacy-path apps are now across.** The `view*`/`text_items`
1376 Application-trait deletion is unblocked.
1377 - **6al — legacy `view*`/`text_items`/`text_areas` DELETED. DONE (16 client
1378 apps compile; 180 tests pass; settings live-verified).** With every app on
1379 `display_list()`, the legacy geometry/text trait surface was dead code:
1380 removed `view` / `view_rounded_quads` / `view_vectors` / `text_items` and the
1381 default `text_areas` mapping from the `Application` trait; in `render()`,
1382 dropped the `quads`/`rounded_quads`/`vectors` collection + the tuple-wrapping
1383 `None =>` branch (so `dl = display_list().unwrap_or_else(empty)`) and the
1384 `text_areas()` call (an empty `areas` vec the dl-text loop fills). KEPT:
1385 `overlay_quads` (status-bar tray hover), `custom_vertices` (display-manager
1386 card, test-interface gallery), `display_list` / `display_list_text`. Pure
1387 dead-path removal — every implementor already took the `Some(dl)`/empty-text
1388 arms. (cce-designer + cce-cloud drive `WgpuAdapter` directly, never implement
1389 `Application`, so they're untouched.)
1390 - **6am — dead widgets SplitBox / MultiControl / KeybindsControl DELETED.
1391 DONE (cce-ui + all 18 apps compile; 175 tests pass; settings A/B AE=0).**
1392 Phase-6 dissolutions orphaned all three — no client app (nor live cce-ui
1393 path) constructs them. Removed the files, re-exports, the two
1394 `get_*_sub_widget_info` helpers + their `render_widget`/`window_runner`
1395 downcast blocks (fire only when the widget IS that type — none is, so
1396 behavior-preserving), the dead `name == "MultiControl"/"KeybindsControl"`
1397 span-full terms, and `render_widget`'s now-redundant `let mut corners = …`
1398 shadows. Each carried the full legacy text-getter aggregation, so this trims
1399 a big slice of the getter consumer graph.
1400 - **Per-widget text getters — GATED, not yet deletable.** Investigation
1401 (6am) established that every getter still has a LIVE consumer, so none can
1402 be removed until those move to `paint_self`/prims first:
1403 - `get_text_items` → **cce-designer** (custom `WgpuAdapter` render loop in
1404 `render.rs`: text-buffer cache + curved-menu-text special cases). Designer
1405 + cce-cloud never implement `Application` — they were skipped by all of
1406 Phase 6 and still drive `WgpuAdapter` directly.
1407 - ~~`text_labels_with_font_and_bounds` + `widget_font` → `layout::render_widget`~~
1408 **DONE (6an).** render_widget now sources its text from the scene walk
1409 (`paint_root_into` → keep only `Prim::Text` → emit onto the RenderTarget),
1410 keeping its own geometry path; the prim carries the per-widget font+clip so
1411 the getters are gone from here. Settings A/B AE=0 on Accounts + (stash-based)
1412 the spinbox-heavy Audio page. The last difference vs the getter is
1413 widget_font→text_font, which coincides except for a custom-font TextBox.
1414 - ~~`.text_labels()` / `.text_labels_with_bounds()` → **four hand-aggregate
1415 apps** (email, authenticator, display-manager, layout-interface) whose
1416 `display_list()` emits widget text by calling the getter per widget.~~
1417 **DONE (6ao)** — see below.
1418 - Note: the "orphaned" containers Layer / Page are NOT deletable — Layer is
1419 the embedded base of the live Plate/Page; Page is embedded by the live
1420 Paginator (transitive liveness through inheritance, not direct app use).
1421 - **6an — render_widget off the getters (see above). DONE (all 18 apps
1422 compile; 176 tests; settings A/B AE=0 on Accounts + Audio).** One of the
1423 three getter-consumer classes cleared.
1424 - **6ao — the four hand-aggregate apps off the getters. DONE (176 tests;
1425 all four A/B-verified live).** New `scene::painter::append_widget_text(ui,
1426 &dyn Element, &mut PaintCtx)`: walks the subtree and appends only its
1427 `Prim::Text` items — per-widget content font, walk clip composed into prim
1428 bounds (the 6an recipe as a reusable helper). Every per-widget
1429 `.text_labels()`/`.text_labels_with_bounds()` call in email, authenticator,
1430 display-manager and layout-interface replaced with it. A/B results:
1431 email + layout-interface byte-identical; authenticator + display-manager
1432 identical except widget-owned labels now render in the widgets' configured
1433 control font (legacy aggregates dropped the font to `None` — the same
1434 widget_font→text_font delta as 6an, here visible because these apps
1435 configure a monospace control font).
1436 - **Display-manager UAF found + fixed:** its `new()` linked the ui_context
1437 tree and captured `focused_widget` while the State was a stack local, so
1438 every registered pointer dangled after the move — the walk's child
1439 descent was the first render-path consumer to dereference them (abort on
1440 a garbage-length alloc); `propagate_event` and the `all_*` child
1441 aggregation read the same stale pointers all along. Fix: per-frame
1442 idempotent `relink_tree()` (register/link are id-keyed) + initial focus
1443 re-derived from the boxes' own focus flags. Also: its flat `widgets_iter`
1444 lists the card AND the card's children, so text moved to walking the TRUE
1445 roots (bg + root_container) — flat would double-emit — and `LoginCard`
1446 (a container with own, non-aggregating labels) got the Plate-style
1447 `paint_self` override for its two header labels.
1448 - Lesson for the getter deletion: an app whose tree is linked from `new()`
1449 by value is a dangling-registry candidate — audit any remaining
1450 `link_parent_child` calls made before the owning struct reaches its
1451 final address.
1452 - **6ap — cce-designer's render loop off the getters. DONE (default +
1453 circular-pane A/B pixel-identical; add_node label renders via the new
1454 path; full workspace builds).** The custom `WgpuAdapter` loop's two text
1455 sources (`get_text_items` widget-buffer fast path +
1456 `text_labels_with_font_and_bounds` fallback) became one walk: per
1457 non-menubar widget, `append_widget_text` → text prims, shaped app-side in
1458 `text_buffer_cache` with the same size*1.4 metrics the fallback always
1459 used. Per-widget special cases (plate-ancestor bounds, circular cull,
1460 network opacity, curved-ring feed) operate on prim fields unchanged. The
1461 curved-ring branch is unreachable today (menubars are skipped before its
1462 condition) — preserved verbatim, flagged for a future dead-code decision.
1463 **ALL app-side getter consumers are now gone.**
1464 - **6aq — walk getter-use consolidated to ONE fonted default; the three
1465 missed app consumers cleared. DONE (176 tests; settings audio AE=0;
1466 test-interface AE=5 cursor-level; data-editor loaded-tree AE=0; cloud
1467 fuzzel+json standalone AE=0; designer unchanged).** CORRECTION to 6ap's
1468 "all app-side consumers gone": three call sites had escaped the audit —
1469 settings' `renderer.rs` `collect_window_child` (outside render_widget),
1470 test-interface's gallery loop, and cce-cloud's `jl`/`fuzzel` labels (cloud
1471 drives WgpuAdapter directly, like pre-6ap designer, and json_layout was
1472 live only through it). All three now use walk-derived text. Engine side:
1473 the `renders_own_subtree` walk branch is just `paint_self` (TreeList +
1474 newly-flagged JsonLayout carry subtree-emitting overrides; descending
1475 JsonLayout would draw inactive pages and miss its checkbox side-labels);
1476 the default `paint_self` leaf drain moved from `text_labels()` to the
1477 FONTED getter — same labels every legacy tuple consumer served. Ramp got
1478 the own-labels `paint_self` (the Plate/LoginCard class: container own
1479 text vs the walk's aggregate rule). Traps recorded: a widget with a
1480 ui-tree parent must NOT also be walked as a top-level root (test-
1481 interface's page selector under the status bar double-drew ~10%
1482 brighter); cce-cloud launches reach the user's DAEMON via
1483 /run/user/UID/cce-cloud.socket — hold the socket aside to A/B a local
1484 standalone build.
1485 - **6ar — the per-widget text getters are DELETED from `Element`. DONE
1486 (176 tests; full workspace builds; nine apps A/B-verified — AE=0 or
1487 cursor/translucency/status-race noise only).** `text_labels` /
1488 `text_labels_with_bounds` / `text_labels_with_font_and_bounds` /
1489 `get_text_items` are gone from the trait, with Adapted's impls, the
1490 `Paint::text_items` hook, and every container aggregate (Backplate,
1491 Layer, Page, Plate, SectionContainer, ColorRamp, ControlPanel,
1492 JsonLayout, ButtonStrip). Every widget reaches the frame through
1493 `paint_self`. The deleted defaults survive as painter helpers with the
1494 labels passed in (`paint_legacy_leaf`, `fonted_leaf_labels`,
1495 `scroll_ancestor_text_bounds`, `base_control_label`); legacy leaves keep
1496 their label logic as inherent `own_labels()`; TreeList reads its concrete
1497 Adapted children via the now-pub(crate) `own_labels_with_font_and_bounds`;
1498 ControlPanel/ParametersBg/JsonLayout source dyn-children labels off the
1499 paint walk (ControlPanel re-applies its scroll shift + viewport clamp;
1500 List and ControlPanel are `renders_own_subtree` — walking into a legacy
1501 scroll frame desyncs text from geometry at scroll ≠ 0). The concrete
1502 `context_menu::text_labels()` global stays (inherent method, not the
1503 trait). ~125-method god-trait is now 4 methods lighter and text has ONE
1504 path: prims.
1505 - **6as (teardown, in progress).** Landed: (1) census of all 117 `Element`
1506 methods vs workspace-wide call sites — five were call-less and are
1507 DELETED (`Element::paint` — the hover-registration default nothing
1508 invoked — `as_geom_controller`/`as_spreadsheet_controller` & variants,
1509 `color_u8`, `is_layer`); (2) the seven tree context-menu actions
1510 (`copy_key`/`copy_value`/`delete_key`/`expand_node`/`collapse_node`/
1511 `expand_all_nodes`/`collapse_all_nodes`) are now transitional `Input`
1512 capability hooks with `Adapted` forwards (the 5k pattern), so the global
1513 context menu's `dyn Element` dispatch survives the TreeList conversion.
1514 - **TreeList → `Adapted<TreeList>` — DONE (the staged plan below executed
1515 verbatim; 176 tests; data-editor loaded-tree A/B AE=0 byte-identical incl.
1516 the focus wash; row select / context-menu Copy-Key-to-clipboard / search
1517 click-to-focus verified live).** Two new transitional hooks landed with
1518 it: `Input::tick_ctx` (EventCtx-carrying tick — the rename-commit focus
1519 re-target needs the routing ctx) and `Paint::paints_own_subtree` →
1520 `Element::renders_own_subtree` (the field widgets stay ctx-linked for
1521 event propagation, but the walk must not also descend — descending
1522 double-painted them and drew the CLOSED add-key popover box). The 5q
1523 `legacy_focus_highlight` trap struck again (the focused tree's teal wash).
1524 KNOWN-LATENT (pre-existing since 6ac, verified identical in the
1525 pre-conversion baseline): wheel-over-tree doesn't scroll and the Add-Key
1526 button doesn't open its popover — children-first propagation
1527 short-circuits on the hit child before the tree's own toggle/scroll
1528 logic runs. Fix belongs to the event-routing follow-up, not the widget.
1529 Original staged plan (executed):
1530 - `Layout`: `rect_assigned` caches the rect; the `set_rect` body
1531 (search box / add-key button / popover box / scroll box arrangement +
1532 `update_bounds`) moves to the assignment hook. No container children —
1533 the walk treats the adapter as a leaf, so `renders_own_subtree`
1534 becomes unnecessary.
1535 - `Paint`: `color`/`rounded_corners`/`corner_radius`/`solid_border`/
1536 `widget_font` port straight; geometry aggregates become ctx-less
1537 (TreeList's children are FIELDS — its `children(_ctx)` ignores the
1538 ctx already); subtree text rides the `serves_legacy_labels` hatch
1539 (`legacy_labels_with_font_and_bounds(rect, ctx)` = today's
1540 `subtree_fonted_labels`); `prepare_text`; popover via
1541 `Paint::popover`/`draw_popover`.
1542 - `Input`: the mouse/cursor/wheel/keyboard/tick bodies move into
1543 `on_event` arms with `ectx.ui` (the 5s ParametersBg pattern);
1544 drag via the Input drag hooks; `blocks_backplate_drag`; the seven
1545 tree hooks return their inherent bodies; focus semantics —
1546 `ctx.set_focused(self)` sites become `ectx.request_focus()` (the
1547 ADAPTER's pointer, not the inner). CAUTION: `mouse_input` registers
1548 the inline `edit_box` into the ctx TREE (`register_widget` +
1549 `link_ids(self_id, …)` + `set_parent`) — under the adapter, `self_id`
1550 must be the adapter's base id (`EventCtx::widget_addr` precedent).
1551 - Sweep: data-editor field → `Adapted<TreeList>` (Deref covers the
1552 concrete calls: `scroll_box.scroll_y`, `set_flat_keys`, `take_*`
1553 drains, `get_row_rect`, `select_and_show_key`, `focus_search`), one
1554 raw `*mut TreeList` cast → `as_ptr_mut()`. A/B: loaded tree, row
1555 click + inline rename (double-click), context menu Copy Key via
1556 wl-paste, search focus, add-key popover, wheel.
1557 - **Leaf sweep COMPLETE — every widget is on the narrow traits (6as).**
1558 Trackpad, KeybindRecorder, FontSelector, ColorSelector, Ramp, ColorRamp
1559 all converted (each A/B'd: gallery/DE/LI diffs = cursors, launch-phase
1560 animation, or AE=0). Notables: ParametersBg's typed color rows re-typed
1561 to `Adapted<ColorSelector>`; ColorSelector's in-file keyboard tests pass
1562 THROUGH the adapter; Ramp/ColorRamp take the TreeList shape
1563 (paints_own_subtree + tick_ctx + per-tick re-parenting of field widgets
1564 so their label fade blends against the adapter's color); and
1565 `Adapted::paint_self` gained a subtree TEXT PASS-THROUGH — a
1566 paints_own_subtree widget's Text prims forward verbatim with per-child
1567 fonts/bounds instead of being flattened to widget_font by the
1568 own-labels re-derivation (composites with mixed child fonts rendered in
1569 the default serif without it). `impl Element` now remains ONLY on: the
1570 containers (Layer/Container/Page/Plate/Backplate/ScrollBox/ScrollBar/
1571 List/ControlPanel/SectionContainer/JsonLayout/Menu/MenuBar-internals),
1572 app-local widgets, ContextMenu, and `Adapted` itself.
1573 - **Then: retire the `as_*_controller` pairs — SCOPE CORRECTED.** The
1574 earlier four-site estimate came from an over-filtered grep; the real
1575 surface is ~45 sites: cce-designer's HTTP-action/menu plumbing holds
1576 ~16 (including generic roster queries like "does ANY widget have an
1577 open menu" via `as_menu_controller()` over `Box<dyn Element>`), and
1578 `Switcher` implements MenuController by forwarding to its ACTIVE PANE
1579 through `as_menu_controller_mut()` on `dyn Element` — a live
1580 capability-dispatch system, not vestigial casts. Retirement needs a
1581 design decision first: either a standalone capability registry
1582 (`&dyn XController` handles registered beside the tree) or deferral to
1583 the Element deletion itself, where the designer's roster becomes
1584 concretely typed. Do NOT sweep it mechanically.
1585 - **Controller-capability decision RESOLVED (6aw): option 2 — defer to
1586 concrete typing; no registry.** A registry would be permanent
1587 infrastructure preserving the anonymous-widget pattern the rebuild
1588 exists to kill. Instead the queries die when their callers get
1589 retyped. First payoff immediately: `Switcher` — the largest holder
1590 (17 sites) — turned out to have ZERO constructors workspace-wide
1591 (settings dissolved its switcher in 6u) and is DELETED. Remaining
1592 map: cce-designer's roster retype (~16 sites, the bulk), the
1593 test-interface roster (3), one production site in
1594 `display/serialize.rs` (`serialize_widgets`' menu-state dump), and
1595 in-file tests that assert capabilities ride the adapter (die with
1596 `Element`). The designer retype is therefore the finale's next
1597 structural step, folded into the tree-machinery retype.
1598 - **Capability system DELETED (6aw, same session).** The designer's
1599 accessor block reaches each controller trait by `as_any` downcast to
1600 the roster index's known concrete type (note: `Adapted::as_any`
1601 exposes the INNER widget, so the downcast targets `MenuBar`/`Graph`/
1602 `ParametersBg`/`Spreadsheet`/`Breadcrumb` directly — the first build
1603 against `Adapted<W>` panicked at launch, caught by the live A/B);
1604 test-interface downcasts its Paginator; `serialize_widgets` tries
1605 the two MenuController implementors a roster can hold. With no
1606 callers left, Element's 12 `as_*` hooks, Adapted's forwards, and the
1607 14 `Input` capability hooks + their per-widget impls are all
1608 deleted. Element: 92 methods. A/B: designer/TI static diffs are the
1609 known noise shapes; designer HTTP `menu_click` (the dynamic-index
1610 path) verified live.
1611 - **Gallery containers went app-local (6as).** cce-test-interface's
1612 widget gallery was the last constructor of ControlPanel / Plate /
1613 SectionContainer / Backplate: it now owns `ti_widgets.rs` — a verbatim
1614 ControlPanel copy plus passive Plate/SectionContainer/Backplate
1615 lookalikes (each reproducing EVERY Element getter the render/event
1616 paths read: plate color/opacity/blur alpha-negation/corner radius/
1617 border/label_offset bg shift, the childless section-header row, the
1618 backplate bevel). A/B: launch-phase progress-bar animation only.
1619 - **CONTAINER TYPES DELETED (6as).** `Backplate`, `Plate`, `List`,
1620 `ControlPanel`, `SectionContainer` (+`SectionHeader`) removed from
1621 cce-ui outright — five files, the re-exports, the `List` branches in
1622 the scroll-ancestor text-bounds walks, and the then-dead
1623 `own_labels_with_font_and_bounds`. `ColumnWidth`/`ListColumn` moved to
1624 their only consumer (cce-files `row_list.rs`). TreeList's drag tests
1625 re-anchored on `drag_allowed_at`. 171 tests (5 died with their files);
1626 settings/DE A/Bs cursor-only / AE=0.
1627 - **Finale opened: dead-flag constant-fold sweep (6at).** With the
1628 plates gone, census round 2 found no zero-CALL methods but seven
1629 zero-OVERRIDE ones (only the trait default exists ⇒ they are
1630 constants). Folded and deleted: `capture_event` (the capture-phase
1631 branch in `propagate_event_impl` was unreachable), `is_active` (folded
1632 into the `highlight_color` default), `is_plate` (designer render.rs's
1633 whole `parent_plate_rect` text-clamp machinery was dead),
1634 `is_backplate` + `is_movable_backplate` (folded the drag walks —
1635 `UiContext::is_movable_backplate_at` had become constant-false and is
1636 DELETED; the `Application` default now just returns false — plus the
1637 backplate-parent theming/corner branches in MenuBar, StatusBar, and
1638 Dropdown's `backplate_ancestor` walk; MenuBar/StatusBar `corner_style`
1639 still reports the parent radius for children that read it through the
1640 parent pointer, but corners never round). `corner_radii`/`mark_dirty`
1641 also have zero overrides but carry real derived logic — they die with
1642 the retype, not by folding. Element: 112 → 107 methods. A/B: files
1643 AE=0, settings audio AE=0, designer diff = terminal behind the
1644 translucent window.
1645 - **Layer + Page DELETED (6au).** Per the 5r survey, no app ever put
1646 content in Paginator's pages — every consumer keys its own content
1647 on `selected_page()` — so the `Vec<Page>` was empty containers being
1648 arranged/registered/toggled/event-proxied for nothing. Their one
1649 visual (the page-area bg quad, page_color × page_opacity) moved into
1650 `Paint::paint`. With the stack gone Page had no constructor and
1651 Layer's only constructor was Page's base: both files deleted, plus
1652 `Element::is_page`, ScrollBar's Page-downcast write-back, and the
1653 dead 8/11 of `PageSelector` (now just selected_page /
1654 set_selected_page / sidebar_w; MenuBar's impls + `page_hidden` field
1655 went with it). A/B: email/LI/files AE=0; live LI tab click switches
1656 pages correctly. Census round 3 then found the deletion stranded two
1657 more zero-override methods — `check_out_of_bounds` and
1658 `transform_event_for_child` (Page was the only override of each) —
1659 folded and deleted in the follow-up. Element: 104 methods.
1660 - **ScrollBar DELETED, ScrollBox demoted off `Element` (6av).**
1661 ScrollBar's only consumer was cce-system-settings' page scrollbar
1662 (the dissolved Page subtree's survivor, evented through
1663 `propagate_event` and painted through `collect_window_child`) — the
1664 file moved there verbatim and the cce-ui type is gone. ScrollBox is
1665 never ctx-registered by either consumer (TreeList + the
1666 test-interface panel copy call it concretely), so its `Element`
1667 impl was dyn-dispatch ballast: now a plain struct whose former
1668 Element entry points survive as inherent methods with
1669 default-derived parity (the scrollbar-click focus claim became
1670 `focus::clear_focus()` — unfocusing the previous holder was its
1671 only observable effect). The painter/model scroll-ancestor text
1672 clamps folded to `None` (no tree parent can be a ScrollBox; none
1673 ever was at runtime). A/B: data-editor AE=0 plus live wheel +
1674 track-jump-scroll on a 100-key tree; settings diff = process-row
1675 churn; TI sub-threshold. Census round 4: only `corner_radii` +
1676 `mark_dirty` remain zero-override (real derived logic — they die
1677 with the retype, not by folding).
1678 - **Dead raw-widget sweep (6ax).** The raw-`Element` census after the
1679 capability deletion found four more zero-constructor widgets:
1680 Header, VBox, HBox (export-only) and Svg — which also rode Button
1681 as an `Option<Svg>` payload no caller ever set, so Button's icon
1682 branches were statically dead and went too. All deleted.
1683 - **Container DELETED (6ax part 2).** Settings' system-info
1684 `actions_row` was a dead field; dm's `root_container` was a
1685 transparent origin-anchored fan-out — dissolved into direct
1686 dispatch/walk roots (session list first for events, matching the
1687 reversed child order; card first for text, matching `children()`
1688 order). dm A/B: background-animation phase only (B-vs-B control
1689 differs full-frame), click behavior identical to baseline, 3/3
1690 interaction rounds alive on both builds. One UNREPRODUCED
1691 bogus-alloc seen once on the new build — dm's known latent
1692 stale-pointer signature, 0/3 repro on either binary; watch it.
1693 - **JsonLayoutWidget moved into cce-cloud (6ay).** The KDL/JSON
1694 launcher-layout host had one consumer; it cannot be demoted off
1695 `Element` (cloud feeds it to the paint walk as `&dyn Element`), so
1696 the file moved app-side verbatim — the impl dies with the machinery
1697 retype. `Justification` stayed in cce-ui (Button/files/settings
1698 share it), same path. A/B: content-identical renders (raw diff =
1699 the overlay's run-to-run spawn position + wallpaper bleed through
1700 the translucent plate); live checkbox click toggles.
1701 - **Canvas + Viewport3D moved into cce-designer (6ay part 2).** Both
1702 designer-only (preview.rs's "canvas" is its own internal type).
1703 A/B: AE=0. cce-ui's raw `impl Element` surface is now exactly ONE
1704 production type: ButtonStrip, the ctx-registered embed of
1705 MenuBar/Paginator — load-bearing in the tree, pinned to the
1706 machinery retype.
1707 - **App-local impls onto the narrow traits (6az) — COMPLETE:
1708 `Element` has exactly ONE production implementor (`Adapted<W>`).**
1709 Each remaining raw `impl Element` converted to
1710 `Layout`/`Paint`/`Input` + `Adapted<W>` ahead of the machinery
1711 retype; only test mocks still implement the trait directly.
1712 Done: cce-colors' ColorSlider (constructor returns the wrapper, so
1713 construction and direct-dispatch sites are untouched; A/B AE=0,
1714 click/wheel live-verified); settings' ScrollBar (now plain data —
1715 its raw-pointer parent/children fields, Drop, and unsafe Send/Sync
1716 had zero consumers; strip A/B AE=0 and cross-build byte-identical
1717 after an identical wheel + track-click sequence); cloud's Fuzzel
1718 (set_rect side effects → rect_assigned; overlay A/B identical, row
1719 click moves the selection live) and JsonLayoutWidget (the TreeList
1720 shape — paints_own_subtree + whole-subtree routing in on_event with
1721 gates_presses off + tick_ctx; the old overrides survive verbatim as
1722 inherent methods paint composes with a dummy ctx; render sites'
1723 all_quads/all_rounded_quads calls now resolve to the adapter's
1724 reverse bridges, same tuples; checkbox toggle live-verified) —
1725 cce-cloud is raw-impl-free. Designer's four (PassivePlate, Canvas,
1726 NodePalette, Viewport3D) followed — constructors return the wrapper
1727 so the roster's Box pushes and as_any downcasts stand; PassivePlate's
1728 full getter surface rides Paint, Viewport3D's wheel inertia moves to
1729 on_event/tick; static A/B = the status sliver only, cross-build
1730 captures after an identical HTTP circular-toggle + viewport wheel
1731 structurally identical — cce-designer is raw-impl-free. TI's four
1732 followed (part 6): ControlPanel keeps its aggregate overrides as
1733 inherent methods composed by a paints_own_subtree paint, its child
1734 arrangement in Layout::arrange_children (adapter as parent), and its
1735 routing in on_event; the raw `as *mut ControlPanel` casts became
1736 as_any downcasts; inline_label keeps the adapter's label machinery
1737 out of all four. Gallery + child-window A/Bs pixel-parity, cross-
1738 build sequences identical — cce-test-interface is raw-impl-free.
1739 The greeter's three followed (part 7: labels fold into paint off the
1740 laid-out rect; the display list's card-skip moves from base-pointer
1741 equality to id comparison; cursor-only diffs, 3/3 click rounds alive
1742 on both builds, after-state byte-identical). ButtonStrip closed the
1743 sweep (part 8): embedders hold `Adapted<ButtonStrip>`; two wrapper-
1744 shadowing collisions (`set_selected`, `take_click` — Element's bool
1745 signatures vs the model's `Option<usize>` ones) route through
1746 `inner_mut()`; presses stay ungated so a tab press lands under an
1747 open dropdown popover; LI's Paginator strip and email's MenuBar
1748 strip both cross-build byte-identical after identical clicks.
1749 - **Compiler census of the dyn surface (6ba, experiment reverted —
1750 the finding is the deliverable).** Hypothesis: with one implementor,
1751 many `Element` methods are only ever called on concrete
1752 `Adapted<W>` receivers and could move off the trait before the
1753 retype. Method: delete a method from the trait, keep it inherent on
1754 the wrapper (same signature — concrete sites resolve unchanged),
1755 `cargo check --workspace`; every error is a true dynamic-dispatch
1756 site. Verdict: **~91 of 92 methods fail — the trait IS the
1757 machinery surface; there is nothing to slim first.** Three consumer
1758 classes pin it: (1) cce-ui machinery (context.rs routing/focus/
1759 drag, the paint walk, layout.rs render paths — `T: Element`
1760 generics count: measure/preferred_height/set_row_rect live there —
1761 and core.rs context-menu actions on `dyn` targets); (2) container
1762 child aggregation over raw child pointers (menu/paginator/treelist
1763 in-tree, ControlPanel/JsonLayout app-side) touching the full
1764 paint+input getter surface; (3) the designer/test-interface roster
1765 broadcast loops (`Vec<Box<dyn Element>>`) calling nearly everything.
1766 NOTE: a `--workspace` check that fails in cce-ui never reaches the
1767 app crates — the first pass under-reported; the app rosters were
1768 where 4 of 5 "clean" candidates actually failed.
1769 - **The designer roster is concretely typed (6bb).** First retype
1770 slice, in census order: `Vec<Box<dyn Element>>` → one
1771 `Box<WidgetSlots>` of 17 named concrete fields (boxed whole so
1772 registered pointers stay stable while `State` moves). Const-indexed
1773 sites reach fields directly; the genuinely index-driven paths
1774 (draw order, focus cycling, broadcast dispatch, the `*_IDX`-keyed
1775 HTTP API) go through `get_dyn`/`get_dyn_mut`; serialize takes a
1776 per-slot `dyn_refs` view (`serialize_widgets` now takes
1777 `&[&dyn Element]`). Static A/B byte-identical; cross-build final
1778 after an identical menu/toggle/wheel/click sequence within 6 px.
1779 TI's two-mode gallery followed (part 2): 53 named concrete slots
1780 for the gallery, typed enums for the child window's runtime-variant
1781 slots, a `Roster` enum carrying whichever mode runs; the numeric
1782 indexes keep addressing slots through `get_dyn`. **No app stores
1783 widgets behind `Box<dyn Element>` anywhere — the roster phase is
1784 complete.** Cross-build A/Bs: gallery empty masks static and after
1785 an identical interaction sequence; Ramp child at noise level;
1786 non-Ramp child arms hit the pre-existing unconditional
1787 Ramp-downcast panic identically on both builds.
1788 Cloud's `JsonControl` closed the owned-storage class (part 3): the
1789 JSON-config controls become a typed enum over `Adapted<T>` replacing
1790 `JsonWidget`'s `Box<dyn Element>`; the label walk dropped its
1791 `as_ptr` round-trip unsafe for a plain reborrow. **No owned
1792 type-erased widget storage remains anywhere in the workspace.**
1793 - **The borrowed-pointer retype design (6bc, decided 2026-07-12).**
1794 The replacement handle is **`WidgetId`, resolved through the
1795 generational `WidgetTree` at every use**. Rationale: apps own
1796 widgets concretely (the 6bb rosters) and re-register pointers
1797 idempotently per frame from boxed storage, so the registry is the
1798 one place a raw pointer is refreshed before use; every *other*
1799 stored `*mut dyn Element` bypasses that guard and is exactly where
1800 the real UAFs happened (display-manager 6ao — dangling registry
1801 from a by-value `new()`; settings 6w — `FOCUSED_WIDGET` surviving a
1802 rebuild via same-size alloc reuse). Under the retype, raw pointers
1803 remain only (a) as the `WidgetTree` registry payload and (b) as
1804 transient same-frame values inside resolution helpers; every stored
1805 reference becomes a `WidgetId`, and a stale id resolves to `None`
1806 and is skipped — the UAF class becomes unrepresentable outside the
1807 registry itself. Public signatures taking `&dyn Element` stay
1808 (deriving the id from `base()` internally) so most call sites
1809 survive verbatim; direct field readers convert compiler-driven.
1810 Slices, each independently shippable and A/B-verifiable:
1811 1. **Focus (DONE 2026-07-12)** — both stores
1812 (`UiContext.focused_widget` AND the `core.rs` thread-local
1813 `FOCUSED_WIDGET`) → `Option<WidgetId>`, kept as two stores with
1814 their existing reader sets (merging them changes observable
1815 focus behavior — not this phase's job). Thread-local fns that
1816 must dispatch `unfocus`/`FocusOut` gained a ctx param (every
1817 dispatching call site had one in reach). `set_focused` /
1818 `set_focused_ptr` refresh the registry with the pointer they
1819 are handed, so focus on a not-yet-registered widget keeps
1820 working; `is_focused_addr` → `is_focused_id` (base-id
1821 comparison). Verified: 168 tests; identical click sequences on
1822 text-editor (AE≤6, empty 8% masks) and data-editor (focus
1823 click AE=0); settings spinbox click-to-focus live.
1824 2. **Popovers + context-menu target (DONE 2026-07-12)** —
1825 `active_popovers: Vec<WidgetId>`, `ContextMenuState.target:
1826 Option<WidgetId>`; every occlusion/render walk resolves through
1827 the tree; `register_popover` takes `&mut` and self-registers;
1828 `show_context_menu` derives + registers the target id;
1829 `context_menu::mouse_input` takes the resolving ctx;
1830 `is_coordinate_covered` id-keyed with `WidgetId(0)` as the
1831 no-base sentinel; `EventCtx::widget_addr` deleted (use
1832 `ectx.id`). Verified: A/B vs slice-1 captures byte-equivalent
1833 (incl. the TE File-menu popover); DE leaf context menu Copy
1834 Key → wl-paste; settings page-dropdown popover switches pages.
1835 3. **Container child storage (DONE 2026-07-12 — by deletion, not
1836 retype).** The stored-field census found the slated vecs were
1837 ballast: `ParametersBg.children` was never populated anywhere
1838 (deleted with its parent-tracking twin, the container hooks,
1839 eleven dead iteration blocks, `collect_child_quads`, and
1840 window_runner's quad-attribution loop); `ScrollBox.children`
1841 was write-only (deleted with TI's one push). What still stores
1842 raw pointers after this slice: the `WidgetTree` registry (by
1843 design) and TI's app-side `ControlPanel.children` — live,
1844 pointing into boxed slots, consumed in ctx-less paint/arrange
1845 paths, so it retypes when those paths gain ctx (the endgame).
1846 Paginator's `container_children` is a transient field ref, not
1847 storage. The `Element::children`/`parent`/`add_child`/
1848 `set_parent` *signatures* still traffic in pointers, but every
1849 value is transient and tree-resolved at call time — they die
1850 with the `Element` endgame rather than warranting a standalone
1851 signature sweep.
1852 4. **`propagate_event(event, root: WidgetId)` — DONE (2026-07-13,
1853 the plumbing retype).** The handle rule it establishes: raw
1854 `*mut dyn WidgetHost` may appear ONLY as (a) the `WidgetTree`
1855 registry payload — the one ownership bridge, written at
1856 registration; (b) a registration argument derived from a live
1857 `&mut` (`register_widget`, `set_focused_ptr`-class
1858 self-registration — never stored); (c) machinery-internal
1859 transients resolved from the registry inside one call.
1860 Everything else crossing an API boundary carries `WidgetId` and
1861 resolves through the generational tree at use — a stale id is a
1862 loud no-op (`eprintln` canary), never a deref. Executed: the
1863 router resolves the root at the top of `propagate_event`
1864 (`propagate_event_impl` keeps its private resolved-ptr param);
1865 ~470 app dispatch sites across 14 apps went `.as_ptr_mut()` →
1866 `.id()` (field paths regex-converted; `let ptr = …` pairs,
1867 ptr-Vec collections, and dyn-roster receivers hand-converted);
1868 settings' `section_widgets`/`extra_dispatch_roots`/
1869 `page_dispatch_roots` retyped to `Vec<WidgetId>` with the
1870 keyboard section-focus block on `focus::is_focused_id`/
1871 `set_focused_id`; dead `WidgetPtr` + caller-less
1872 `register_popover_ptr` deleted. THE CONTRACT THE RETYPE
1873 SURFACES: a dispatch root must be REGISTERED. Most apps get
1874 registration as a `render_widget`/`paint_root_into` side
1875 effect; the canary caught every gap live: TI (roster never
1876 registered — per-frame `register_roster()`), settings chrome
1877 (wiped by `rebuild_layout`'s `clear_hierarchy` — re-registered
1878 after the view pass) + per-page rows rebuilt on data refresh
1879 (`AppPage::register_extra_dispatch_roots(ctx)` runs before
1880 each dispatch — the same liveness cadence the ptr router had)
1881 + custom-drawn menus (fonts/system pages), email (hand
1882 aggregate, per-frame block), authenticator (same), dm's bg
1883 root, LI's word-processor box, cloud (registers at its five
1884 dispatch sites), designer (frame re-registration skipped
1885 INVISIBLE slots while the wheel loop dispatches the whole
1886 roster). BUG FOUND: cce-graph registered its graph under a
1887 hand-minted `NEXT_WIDGET_ID` instead of the widget's own base
1888 id — `graph.id()` was unresolvable all along (focus/drag
1889 lookups on it silently failed); registration now uses the
1890 real id and the synthetic field is gone. Verified: 165 cce-ui
1891 tests + full workspace suite; canary-silent pointer/click/
1892 wheel probes over 13 apps and all 10 settings pages; TI
1893 dropdown→Grid relayout lands end-to-end through the id
1894 router; designer /state serves; demo's four event loops shed
1895 their unsafe self-alias entirely.
1896 FOLLOW-UP (same day): **`children`/`parent` left the trait
1897 (~52→50)** — tree structure is read off `ctx.tree`
1898 (`parent_id`/`parent_ptr`/`child_ids`/`children_ptrs`); no
1899 trait method returns a raw pointer anymore. `Adapted`'s
1900 container branch was redundant: Paginator (the one
1901 `Layout::container_children` implementor) tree-links its strip
1902 every tick via `register_embedded_children`, so the tree
1903 serves the walks identically (worst case a first-frame gap
1904 before the first tick). Machinery consumers (propagate
1905 descent, `find_hovered_scrollable`, the paint walk,
1906 `all_quads`/`all_rounded_quads` defaults, designer's render
1907 walks, TI's flat-walk parent skip) now read the tree directly
1908 — sanctioned transient-pointer class. DEAD CODE FOUND: both
1909 `navigate_focus` twins deleted — `UiContext::navigate_focus`
1910 had zero callers, and `focus::navigate_focus` (settings'
1911 ctrl-nav preamble) resolved parent/children through a
1912 freshly-made EMPTY UiContext, so it always returned false
1913 (parent has no field-derived form; ctrl+i needed a focused
1914 Paginator, which is never focusable). Settings' real ctrl-nav
1915 is its own section machinery, unchanged. serialize.rs's
1916 dummy-ctx child lookup could only ever surface Paginator's
1917 strip — kept via the 6aw concrete downcast. Verified: 165
1918 tests + workspace suite; email tab strip paints and a Sent
1919 click lands through the tree link; TI page-selector crop
1920 byte-identical (no double-draw); designer /state + full panel
1921 text intact; settings/files/demo canary-silent.
1922 FOLLOW-UP: **`as_ptr`/`as_ptr_mut` left the trait (50→48).**
1923 They live on as inherent `Adapted<W>` methods (the
1924 registration-bridge class) — every concrete call site resolved
1925 unchanged; dyn/generic receivers became the plain casts the
1926 impl always was (`w as *mut (dyn WidgetHost + 'static)`;
1927 `impl_widget_base!` no longer generates them). Designer's
1928 `find_widget_index` now honestly takes the thin `*const ()`
1929 address its body always compared. Verified: 165 tests +
1930 workspace suite; designer capture pixel-identical to the
1931 prior slice's (empty 8% mask) + /state live; TI click probe
1932 canary-silent.
1933 **THE DESIGNER EVENT REDESIGN — DONE (2026-07-13), closing the
1934 6bd deferral.** The resolution is a design decision, not a
1935 router conversion: the designer's event layer IS its own
1936 z-ordered windowing system (custom hit shapes with
1937 circular-pane overrides, hardcoded pane z, pane-focus
1938 derivation, unfocus rituals) and deliberately delivers through
1939 `handle_event` directly — the UiContext router's
1940 hit-gating/descent/drag tracking cannot own that policy, and
1941 mixing the two would double-run drag state machines. What the
1942 redesign fixes is the CONFLATION the deferral named:
1943 `drag_widget` no longer doubles as app-mode-drag marker. A new
1944 `AppDrag` enum (NetworkResize/ParamResize/SpreadsheetResize,
1945 each variant carrying its whole gesture state — dir, start
1946 rect/width/height, start mouse) owns the floating-pane edge
1947 resizes; the three `is_resizing_*` flags and four
1948 `drag_start_*` scratch fields are deleted; `drag_widget` only
1949 ever names a widget drag driven through the slot's Input drag
1950 hooks; exactly one of the two is armed per press. Release
1951 teardown, cursor hiding, path-change resets, and the
1952 hover-loop gate all read the split state; the hot-path
1953 MouseInput debug `println!` died as a rider. Verified: A/B vs
1954 stashed baseline — canvas-click and menu-open frames
1955 byte-identical (AE=0), id-stripped /state identical, launch
1956 frame differs by cursor sprites only (same-binary control
1957 AE=0). Held-gesture spot-check (2026-07-13, ccectl held-drag
1958 injection): node drag PASSED (Camera [1,1]→[3,3] via /state),
1959 NetworkResize right-edge PASSED, ParamResize left-edge PASSED,
1960 and SpreadsheetResize top-edge PASSED (the earlier "pane opens
1961 collapsed" report was a misdiagnosis: the HTTP menu_click driving
1962 the toggle was a SILENT NO-OP — see below — so the pane was never
1963 open; a real click opens it at its correct 250px default and the
1964 held top-edge drag grows it). Found instead: the HTTP `menu_click`
1965 action only reaches the MenuBar widgets' index-matched dispatch in
1966 window.rs (LEFT_MENUBAR's View menu has items 0-4), while the
1967 pane-toggle items ("Show Spreadsheet Pane" etc.) live in the
1968 OTHER menu system — the button-param menu pane drained by
1969 `sync_parameters_to_project`'s label match; out-of-range item
1970 indices replied "success" while doing nothing, and the handler's
1971 synthesized cursor park (-9999) sheds stray hover-diff pixels
1972 that can masquerade as the click's effect. The menu dispatch
1973 existing TWICE (index-matched in window.rs vs label-matched in
1974 app.rs, with diverging item sets) is the trap that produced the
1975 misdiagnosis. Both API holes are since FIXED (cce-designer
1976 b4ac763 + afe0f78): `menu_click` validates its indices against
1977 the target menubar's real dropdowns (a non-menubar widget_idx
1978 used to PANIC the app) and echoes the clicked label; the label
1979 match is extracted to `State::execute_menu_action`, and the new
1980 `{"action":"menu_action","label":"Show Spreadsheet Pane"}` drives
1981 the menu-pane items directly (verified: pane toggles on at its
1982 exact rect via curl, toggles off byte-identical). The network pane's breadcrumb-strip "panel
1983 move" is CONFIRMED INERT: the press arms
1984 `drag_widget = NETWORK_PANEL_IDX` on `Adapted<PassivePlate>`
1985 ("no children and no events"), so DragUpdates land on a widget
1986 with no drag hooks — dead scaffolding, DELETED (cce-designer
1987 00e148a: both press arms, the always-false hit_menubar block,
1988 the NETWORK_PANEL_IDX drag-driver arms, and the two is_dragging
1989 layout read-backs; border/strip presses keep their real focus+
1990 consume behavior — regression-verified live). And the
1991 spot-check caught a real press-routing bug the A/B frames could
1992 not: VIEWPORT_IDX and PARAM_IDX shared the -4 z tier, and the
1993 stable sort's index tiebreak (3 < 6) sent EVERY press over the
1994 floating params pane to the viewport — slider/Float3 held drags
1995 were dead end-to-end (only the scrollbar, armed via its own
1996 press path, worked). Fixed in cce-designer 4b78aa5 (PARAM_IDX
1997 gets its own -3 tier above the viewport it floats over);
1998 live-verified Position X 2.50→10.00 via /state. The
1999 `draggable`/`is_dragging` trait methods still have this
2000 cascade + TI's ControlPanel as dyn consumers — they leave the
2001 trait with the CP endgame.
2002 **THE CONTROLPANEL ENDGAME — DONE (2026-07-13). The last
2003 stored child-pointer surface is gone, and `draggable`/
2004 `is_dragging` left the trait (49→47).** TI's ControlPanel is
2005 DISSOLVED to scroll chrome (~120 lines: ScrollBox + bg/border
2006 paint + drag hooks): its `Vec<*mut dyn>` children, label-
2007 matched arrangement, aggregate views, dummy-ctx event/tick/
2008 drag forwarding, and scroll-translated coordinates are all
2009 deleted. The app owns the panel now: `arrange_control_panel`
2010 lays the child slots at SCREEN coordinates (scroll offset
2011 applied at layout time, re-run every frame — the wheel moves
2012 content on the frame it repaints); `display_list` emits child
2013 geometry/text clamped to the panel viewport with the legacy
2014 partial-clip radius-zeroing and border-inset rules; children
2015 dispatch as ordinary routed roots. THE GATE THE DISSOLUTION
2016 REQUIRES: children at real rects are hit-testable even when
2017 clipped below the fold — `cp_gate` (panel rect ∪ open child
2018 popovers, the legacy `ControlPanel::hit`) gates the press
2019 pre-scan, release broadcast, and wheel; keys stay
2020 focused-path-only; the panel takes the wheel before its
2021 children (legacy scroll-frame order). BUG THE GATE FIXED
2022 LIVE: a fold-hidden StatusBar toggle stole the page-selector
2023 press. FOUND: the legacy panel double-drew its scrollbar
2024 (rounded AND plain aggregate views — the 6p/6v class);
2025 single-drawn now, thumb correctly dimmer. `draggable`/
2026 `is_dragging` became inherent `Adapted` reads; the two
2027 index-driven rosters (TI 53+5 slots incl. child-mode enum
2028 variants, designer 17) route them through generated per-slot
2029 matches; 14 UFCS test forms became dot calls. Verified live:
2030 Windows page A/B vs pre-dissolution baseline (static +
2031 scrolled + popover states — masks empty except the scrollbar
2032 single-draw strip), dropdown item select updates the Surface
2033 Info description end-to-end, spinbox +/- increments, wheel
2034 scrolls with content following, page switching intact both
2035 ways, Controls page unchanged, child mode alive; 28-target
2036 suite. Scrollbar thumb drag: spot-check PASSED (2026-07-13,
2037 ccectl pointer-press/release): the held thumb drag scrolls the
2038 Windows-page ControlPanel with content tracking the drag —
2039 Create Window/Width/Height/Window Type scroll off as the
2040 Window Elements/Border/Bevel tier comes in, thumb following.
2041 5. **window_runner render plumbing + remaining `as_ptr` sites —
2042 DONE (2026-07-13, the last slice).** The slice-3 census was
2043 right: window_runner held no pointer state (its one mention
2044 was a doc comment). The real residue was the paint walk's
2045 entry signatures — `paint_tree`/`paint_root_into` now take
2046 `&dyn WidgetHost` (the walk only reads; descent resolves
2047 children through the registry and derefs those transients
2048 internally), and `append_widget_text`'s lifetime-erasing
2049 transmute died with the ptr param it bridged to. Every caller
2050 simplified: the unsafe self-alias blocks that existed ONLY to
2051 mint `*mut` arguments (text-editor, graph, data-editor, the
2052 demo, fonts' walk calls) are plain shared borrows now;
2053 `render_widget`'s internal cast is gone. What still carries
2054 `*mut dyn WidgetHost`, all deliberate and documented: the
2055 WidgetTree registry payload + registration arguments (the
2056 ownership bridge), machinery-internal walk transients, the
2057 `Layout::arrange_children`/`container_children` hook
2058 signatures (narrow-trait, ctx-less by design), and
2059 EventCtx's transient host ptr. Verified: 28-target suite;
2060 text-editor/graph/data-editor/fonts live captures render
2061 fully, canary-silent. THE POINTER-RETYPE PROGRAM IS
2062 COMPLETE — no further slices are recorded.
2063 ~~then the `Element` + `Adapted` endgame (own design pass)~~
2064 — landed long since as the 6bd flip (`Element` deleted,
2065 `Adapted` survives as the one host wrapper).
2066 Stored-pointer state remaining after slices 1–3, all deliberate:
2067 the `WidgetTree` registry; TI's `ControlPanel.children`; and the
2068 tick-refreshed parent copies in MenuBar/StatusBar/Dropdown models
2069 (`parent_changed`/`tracked_parent` — written each tick by the
2070 re-parenting pattern with the live host pointer, read in ctx-less
2071 popover-direction/corner-radius math). Endgame option for the
2072 parent copies: snapshot the *data* read through them (parent rect,
2073 radius, is-Ramp flag) at re-parent time instead of the pointer —
2074 same refresh cadence, no deref of potentially-dead memory; watch
2075 the one-frame rect lag on resize if reads move to snapshots.
2076 - **The legacy tuple views retired (the same day).** The five apps moved to `paint_self`
2077 (cce-secrets, the greeter, the settings app's dropdown and search box, the gallery, the
2078 designer's render loop), then the toolkit's own readers: `ParametersBg`, the ramp and the
2079 menubar read their children's `painted_prims`; `Adapted::paint_self` computes the
2080 TextBox's focus highlight itself; the bridge and `corner_radii` read `Paint::corner_style`;
2081 `append_widget_plate` stopped drawing the widget's arcs; `paint_legacy_leaf` and
2082 `widget_vertices` / `push_widget_vertices` had no callers left. The views and the hooks
2083 that served only them (`serves_legacy_plain_quads`, `legacy_plain_quads`,
2084 `aggregates_child_extra_quads`, `forwarded_highlight`) are deleted, and the tests that
2085 asserted on them read `painted_prims`. Pixel A/B in a scale-2 shadow: the designer (five
2086 pane layouts) and the gallery identical before and after the toolkit change; each app's
2087 own move is described in its commit (cce-secrets and the greeter changed look by choice,
2088 the settings dropdown gained its face plate, the gallery lost a stray outline).
2089 - **WidgetHost 38 → 31 (the same day).** The legacy tuple views (`extra_quads`,
2090 `extra_arcs`, `extra_circles`, `all_quads`, `all_rounded_quads`, `highlight_quad`,
2091 `corner_style`) moved to `WidgetHostExt` too, computed from the paint model exactly as
2092 `Adapted` did (its private `content_rect` / `painted_prims` / `visible_children` went
2093 with them). Five apps still draw through them; moving those to `paint_self` is what
2094 retires the views themselves, and it changes how some look (cce-secrets draws its
2095 controls flat through them). Every app that reads them drew identically to the pixel.
2096 - **WidgetHost 57 → 38 (2026-10-08).** The trait had grown back past the blueprint's ~55
2097 with the keyboard and accessibility hooks, and most of what `Adapted` implemented was a
2098 one-line forward to a narrow trait. The narrow traits are dyn-compatible, so the host now
2099 hands its widget out as them (`layout_model` / `paint_model` / `input_model` /
2100 `input_model_mut`) and `WidgetHostExt` (blanket over every host, `dyn` included) carries
2101 the 19 forwards and 3 derivations (`label`, `corner_radii`, `mark_dirty`) as provided
2102 methods; `plate_bevel`, never overridden and so always `None`, went. What stays on the
2103 trait adds host state to the model's answer. Apps changed only by importing the
2104 extension trait; the designer, the demo, the gallery, cce-files, the data editor and
2105 the settings app drew identically to the pixel before and after (scale-2 shadow).
2106 - **The `Element` endgame design (6bd, decided 2026-07-12).** The
2107 endgame is a **trait replacement, not an app rewrite**. Grounding
2108 facts (consumer survey): direct per-method dispatch
2109 (`mouse_input`/`cursor_moved`/`keyboard_input`/wheel on concrete
2110 `Adapted` fields) exists in nearly every app — making routed
2111 events a prerequisite would gate the endgame on ~12 app
2112 migrations, so the direct-dispatch surface *stays on the new
2113 trait* and shrinks later as apps move to routed events at their
2114 own pace. Designer's index-driven roster broadcast needs only a
2115 small dyn set (unfocus, drag hooks, set_modifiers, z_index,
2116 hit_test, visibility, tick, prepare_text, focused, as_any). The
2117 legacy tuple getters' consumers are the `T: Element` generics in
2118 layout.rs (render_widget/Column/Section — settings, LI, files,
2119 colors render paths) and window_runner's tessellators
2120 (widget_vertices — designer/TI): generic, not dyn, so they can
2121 re-bound onto a narrower bound without touching the dyn surface.
2122 Target shape: a **`WidgetHost` trait of ~40 methods** implemented
2123 once by `Adapted<W>` (blanket over the narrow traits). `Adapted`
2124 does NOT die — it survives as the single host wrapper owning base
2125 state; what dies is the 92-method god-trait and its Option-base
2126 escape hatches (`base()` becomes a guaranteed `id()`/state
2127 access, killing the `WidgetId(0)` sentinel class). Phases, each
2128 shippable:
2129 1. **Capability actions → one enum method (DONE 2026-07-12)**:
2130 the 13 context-menu action methods (`cut_selection`…
2131 `copy_path`) became `context_action(ContextAction) -> bool`
2132 across Element, the Input hooks, and the Adapted forwards; the
2133 Input default keeps whole-value Cut/Copy/Paste through the
2134 value-string pair; TreeList's seven action bodies moved to
2135 inherent methods via an impl split (zero code movement);
2136 core.rs maps the menu strings (incl. the load-bearing "Cear")
2137 to enum values. Element is down to 79 methods. Live-verified:
2138 tree Copy Key/Copy Value/Collapse and TextBox Paste through
2139 the enum dispatch.
2140 2. **Tuple getters — DISSOLVED into the flip (measured
2141 2026-07-12).** The premise was wrong on two counts. (a) The
2142 dyn consumers are real: designer's render loop reads
2143 `extra_quads`/`extra_circles`/`extra_arcs`/`color` directly
2144 off its `&dyn` handle AND feeds it to the tessellators, and
2145 the paint walk's legacy-leaf branch reads
2146 `all_quads`/`all_rounded_quads`/`extra_*`/`widget_font` via
2147 dyn — the visual surface must ride the host trait object.
2148 (b) A separate `LegacyVisual` trait can't be reached from the
2149 existing trait object (no cross-trait-object casts; a
2150 supertrait split trips over `rect`/`color` defaults needing
2151 `base()`). The genuinely generic consumers (layout.rs
2152 render_widget/Column/Section, settings'
2153 `collect_window_child`, dm/cloud's local tessellator copies
2154 with concrete receivers) simply re-bind at the flip.
2155 3. **Tree-link methods — landed pre-flip after all (2026-07-12).**
2156 `children`/`parent` are core machinery walk methods (propagate,
2157 painter, navigate) — host-trait material, they stay.
2158 `clear_children` left in batch 2; **`set_parent`/`add_child`
2159 left in batch 4**: both are inherent `Adapted<W>` methods now
2160 (files' concrete sites resolve unchanged); the dyn callers
2161 were only three — `focus::link_parent_child`'s body (rewritten
2162 as the register + `tree.link` + `tree.set_parent` ops the pair
2163 always was), TI's page-selector/StatusBar roster pair (now one
2164 `link_parent_child` call), and TI ControlPanel's per-arrange
2165 dummy-ctx child re-parent (deleted — every effect was
2166 discarded with the dummy ctx, the same inert-ritual class as
2167 Ramp's tick re-parents). A/B: TI gallery (flat-walk `parent()`
2168 skip intact — no page-selector double-draw) + dm greeter, both
2169 empty 8% masks. Note Paginator both serves
2170 `container_children` AND `link_ids`-registers its strip — the
2171 ctx-less walks (popover_rect/prepare_text/render_popover) are
2172 why the field-derived form must stay.
2173 4. **The flip — DONE (2026-07-12): `Element` is deleted; the
2174 trait is `WidgetHost`.** Landed in two shippable halves:
2175 **(a) the base() guarantee** — `base`/`base_mut` return
2176 `&Widget`/`&mut Widget` (no Option), killing the escape hatch
2177 and the `WidgetId(0)` no-base sentinel class;
2178 `as_any`/`as_any_mut`/`as_ptr`/`as_ptr_mut` became required
2179 (their defaults manufactured DummyAny/null-DummyElement
2180 stand-ins nothing could use); every Option-handling call
2181 site collapsed 1:1 to direct reads (12 repos); the
2182 layout/arena/tree test mocks grew a base field via
2183 `impl_widget_base!`. **(b) the rename** — 634 word-boundary
2184 occurrences across 18 crates; the workspace compiled on the
2185 first pass. `ElementState` (input enum) keeps its name;
2186 cce-layout-interface's local `Element` document enum was
2187 already alias-insulated (`Element as UiElement`). Since the
2188 shrink batches had removed every non-blueprint method first,
2189 the rename IS the retype — the registry/context/painter/
2190 window_runner signatures all read `dyn WidgetHost` now.
2191 Verified: 163 tests; settings audio render stream
2192 byte-identical across BOTH halves; files + data-editor A/B
2193 AE=0; live settings page-dropdown popover → Fonts page
2194 switch. The trait sits at ~65 methods; the remaining
2195 shrink-later blocks (direct-dispatch, value, as_ptr
2196 transitional, `preferred_height`/`value` dyn consumers)
2197 thin out per-app as routed events / concrete slots spread.
2198 5. **The routed-events tail — DONE (2026-07-13).** All 12 apps
2199 (+ TE/DE/demo from 6ab–6ad) dispatch through
2200 `propagate_event`; self-routing composites (Paginator, whose
2201 tree-registered strip would consume its presses under the
2202 router's children-first descent) go through `handle_event`;
2203 only designer's press/move cascade stays direct, by recorded
2204 design (its `drag_widget` doubles as app-mode drag with
2205 circular-pane hit shapes). En route, the ROUTED-DRAG GAP was
2206 found and fixed: the router's DragStart/DragUpdate/DragEnd
2207 fell into `Input::on_event`'s default and every routed drag
2208 was silently dead — `Adapted::handle_event` now maps them
2209 onto the Input drag hooks (regression test drives a full drag
2210 through `propagate_event`).
2211 6. **Direct-dispatch block census (2026-07-13) — the collapse
2212 design.** With every app-side dispatch caller gone, the
2213 block's remaining consumers are: (a) cce-ui widget-INTERNAL
2214 forwards — composites driving embedded children (ramp,
2215 parameters_bg, treelist, dropdown, menu, paginator,
2216 breadcrumb, color_selector, scroll_box, TI's ControlPanel,
2217 cloud's json_layout); (b) designer's deferred cascade (dyn
2218 roster calls); (c) ~40 tests (UFCS `WidgetHost::` forms);
2219 (d) `Adapted`'s own entry-point impls (die with the methods).
2220 THE COLLAPSE: every remaining caller rewrites as
2221 `handle_event(&Event::…)` — behavior-identical by
2222 construction (the entry points literally forward there, and
2223 the drag fix routes `Event::Drag*` to the hooks) — then
2224 `mouse_input`, `cursor_moved`, `on_cursor_moved`,
2225 `mouse_wheel`, `keyboard_input`, `drag_begin`, `drag_update`,
2226 `drag_end` leave WidgetHost (8 methods, ~67→59). TWO
2227 CAVEATS: (1) `Adapted::keyboard_input`'s `!visible()` gate
2228 must MOVE INTO `handle_event`'s KeyInput arm (designer's
2229 hidden-widget broadcast relies on it; the routed path
2230 currently lacks it — moving it also fixes that latent
2231 inconsistency); (2) `Adapted`'s PointerMove arm calls the
2232 trait `cursor_moved` internally — inline the
2233 coverage-gate + hover-recompute body as inherent before
2234 deleting. The QUERY/POLLING surface (`draggable`,
2235 `is_dragging`, `take_click`, `take_change`, value getters)
2236 stays — no Event form; dies with typed messages (§3.5) or
2237 container dissolutions. ~100 call sites, one session.
2238 **DONE (2026-07-13): WidgetHost 67→59.** The eight left the
2239 trait; in-crate composite forwards to concrete embedded
2240 children resolve unchanged through the inherent `Adapted<W>`
2241 entry points (the batch-1 recipe — far cheaper than the
2242 feared 100 rewrites); dyn callers (designer's cascade, TI's
2243 ControlPanel child forwards, cloud's json_layout slider
2244 drag) build the equivalent `Event` and call `handle_event`.
2245 Both caveats landed: the keyboard `!visible()` gate lives in
2246 `handle_event`'s KeyInput arm (closing the routed path's
2247 missing-gate hole), and the coverage-gated `cursor_moved`
2248 default became the inherent `Adapted::cursor_moved`. The
2249 trait's default `handle_event` serves test shims only.
2250 Verified: 164 tests; settings render stream byte-identical;
2251 TI interactive four-state A/B empty masks; designer /state
2252 identical across a canvas click.
2253 ~~The MenuBar/StatusBar/Dropdown parent-pointer
2254 snapshot change rides this phase.~~ **Landed early
2255 (2026-07-12): the census showed all five stored widget-side
2256 parent pointers production-DEAD** (nothing ever set_parent's
2257 a MenuBar/StatusBar; ramp's per-tick re-parents fed a
2258 write-only field through a dummy ctx — legacy behaved the
2259 same). TextBox.parent deleted outright; Dropdown.parent
2260 became `parent_snapshot` read-DATA (rect/is_ramp/color, same
2261 direct-write activation, Ramp-clamp test adapted) and its
2262 write-only `tracked_parent` died; MenuBar/StatusBar lost the
2263 fields, their `parent_changed`/`tracked_parent` overrides,
2264 MenuBar's never-firing `adjust_rect` clamp, and now report
2265 the 0.0 corner radius production always read; the
2266 `Layout::parent_changed`/`tracked_parent` hooks are deleted
2267 (implementor-less), `Adapted::parent` is tree-only, and the
2268 Ramp/ColorRamp tick_ctx re-parent rituals are gone. Stored
2269 `*mut dyn Element` survives ONLY in the WidgetTree registry
2270 payload, EventCtx's transient host ptr, and TI's
2271 ControlPanel. A/B: text-editor + TI Ramp-child static AND
2272 preset-popover-open frames all empty 8% masks; 163 tests. **Measured blueprint (~55
2273 methods, from the machinery's actual call sites):**
2274 identity/tree — id (guaranteed, no more `Option<&Widget>`),
2275 type_name, label, as_any/as_any_mut, as_ptr/as_ptr_mut
2276 (transitional), visible/set_visible, z_index,
2277 is_child_visible, children(ctx), parent(ctx), add_child
2278 (transitional, TI);
2279 layout — rect, set_rect, measure, layout, label_x_offset,
2280 set_row_rect;
2281 events — handle_event, hit_test, mark_dirty, tick/wants_tick,
2282 set_modifiers, focus/unfocus/focused, context_action,
2283 blocks_backplate_drag, is_scrollable, plus the
2284 direct-dispatch block (mouse_input, cursor_moved, mouse_wheel,
2285 keyboard_input, drag_begin/drag_update/drag_end/is_dragging/
2286 draggable, take_click, take_change) — shrinks per app as they
2287 move to routed events;
2288 value — get_value_string/set_value_string, set_text,
2289 set_selected — shrinks as app loops go concrete-slot;
2290 paint — paint_self, clips_children, renders_own_subtree,
2291 prepare_text, popover_rect, render_popover, dirty-flag access;
2292 visual tuples (walk legacy branch + designer loop) — color,
2293 all_quads, all_rounded_quads, extra_quads, extra_circles,
2294 extra_arcs, corner_radii, plate_bevel, solid_border,
2295 widget_font, highlight_quad.
2296 Everything else on today's Element (79 methods plus the
2297 generic-only surface) either moves to inherent `Adapted<W>`
2298 methods for the generic render machinery or dies.
2299 **Execution mode: shrink Element IN PLACE toward the blueprint,
2300 then rename it to `WidgetHost` when it matches** — a parallel
2301 trait can't be reached from the existing trait object, but
2302 removing non-blueprint methods one census-driven commit at a
2303 time keeps every state shippable. **First shrink batch (DONE
2304 2026-07-12, 79→73):** `highlight_color` folded into the
2305 `highlight_quad` default (zero overrides); `set_drag_bounds` +
2306 `intrinsic_size` moved to inherent `Adapted<W>` methods (their
2307 concrete callers — designer's network panel, fonts'/graph's
2308 hand-laid sizing — resolve unchanged); `layout_style`/
2309 `layout_children` deleted with **scene/bridge.rs itself** (its
2310 last production user was retired in 6aa; the narrow
2311 `Layout::intrinsic_size` hook stays — `Adapted::measure` reads
2312 it); `layout_ignore` deleted with its only consumers, the
2313 uncalled `layout_widgets`/`layout_widget_ptors`. Census
2314 lesson: grep BOTH `.method(` and UFCS `::method(` forms — the
2315 fonts/graph `Element::intrinsic_size(&x)` callers only
2316 surfaced at compile. **Second shrink batch (DONE 2026-07-12,
2317 73→69):** `hovered`/`set_hovered` deleted — the state is the
2318 base `Widget::hovered` flag, read/written directly by the
2319 `cursor_moved`/`on_cursor_moved`/`highlight_quad` defaults and
2320 `serialize.rs`; Button/Checkbox keep inherent accessors for
2321 immediate-mode hosts (cloud's json_layout downcasts to
2322 concrete `Checkbox`, so it already resolved to those).
2323 `corner_radius` + `rounded_corners` replaced by ONE
2324 `corner_style() -> (f32, (bool,bool,bool,bool))` mirroring the
2325 narrow `Paint::corner_style` — NOT folded into `corner_radii`,
2326 which is lossy: the radius is meaningful with every corner off
2327 (Menu/StatusBar report their parent's radius to children
2328 through the flags-off channel; breadcrumb can be
2329 flags-true/radius-0, whose legacy radius-0 rounded bg quad
2330 would vanish). `clear_children` moved to an inherent
2331 `Adapted<W>` method (every caller is a concrete Adapted field
2332 in cce-files). Consumer commits: settings renderer,
2333 TI ControlPanel aggregates, cloud json_layout. Verified:
2334 settings audio render stream byte-identical, files A/B AE=0,
2335 TI gallery empty 8% amplitude mask, 163 tests.
2336 Census facts for the leftovers: `preferred_height` has DYN
2337 consumers (container_layout.rs child-ptr walks + layout.rs
2338 machinery on `&dyn` children) — blueprint-adjacent, rides the
2339 flip, not inherent-movable; `value` has a live dyn consumer
2340 (`serialize.rs` over designer's `dyn_refs()`) — rides the
2341 flip. Remaining non-blueprint candidates:
2342 `on_cursor_moved` (belongs in the direct-dispatch block —
2343 blueprint addition, not a deletion), `set_parent` (flip
2344 material, with the parent-ptr snapshot change), `value`,
2345 `preferred_height`.
2346 7. **The value/polling block — DONE (2026-07-13): WidgetHost
2347 59→52.** This is the §3.5 "typed messages" resolution, and it
2348 lands the way 5k's controller half did: no app-defined message
2349 channel is needed — the polling drains stay concrete (inherent
2350 `Adapted<W>` forwards to the narrow `Input` hooks), and what
2351 dies is reaching them through the host trait. Seven methods
2352 left: `take_click`, `take_change`, `get_value_string`,
2353 `set_value_string`, `value`, `set_text`, `set_selected`.
2354 Census: five had ZERO non-test dyn consumers (the old
2355 designer-side serialize consumer of `value` is gone; the
2356 in-crate `widget/display/serialize.rs` inspector feed was the
2357 one live reader — now a concrete downcast chain over the five
2358 `Input::value` implementors Checkbox/Dropdown/Slider/
2359 RangeSlider/Spinbox, pinned by a unit test that fails if a new
2360 implementor is missed). The dyn readers of the rest went
2361 concrete-slot: TI's index-driven roster reads route through
2362 app-local `Roster::take_click/value/get_value_string/set_text
2363 (idx)` matches onto the concrete gallery slots (arms exist per
2364 drained slot; an unwired slot panics loudly); cloud's
2365 `JsonControl` grew an inherent variant-matched `take_click`
2366 (both call sites already gate on the button type); designer's
2367 pane-focus menubar loop writes `set_selected` on its five
2368 concrete `Adapted<MenuBar>` fields. The UFCS test forms became
2369 dot calls resolving to the inherent methods. STILL on the
2370 trait, each with live dyn consumers: `draggable`/`is_dragging`
2371 (designer's deferred press/move cascade + TI's ControlPanel
2372 child pointers), `preferred_height` (layout.rs container
2373 machinery) — these ride the designer event redesign / CP
2374 dissolution. Verified: 165 tests (new serialize pin);
2375 settings audio render stream byte-identical vs the stashed
2376 baseline; TI live probe — Button/Toggle clicks, Layout
2377 dropdown popover open, and a "Grid" selection re-laying out
2378 the gallery through the new roster drains end-to-end.
2379 RIDER: the `Control` subtrait (set_label + control_label) is
2380 DELETED — zero dyn consumers, zero `control_label()` callers;
2381 every impl just routed `set_label` to the inherent shadow, so
2382 the deletion is call-site-invisible (compile-verified across
2383 the workspace).
2384 Former slices 4/5 fold in: the app `as_ptr_mut` dispatch sites
2385 are rewritten by whichever of routed-events (per app) or the
2386 phase-4 flip reaches them first; no standalone pointer-to-id
2387 signature sweep.
2388
2389 - **Phase 7 — Plate unification: backplate becomes a ROLE of Plate (7a/7b/7c stages DONE 2026-08-25; open questions below).**
2390 Finish what 6as/6at began. The `Backplate` and `Plate` container widgets are deleted and the
2391 `is_backplate`/`is_movable_backplate`/`is_plate` flags are folded, but "backplate" survives as
2392 a second vocabulary for what is now one concept — a lit base surface (`Prim::Plate`). What
2393 remains under the old name: the `style.surface.backplate.*` config namespace and its getters
2394 (`backplate_{padding,gap,color,blur,corner_radius}`, the menubar/statusbar sub-styles); a
2395 partial merge already in the tree (`layout::plate_corner_radius()` falls back to
2396 `backplate_corner_radius`); and — the real content — a ROLE: "the plate that meets the window
2397 edge" (window-background drag via `blocks_backplate_drag`/`drag_allowed_at`, MenuBar/StatusBar
2398 carving into it, the compositor clipping every window at the span-widened backplate radius).
2399 Each app also hand-rolls its root-surface painting from the backplate getters (`DemoApp`
2400 in `src/main.rs` is the reference copy; every client repeats a variant).
2401
2402 **Motivation.** One surface concept instead of two makes plates fully compositional: a plate
2403 can be the base surface of a window OR a child of another surface, with nothing but role data
2404 distinguishing them. The concrete driver is detachable plates — cce-designer's detached panes
2405 already behave exactly like this (a pane plate becomes a new window's root plate; its interior
2406 corners become window corners), but the geometry lives app-side in
2407 `cce-designer/src/render.rs::pane_plate_radii` and the detach machinery is designer-only.
2408
2409 **Design.** No widget returns (6as stays won). The unification lives in the paint/geometry
2410 layer: a `PlateSpec` — rect, per-corner radii, color, blur, plus role flags:
2411 `window_corners: (bool, bool, bool, bool)` (which corners lie on the window silhouette) and
2412 `drag_background: bool` (whether uncovered area is a window-drag region). The toolkit computes
2413 per-corner radii from the flags (a window corner wears
2414 `window_corner_radius() * corner_span_factor()`, an interior corner wears
2415 `plate_corner_radius()` — the `pane_plate_radii` math, moved in from the designer), and the
2416 engine paints any plate root-or-nested through the one path, absorbing the per-app hand-rolled
2417 root painting. A window's base surface is just a plate whose four corners are all window
2418 corners.
2419
2420 **Invariants.**
2421 - *The window silhouette stays a shared cross-process contract.* The compositor clips windows
2422 from the SHARED corner value; per-plate radius freedom must never leak into a
2423 `window_corners=true` corner (the designer's config.kdl radius override already documents
2424 this trap at its `pane_plate_radii` call site). The role flags are where the constraint
2425 lives: flagged corners read the shared value, period.
2426 - *Blur regime follows the role.* A root plate frosts against the compositor's blur-behind
2427 (the negative-alpha marker convention); a nested plate blurs against app content. Detaching
2428 moves a plate between regimes; the marker choice keys off the role flags, and this is the
2429 subtlest part of the phase — it gets its own design note before code.
2430 - *No root container widget.* `PlateSpec` is data consumed by the paint path, not a node that
2431 owns the window.
2432 - *The droplet family rides the plate PUSH-CONSTANT block, not the plate concept.*
2433 `Prim::Droplet` (shader2d `MODE_DROPLET`) reuses the plate block's fields by mode-10
2434 reinterpretation only — it deliberately does not consume `PlateSpec` or the backplate
2435 getters. 7b may reshape how plates are DESCRIBED, but the shader-side field packing is
2436 shared: changing the plate block means re-checking the droplet arm. If droplets are ever
2437 folded into a generalized plate role, two external contracts must survive: the
2438 `module { droplet "k=v" }` spec-string idiom, and the compositor's scenefx droplet node,
2439 which parses the same `DropletSpec`.
2440
2441 **Stages.**
2442 - **7a — Vocabulary. REVISED at implementation, 7a-1 DONE (2026-08-25).** The original
2443 text said "`style.surface.plate.*` becomes canonical" — implementation surveying found
2444 `style.surface.plate.*` ALREADY EXISTS as the NESTED-plate style namespace (padding,
2445 color, border_color, border_thickness, blur) carrying deliberately different values
2446 from `backplate.*`; a flat alias would have merged root styling into pane styling.
2447 The canonical namespace is therefore role-scoped: **`style.surface.plate.root.*`**
2448 (with `plate.root.menubar.*` for the bar sub-style) — truer to the phase's thesis
2449 anyway: backplate = plate in the root role. Getter names follow as `root_plate_*`.
2450 - **7a-1 DONE.** Both config paths aliased: the layout style-registry table maps
2451 `plate.root.*` rows onto the same slots as `backplate.*` (slot names keep the
2452 historical prefix — invisible), and color.rs's JSON-pointer loads are canonical-first
2453 chains (`/style/surface/plate/root/…` `.or_else(` `/backplate/…)`), so the new
2454 spelling WINS when both are present; in the registry table both spellings write one
2455 slot and document order decides (single-spelling configs — all real ones — are exact).
2456 Canonical getters (`root_plate_{padding,gap,opacity,corner_radius}`,
2457 `root_plate_{menubar,statusbar}_{color,text_color,blur}`) with the old `backplate_*`
2458 names as plain delegating wrappers — NOT `#[deprecated]` yet: 15 crates + the
2459 compositor still call them (16-crate caller census in the 7a-1 commit). "root" joined
2460 `PROP_NODES` for the config-editor path helpers. cce-ui's own callers (config.rs
2461 tests, DemoApp, cce-relief, cce-ramp) migrated. Tests: the legacy styling test now
2462 reads through canonical getters (legacy-config → canonical-getter equivalence), plus
2463 a canonical-spelling test proving parse, precedence over legacy, and legacy-only
2464 fallback. 245 lib tests green; designer A/B AE=0.
2465 - **7a-2 — DONE (2026-08-25).** All caller crates migrated per-repo and the
2466 `backplate_*` wrappers flipped to `#[deprecated]`. Census correction: the compositor's
2467 9 census hits were all its OWN vocabulary (serde fields + a local default fn) — zero
2468 cce-ui getter calls; what it actually needed was the CANONICAL KDL alias, since it
2469 parses the silhouette block from the shared config.kdl itself (`plate { root ... }`
2470 accepted canonical-first, legacy `backplate` unchanged, tested both ways —
2471 cce-compositor@0ef2901). Implementation trap for the record: exact-match renaming of
2472 `backplate_corner_radius()` also matched the compositor's
2473 `default_backplate_corner_radius()` calls while its serde `default = "..."` string
2474 attribute did not — audit renames for substring collisions against local wrappers.
2475 The original census, by call sites:
2476 cce-compositor `server/config.rs` (9 — reads the SHARED silhouette values; the
2477 migration must not change which slot it reads), cce-files (10 across main/
2478 preview_pane/pages), cce-test-interface (11), cce-data-editor (5), cce-terminal (3),
2479 cce-graph (2), cce-fonts (2), cce-color-editor (2), cce-cloud (2), cce-text-editor
2480 (1), cce-system-interface `main.rs` (1), cce-authenticator (1). DONE: cce-designer
2481 (vk-smoke, the exemplar). CONFIRMED CLEAN, nothing to migrate: cce-status-interface
2482 (its bar styling reads `module { }` keys + `/style/status/*`, not the root-plate
2483 getters — verified by its owning session 2026-08-25). The
2484 `Application::is_movable_backplate_at` trait-method NAME is 7b vocabulary
2485 (behavioral role naming), not 7a's. With 7a-2 done, 7a is COMPLETE: new code uses
2486 `root_plate_*` / `plate.root.*`; the deprecated wrappers and the legacy config
2487 spelling were kept for out-of-tree configs.
2488 - **7a-3 — DONE (2026-09-06).** Every live config (shared, cce-graph, cce-designer,
2489 cce-notifier's per-app `plate { }`) was rewritten to the canonical spelling, then the
2490 aliases came out: the `backplate.*` pointer/registry read-aliases in cce-ui, the
2491 compositor's `backplate` node fallback, cce-grid's and cce-notifier's fallbacks, the
2492 `#[deprecated] backplate_*` getters, and `backplate` in `PROP_NODES`. A `backplate`
2493 block in a config is now silently ignored.
2494 - **7a-4 — DONE (2026-09-06).** The internal vocabulary followed: registry slots
2495 (`root_plate_*`), the `ROOT_PLATE_*` statics, `set_root_plate_*` setters,
2496 `read_root_plate_opacity_if_configured`, the trait methods
2497 `Application::is_movable_root_plate_at` and `Widget::blocks_root_plate_drag`, the
2498 compositor's `root_plate_{color,blur,corner_radius}` config fields (and the flat-form
2499 keys of the same name), cce-gallery's `RootPlate` exhibit and `--root-plate` flags,
2500 and the comment prose across every crate. "backplate" survives only in this RFC's
2501 history and in test fixtures that prove the legacy spelling is ignored.
2502 - **7b — `PlateSpec` + window-corner math toolkit-side.** Introduce the spec, port
2503 `pane_plate_radii` in, and give the engine a root-plate paint path fed by a spec instead of
2504 each app's hand-rolled quads (DemoApp first, then the clients). The designer's per-pane
2505 plates convert to specs with computed role flags. A/B: AE=0 per app.
2506
2507 **Blur-regime design note (required before code; written 2026-08-25).** Two frost
2508 regimes; the role selects between them:
2509 - *Root plate* (all four corners on the window silhouette): the fill stays
2510 POSITIVE-alpha translucent; the COMPOSITOR frosts what lies behind the window
2511 (`plate.root.blur` in the shared config drives cce-fx's blur-behind). The app
2512 draws no frost of its own.
2513 - *Nested plate* (any interior corner): frost is the NEGATIVE-ALPHA sentinel on
2514 the fill — the in-app vk frost pass blurs app content drawn BEFORE the plate
2515 (draw-order-dependent by design; see `param_plate_fill`). A pane touching some
2516 window edges is still nested for blur purposes: it frosts app content.
2517 `PlateSpec` therefore stores `color` with positive alpha plus `blur: bool`, and
2518 `fill()` applies the role-correct encoding: root → alpha forced non-negative,
2519 nested+blur → alpha negated. DETACH is exactly a role flip: interior corners become
2520 window corners, `fill()` flips regimes, and the formerly-frosted app content beneath
2521 simply does not exist in the new window. No other app-side blur change is needed.
2522
2523 **Radii rule.** A window-flagged corner wears
2524 `window_corner_radius() * corner_span_factor()` (the SHARED silhouette curve — the
2525 invariant); an interior corner wears `plate_corner_radius()` (the app-overridable
2526 pane value). This is `pane_plate_radii` verbatim, moved in.
2527
2528 - **7b-1 DONE (2026-08-25).** `PlateSpec` in `scene/paint.rs` (rect, positive-alpha
2529 color, `blur`, per-corner `window_corners`, perimeter `depth`) with
2530 `window_corner_flags(rect, win_w, win_h)`, `radii()`/`radii_for()`, role-aware
2531 `fill()`, and `PaintCtx::plate_spec`. DemoApp's hand-rolled root plate migrated —
2532 DELIBERATE visual correction: its radius was the un-spanned
2533 `root_plate_corner_radius`, so under squircle `corner_shape` its perimeter shading
2534 detached from the compositor's span-widened clip; the spec snaps it to the
2535 silhouette (demo AE≠0 expected and intended). The designer's `pane_plate_radii`
2536 delegates to the toolkit (A/B AE=0). Two 7a stragglers the getter census could not
2537 see (raw JSON-pointer reads, not getter calls) gained the canonical-first chain:
2538 `layout::window_corner_radius`'s shared-config read, and cce-grid's silhouette
2539 read.
2540 - **7b-2 — DONE (2026-08-25).** Nine clients migrated: files, terminal,
2541 system-interface, authenticator, data-editor (Prim::Plate hand-rolls →
2542 `plate_spec`, plus data-editor's concentric `corner_frame` now follows the
2543 silhouette), and graph, fonts, color-editor, text-editor (non-Plate root emissions —
2544 rounded_rect/border/legacy tuples — take their values from the spec via the new
2545 `layout::window_silhouette_radius()` scalar, which `radii_for` also uses). A/B
2546 revision: the original "AE=0 each" predates 7b-1's discovery that migrating IS a
2547 correction — every app moved off the un-spanned radius, so corners change by
2548 design. Verified: seven apps diff ONLY within 120px corner squares; the eighth
2549 (system-interface, hardcoded r=12 → silhouette, the largest jump) also shifts the
2550 perimeter roll's edge gradient, eyeball-confirmed as the arc correction.
2551
2552 **CORRECTION (2026-08-25, post-7c): the Plate-group half of that story was
2553 inverted — a double-span, since fixed.** `plate_spec` fed the spec's FINAL
2554 radii into `Prim::Plate`, whose contract is NOMINAL radii spanned downstream
2555 by `plate_push_raised(scale_corners = true)` — so the five Plate-group
2556 clients drew window corners at span² (12 → ~100 logical at n=4.5). Those
2557 apps had been CORRECT all along (nominal in, spanned once by the push); the
2558 "arc correction" the A/B eyeball accepted was the regression itself, caught
2559 when the user reported corners rounder than the desktop grid. Measured on a
2560 live corner diagonal: clip/grid arc at the expected 0.202·span depth, plate
2561 arc ~2× deeper. The four scalar-group clients (graph, fonts, color-editor,
2562 text-editor) tessellate without a downstream span, so for them the spanned
2563 scalar was and remains the genuine correction. Fix: `plate_spec` pre-divides
2564 by `corner_span_factor()` so the push's multiply reconstructs the spec's
2565 exact values; unit test `plate_spec_emission_round_trips_the_span` guards
2566 it. The same double-span reached DemoApp via 7b-1 — its "AE≠0 expected"
2567 diff bundled the genuine correction WITH the overshoot.
2568 Authenticator is values-only verification (never launch it in a shadow — it claims
2569 the PolicyKit D-Bus name). SKIPPED deliberately: cce-cloud (overlay popup windows —
2570 whether they share the decorated-window silhouette was an open question, since
2571 CLOSED: **yes** — resolved 2026-08-25 post-7c, cce-cloud@4829451. The compositor
2572 never clips layer surfaces, so the app's drawing IS the overlay's silhouette, and a
2573 launcher-sized panel at the nominal radius read nearly square beside real windows;
2574 its root emission is now a PlateSpec with all four corners window-flagged, verified
2575 pixel-identical corner depth to a real window's plate) and
2576 cce-test-interface's `Backplate` gallery shim (a legacy-lookalike test fixture;
2577 migrating it would defeat its purpose — still skipped, still deliberate). Designer pane EMISSION: radii and the
2578 nested-blur sentinel already flow from spec-derived values; full spec-OBJECT
2579 emission is deferred into 7c, because pane plates carry focus tint and
2580 widget-driven bevel styling `PlateSpec` does not yet model — detach will dictate
2581 whether the spec grows those fields or the widget hooks stay authoritative.
2582 7c builds on the role flip.
2583 - **7c — Detach/dock generalization.** Lift the designer's plate-corner control, collapse,
2584 and dock-drag onto `PlateSpec` so any app can offer them. The detached-window PROCESS model
2585 and sync channel (`default_project.json` polling) remain app policy — the toolkit provides
2586 the plate-role flip (interior→window corners, blur regime swap, CSD hookup via the existing
2587 `standard_csd`/`take_window_action` hooks), not the process management.
2588 - **7c-1 DONE (2026-08-25).** `widget::plate_dock`: the app-agnostic PROTOCOL —
2589 constants (control radius/inset, min plate span, stub height, drag threshold),
2590 `PlateDockState { collapsed, detached }` with `stubbed()`, `corner_center(rect,
2591 stubbed)` (rect placement incl. the stub exemption; the designer's circular-pane
2592 arc placement stays app policy), `corner_hit`, `press_becomes_drag`, and
2593 `standard_menu(state, can_detach)` → (label, `PlateDockAction`) rows the host
2594 appends its own items after (the designer's spreadsheet span modes). Deliberately
2595 LEAN: no container type, no widget — a second consumer decides those.
2596 `PlateSpec::detached()` is the role flip (all corners→window, radii snap to the
2597 silhouette, `fill()` swaps frost regimes), unit-tested. The designer delegates:
2598 constants re-exported, rect placement/hit/threshold/standard-menu all
2599 toolkit-calls; PLATE_SLOTS membership, visibility, dock regions, layout
2600 application, and the detach process spawn stay designer policy. Verified live:
2601 corner menus (Collapse/Detach; Reattach-only stubs), the collapse→stub→expand
2602 cycle, and dock-drag arming+drop all behave identically through the delegation.
2603 - **7c-2 — DONE (2026-08-25): cce-files is the second consumer.** Its preview pane
2604 carries the corner control; Collapse narrows every page's pane column to a Preview
2605 title stub (the list keeps the freed width — the feature's actual value), the
2606 stub's control restores the prior split fracs, and the divider/wheel are inert
2607 while collapsed. The adoption answered the open API questions:
2608 - `draw_corner_dot(pc, center, emphasized)` EARNED — both hosts drew the identical
2609 dot; the designer now uses it too.
2610 - A `PlateDock` CONTAINER did NOT earn its place: one `PlateDockState` field
2611 sufficed. Revisit only if a host manages many dockable plates outside its own
2612 state arrays.
2613 - Press arming was designer-specific after all: it exists to disambiguate click
2614 from dock-DRAG, and a single pane has nowhere to dock — files opens the menu
2615 directly. Moreover, files keys the whole interaction on mouse RELEASES: its
2616 routed-widget path consumes left PRESSES before the `Application` hook (only
2617 releases reliably arrive there), a per-app dispatch reality any adopter must
2618 check first. The designer's release-opened menu means the two feel identical.
2619 - The detached-window CSD packaging question is ANSWERED (2026-08-25,
2620 cce-files@805be06 — a working, end-to-end-verified preview-pane detach;
2621 RETIRED the same day at the user's direction, cce-files@f2a89d5:
2622 detach is designer-only for now. The answers below were derived from
2623 that implementation and stand; the code is one revert away at 805be06
2624 for whichever app adopts detach next): **no new toolkit packaging was
2625 needed.** A detached window is an ordinary
2626 `Application` whose root plate carries the detached role (all window
2627 corners); the compositor's decoration IS the CSD in this DE (border =
2628 grab surface, no titlebars), and plate_dock's existing pieces —
2629 `corner_center`/`corner_hit`/`draw_corner_dot`/`standard_menu` — cover
2630 the control. What the second implementation DID establish as the
2631 convention worth naming: **the child's Reattach is process exit** — the
2632 parent `try_wait`s and reclaims the pane, so every way a detached
2633 window can die reattaches it; and the child exits itself when its sync
2634 file or parent pid disappears, so orphans (crash, stale session
2635 restore) self-collect. The sync-file idiom generalizes: parent pid
2636 then payload, written before the spawn, unlinked on reattach.
2637 One packaging gap surfaced and stands as app policy for now: the
2638 toolkit context menu dispatches through a widget tree a minimal
2639 detached window does not have, so the child draws its one-row menu
2640 itself — a third consumer hitting this earns a widget-tree-free menu
2641 helper.
2642 - The designer pane spec-OBJECT emission question got its answer from the
2643 same implementation: detach dictated NO new spec fields — the files
2644 pane detached with `PlateSpec` as it stands, so focus tint and
2645 widget-driven bevel styling stay widget-hook territory and the spec
2646 stays lean. The designer's emission migration remains optional and
2647 unblocked.
2648
2649 All numbered stages DONE (2026-08-25), and all three open questions CLOSED the same
2650 day: cce-cloud overlays share the silhouette (7b-2); the first non-designer detach
2651 (cce-files' preview pane — built, verified, then retired by product choice the same
2652 day, see 7c-2) answered CSD packaging — nothing new was needed, the conventions are
2653 recorded there — and established that detach dictates no new `PlateSpec` fields,
2654 leaving the designer's spec-OBJECT emission optional and unblocked. The designer
2655 remains the only detach host. Phase 7 is COMPLETE.
2656
2657 **Design note (recorded 2026-08-30) — why carve GROUPING exists, and why its
2658 rarity is correct.** The mechanism predates this phase: it landed with the SDF-lit
2659 plate system (cce-ui@3877dd6), and its rationale lived only in that commit message
2660 and the `tessellate_display_list` doc comment until now. The tessellator promotes a
2661 `Recess` emitted while a `Plate`/`Bevel` is still "open" into a **CSG feature of
2662 that plate's single draw** (the per-frame feature UBO); every other carve renders
2663 through the standalone overlay branch. Two things justify the dual path:
2664
2665 - *Junction correctness at the perimeter roll.* A grouped plate is ONE composite
2666 height field — the rolled edge minus its carves — lit once per pixel from summed
2667 analytic slope vectors, so a carve wall meeting the plate's perimeter roll is an
2668 arithmetic junction. The overlay branch approximates that meeting with the
2669 host-box fade.
2670 - *Features never blend in color space.* An overlay is shading drawn over
2671 already-lit pixels, so stacked shading double-counts — the same reason
2672 `Prim::Ridge` exists rather than a boss+recess pair (double-counted specular at
2673 the crest).
2674
2675 The corollary that makes the design coherent: **the two paths differ visibly only
2676 near the host's rolled perimeter.** An interior carve (a TreeList or TextBox well
2677 in the middle of a window plate) never touches the roll, so the fallback is
2678 effectively exact there. Two scope corrections recorded 2026-08-30 (the first
2679 version of this note got them wrong): what groups is **full-ring untinted carves**
2680 (button grooves, slider wells) — the flush menubar/status bands never group, by
2681 design since cce-ui@80d50de: an edge-suppressed carve's wall rect extends past the
2682 boundary, relying on the overlay cover quad to clip it, a clip the grouped
2683 whole-plate draw does not have (grouped, the extended walls smeared across the
2684 plate). So the junction where a flush band meets the plate's roll is ALWAYS the
2685 host-box fade; grouping's value is the single-evaluation lighting of full-ring
2686 carves, exact wherever one sits near the roll. And per the audit that shipped
2687 `CCE_PLATE_DEBUG` (cce-ui@949e35e, three apps): **no misgrouping — every fallback
2688 is a documented rule firing correctly**; grouping is rare (demo 2 of 10, cce-files
2689 0 of 7) because apps constantly interleave flat fills with reliefs, and each one
2690 correctly closes the grouping window (the carve's shading is baked into the
2691 plate's earlier draw). Neither path is retirable: grouping-always is impossible
2692 for exactly that reason, and fallback-only would forfeit the exact junctions
2693 full-ring carves get when they do group.
2694
2695 The standing hazard is the *silent flip*: three of the six grouping conditions are
2696 dynamic (draw order, sibling plates, whether another plate claimed the host's
2697 feature run), so the same widget can render through either path depending on its
2698 surroundings. That shipped as a bug once — a hovered button's opaque fill severed
2699 every later button from the backplate they carve into — fixed by making the
2700 carve-host tracker a stack (`plate_stack`, cce-ui@9cfadad). `CCE_PLATE_DEBUG=1`
2701 reports each carve's verdict, the fallback reason, and which prim closed a
2702 grouping window; it is the first tool for any "same widget, different look"
2703 report. Hardening (landed 2026-08-30): debug builds warn loudly — once per
2704 geometry, no env var — when a groupable full-ring carve is enclosed by a
2705 still-open plate, its shaded region reaches that plate's roll band, and a dynamic
2706 rule (occlusion, feature-run contiguity, budget) rejected it: the one class where
2707 the flip is visually significant (`near_roll_fallback_reason` in
2708 `backend/window_runner.rs`, unit-tested). Deliberately a warning, NOT an assert:
2709 the audit established every rejection is conservative-correct — the render is
2710 right, it is the frame-to-frame look that flips — so a panic would crash debug
2711 builds on correct behavior. The ubiquitous accepted case (ordinary geometry
2712 already closed every grouping window) stays quiet by construction: no open
2713 enclosing plate remains for the check to run against.
2714
2715 - **Design note (2026-09-01) — the scroll-virtualization contract, and the ScrollRegion
2716 de-duplication sweep.** A class fix, recorded because the class outlived every
2717 individual sighting of it. `ScrollBox::get_item_draw_y` returned a row's position
2718 only when the row was FULLY inside the viewport, so callers drew nothing for a row
2719 straddling the edge — cards/rows visibly vanished mid-scroll. The helper predates
2720 the §3.4 clip stack (when there was no way to draw a row "cut", culling whole was
2721 the only option), and the contract then traveled: the struct around it was copied
2722 into cce-system-interface, and from there into cce-fonts, cce-mail, cce-cloud, and
2723 cce-layout-interface as each dissolved its List/ScrollBox embedded base (Phase 6q),
2724 plus reimplemented in cce-files' RowList — the multi-repo copy-drift failure mode,
2725 in widget form. The bug was then rediscovered and fixed **per app**
2726 (cce-system-interface first; cce-cloud@1063422 fixed only the CLICK half), which is
2727 exactly the cost the sweep exists to stop paying.
2728
2729 The sweep (one commit per repo, 2026-09-01): the fixed system-interface copy was
2730 lifted verbatim into **`widget::ScrollRegion`** (cce-ui@764d9e7) — the union of all
2731 the copies' APIs — so the contract lives in one place: **`get_item_draw_y`/
2732 `get_draw_y` return every row that INTERSECTS the viewport; callers draw those rows
2733 under a clip (the §3.4 stack, or exact per-quad clamping for flat pipelines), and
2734 hit-test the SAME partial rows the draw shows** — visible ⇒ clickable, culled ⇒
2735 not; a fix to only one half just mirrors the bug (1063422's blank-band click gate
2736 became the sliver-selects-correctly gate with no shape change once the draw side
2737 caught up). `ScrollBox::get_item_draw_y` itself moved to the intersection contract.
2738 Deliberately NOT migrated: TreeList's `get_row_rect` keeps full containment — it
2739 places a floating overlay that draws over the well unclipped, and an editor hanging
2740 half off the list edge is worse than one that waits — and TreeList's row-bottom
2741 separator gate, which is spatial (the separator would land outside the well), not a
2742 cull.
2743
2744 Two pipeline lessons from the app migrations, for anyone adding a scrolled list:
2745 the clip must survive to EVERY stage that renders row content, or partial rows
2746 bleed instead of cut. cce-mail's hand-emitted row labels ride a boundless labels
2747 drain — they drew whole into the menubar until given viewport bounds; cce-cloud's
2748 span assembly dropped the paint walk's merged clip on the floor (`bounds: None`)
2749 until it was threaded through to the glyph pass. And where widgets keep their full
2750 rect while drawing cut (cce-layout-interface's row buttons above other controls),
2751 the stored hit rect is re-clamped to the visible sliver after the visuals are
2752 recorded, so the hidden part cannot shadow what's beneath it. New scrolled lists
2753 build on `widget::ScrollRegion`; hand-copying it back into an app is how this
2754 class got six lives.
2755
2756 Order rationale: each phase is independently valuable and reversible, and no phase requires the
2757 next to compile. Phase 0 can land immediately regardless of the rest.
2758
2759 ---
2760
2761 ## 7. Quick wins to land first (Phase 0 detail)
2762
2763 1. **Breadcrumb black rectangle — DONE.** `cce-files` `BrowseContainer::set_rect` now positions
2764 the breadcrumb to exactly match `browse::view`'s layout (inset by the page margin, reserving
2765 the dropdown width) so the container's duplicate paint sits fully behind the page copy
2766 instead of leaking a dark strip. This is a stop-gap; the real fix is the single paint path in
2767 Phase 3 (the breadcrumb is still painted twice — the copies now just coincide).
2768 2. **Hot-path debug I/O — DONE.** Removed the per-frame `eprintln!` in `render()` that
2769 reconstructed every text area's string via `layout_runs()`.
2770 3. **Dead code — DONE.** Removed `UiContext::tick_hover`/`get_hover_quad` (zero callers
2771 workspace-wide), the dead duplicate of `hover_animation`.
2772 4. **GPU scissor — DEFERRED to Phase 3.** Threading clip rects to `render_pass.set_scissor_rect`
2773 is not actually a "quick win": clip rects are computed CPU-side and folded into geometry
2774 today, with nothing carried to the render pass. Doing it properly needs the clip stack from
2775 §3.4, so it lands with the paint-pass rework rather than as a risky standalone change.
2776
2777 ---
2778
2779 ## 8. Risks & mitigations
2780
2781 - **Migration surface across ~19 apps.** Mitigation: adapter shim + per-app Phase 6; the core
2782 lands and is validated before any app is forced across.
2783 - **Borrow-checker friction with an arena tree.** Mitigation: layout operates on
2784 `style`/`layout_out`, not the `widget` payload; `get_disjoint_mut` for the rare dual-borrow.
2785 - **Hand-rolled layout correctness.** Grow/shrink/wrap/alignment are subtle. Mitigation: keep
2786 the box model small (row/column + flex + align + gap/padding only), and unit-test the solver
2787 in isolation — it operates on `Style`/`Size`, independent of paint, so it is directly testable.
2788 - **Effort.** This is multi-week. Phasing keeps every intermediate state shippable so it can be
2789 paused/resumed without a broken tree.
2790
2791 ---
2792
2793 ## 9. Deferred (designed-for, not built now)
2794
2795 - **Per-subtree geometry caching:** cache tessellated vertices per node, re-tessellate only
2796 dirty subtrees instead of the whole scene each frame. The arena `Dirty` flags are the hook;
2797 worth it only once scenes are large.
2798 - **Full affine transforms / rotation** beyond translate+scale.
2799 - **Damage-rect partial redraw** at the GPU level (currently full-surface clear each dirty frame).
2800
2801 ---
2802
2803 ## 10. Decisions (resolved 2026-07-07)
2804
2805 1. **Layout solver: hand-rolled** (not taffy). Compact measure/arrange engine owned in
2806 `cce-ui`, scoped to the DE's box model. See §3.2.
2807 2. **A new shared dependency in `cce-ui` is acceptable** when needed (one shared path dep does
2808 not break standalone builds). Note the layout decision means no layout dep is required.
2809 3. **Phase 0 quick wins land now**, as separate commits ahead of the rebuild. See §7.