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