GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(scene): paint walk carries per-widget fonts and clip rects (Phase 6d)
Adapted::paint_self no longer forwards Paint::paint's plain Text prims.
It re-emits the geometry verbatim (through the ctx, so the walk's
offset/clip apply exactly once) and serves text as text_with prims from
the same views the standard text bridges use: own_labels_with_font_and_
bounds (prim text + detached base label, widget_font, text_bounds or the
scroll-ancestor clip), or the 5s per-label hatch verbatim for hatched
widgets. A paint_tree display list's text is now renderable-correct for
migrated widgets — the precondition for the display_list() adopters
flipping display_list_text.
Recorded on the way: the legacy Element::paint_self default drains the
child-aggregating text_labels, so scene-path text double-emits for trees
still containing embedded-base containers — invisible while text prims
go unrendered, but it gates the flag flip on an embedded-base-free tree
(RFC 6d note).
Verified: 179 tests (new: paint-walk font/bounds attachment); cce-graph
live A/B zero structural diff (all residual under the 8% translucency
noise amplitude).
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MjP3pGQEDLkJbV5WEmYBe
docs/rfc-core-rebuild.md | 26 ++++++++++++++--
src/widget/model.rs | 80 +++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 97 insertions(+), 9 deletions(-)
diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index f30ab7a..21d4b44 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -776,9 +776,31 @@ Constraint respected: **each crate still builds standalone** — the new core is
`FontSystem`, the `TextItem` cache, `rebuild_layout`, and the scale/rebuild bookkeeping.
Non-interactive, so no event surface. This is the reference shape for a minimal Phase 6
app.
- - **Still to do (per-app, roughly smallest-first):** wallpaper/screenaver (likely trivial),
+ - **6c — `cce-wallpaper` + `cce-screenaver` across; `display_list` gains `(size, scale)`.
+ DONE (wallpaper live-A/B AE=0; screensaver background-fill verified live, sim quads are
+ the same mechanical loop).** The Phase 6 frame entry point now receives the frame's
+ logical size and HiDPI scale like `view` did (fullscreen apps size geometry from it);
+ mechanical sweep across the eight implementors. Both apps' dead `TextItem` caches
+ deleted.
+ - **6d — the paint walk carries per-widget fonts + clip rects. DONE (179 tests; cce-graph
+ live A/B shows zero structural diff — all residual below the 8% translucency-noise
+ amplitude).** `Adapted::paint_self` no longer forwards `Paint::paint`'s plain Text prims:
+ it re-emits the geometry verbatim (through the ctx so the walk's offset/clip apply
+ once) and serves text as `text_with` prims from the SAME views the standard text bridges
+ use — `own_labels_with_font_and_bounds` (prim text + detached base label, `widget_font`,
+ `text_bounds` or the scroll-ancestor clip) or the 5s per-label hatch verbatim (caveat
+ noted in-code: the hatch contract includes raw container children). This makes a
+ `paint_tree` display list's text renderable-correct for migrated widgets, which is the
+ precondition for the seven adopters flipping `display_list_text`. Found and recorded on
+ the way: the LEGACY `Element::paint_self` default drains the child-aggregating
+ `text_labels` for legacy containers, so scene-path text double-emits under the walk for
+ trees that still contain Layer/Page/etc. — invisible today (text prims unrendered
+ without the opt-in), but it means an app can only flip `display_list_text` once its
+ tree is embedded-base-free. Consistent with the dissolution plan; revisit per app.
+ - **Still to do (per-app, roughly smallest-first):**
the seven display_list() adopters (flip `display_list_text` + drop their TextItem
- assembly, one at a time, each A/B'd), then the widget-tree apps (routed events + scene
+ assembly, one at a time, each A/B'd — precondition: embedded-base-free tree, see 6d),
+ then the widget-tree apps (routed events + scene
layout + dissolving the embedded-base containers), the demo (`cce-ui/src/main.rs`) as the
reference `Application`, and popover-occlusion for display-list text before any app with
popovers flips the flag. Delete the legacy `view*`/`text_items` paths, the per-widget
diff --git a/src/widget/model.rs b/src/widget/model.rs
index e12c966..c7ea4a4 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1198,8 +1198,26 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
fn widget_font(&self) -> Option<String> {
Paint::widget_font(&self.inner)
}
+ /// Scene-path emission. Geometry comes from [`Paint::paint`]; its plain `Text` prims are
+ /// REPLACED by the same font+bounds view the standard text bridges serve
+ /// (`own_labels_with_font_and_bounds`, or the per-label hatch), so a display list built by
+ /// the paint walk carries per-widget fonts and clip rects (Phase 6 — text ordering
+ /// relative to geometry is immaterial: glyphs always render in the later text pass).
fn paint_self(&self, ui: &UiContext, ctx: &mut PaintCtx) {
- Paint::paint(&self.inner, self.content_rect(), ctx);
+ let mut tmp = PaintCtx::new();
+ Paint::paint(&self.inner, self.content_rect(), &mut tmp);
+ for item in tmp.finish().items {
+ match item.prim {
+ Prim::Text { .. } => {}
+ Prim::Quad { rect, color } => ctx.quad(rect, color),
+ Prim::RoundedRect { rect, radius, corners, color } => ctx.rounded_rect(rect, radius, corners, color),
+ Prim::Border { rect, radii, fill, border, thickness } => ctx.border(rect, radii, fill, border, thickness),
+ Prim::Bevel { rect, radii, color, depth } => ctx.bevel(rect, radii, color, depth),
+ Prim::Arc { cx, cy, radius, thickness, start, end, color } => ctx.arc(cx, cy, radius, thickness, start, end, color),
+ Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => ctx.vector(x1, y1, x2, y2, thickness, color, cap),
+ Prim::Circle { cx, cy, radius, color } => ctx.circle(cx, cy, radius, color),
+ }
+ }
// The legacy default `paint_self` drained `all_quads`, which carries the focus
// highlight — replicate for opt-in widgets, over the background (same draw order).
// Forwarded highlights stay out: the paint walk reaches the owning child itself.
@@ -1210,12 +1228,17 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
}
}
}
- // Inline-label widgets emit their own text in `paint`; detached labels come from the
- // base, exactly as the legacy default `paint_self` emits them.
- if !Layout::inline_label(&self.inner) {
- for tl in self.base_label_fallback() {
- ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
- }
+ // Own text with per-label font+bounds: the hatch view verbatim for hatched widgets
+ // (caveat: its contract includes raw container children — those few widgets keep the
+ // hatch until their hosts adopt the walk), else the standard own-labels bridge (prim
+ // text + the detached base label, one font, text_bounds or the scroll-ancestor clip).
+ let labels = if Paint::serves_legacy_labels(&self.inner) {
+ Paint::legacy_labels_with_font_and_bounds(&self.inner, self.content_rect(), ui)
+ } else {
+ self.own_labels_with_font_and_bounds(ui)
+ };
+ for (tl, font, bounds) in labels {
+ ctx.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
}
}
@@ -1912,4 +1935,47 @@ mod tests {
assert!(elem.as_menu_controller().is_none());
assert!(elem.as_graph_controller().is_none());
}
+ /// Phase 6: the paint walk's text prims carry the widget's font and clip rect (what the
+ /// display-list text path renders), not the bare `Paint::paint` text.
+ #[test]
+ fn paint_walk_text_carries_font_and_bounds() {
+ struct Tag;
+ impl Layout for Tag {}
+ impl Paint for Tag {
+ fn color(&self) -> [f32; 4] {
+ [0.0; 4]
+ }
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ ctx.text("hi", rect.x + 2.0, rect.y + 2.0, 12.0, [1, 2, 3]);
+ }
+ fn widget_font(&self) -> Option<String> {
+ Some("Mono:12".into())
+ }
+ fn text_bounds(&self, rect: Rect) -> Option<[f32; 4]> {
+ Some([rect.x, rect.y, rect.x + rect.width, rect.y + rect.height])
+ }
+ }
+ impl Input for Tag {}
+
+ let mut ctx = UiContext::new();
+ let mut w = Box::new(Adapted::new(Tag));
+ let (id, ptr) = (w.id(), w.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ unsafe { (*ptr).set_rect(10.0, 20.0, 100.0, 30.0) };
+
+ let list = paint_tree(&ctx, ptr);
+ let texts: Vec<_> = list
+ .items
+ .iter()
+ .filter_map(|it| match &it.prim {
+ Prim::Text { text, font, bounds, .. } => Some((text.clone(), font.clone(), *bounds)),
+ _ => None,
+ })
+ .collect();
+ assert_eq!(texts.len(), 1, "one text prim, no plain duplicate");
+ assert_eq!(texts[0].0, "hi");
+ assert_eq!(texts[0].1.as_deref(), Some("Mono:12"), "widget_font attached");
+ assert_eq!(texts[0].2, Some([10.0, 20.0, 110.0, 50.0]), "text_bounds attached");
+ }
+
}