GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(scene): walk text for legacy containers and subtree painters (6d trap fix)
Two changes that make a paint_tree display list's text emit exactly once:
- The legacy Element::paint_self default emits text only for LEAVES. The
legacy container text_labels overrides (Backplate/Layer/Page/SplitBox/
Plate) AGGREGATE their children's labels, and the walk reaches those
children itself, so emitting the aggregate double-drew every
descendant's text — the recorded Phase 6d trap. A legacy container
with OWN text overrides paint_self: Plate now does, serving its label
(plain text, matching the legacy widget_font: None) over geometry
identical to the default.
- The walk's renders_own_subtree branch (TreeList) now emits the
subtree's text from the recursive bounded getter
(text_labels_with_font_and_bounds) — the walk never descends into such
subtrees, so the aggregate is that subtree's text, emitted once, with
the same fonts/bounds the legacy tuple pipeline served.
Both are inert until a paint_tree app flips display_list_text.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016MjP3pGQEDLkJbV5WEmYBe
src/scene/painter.rs | 7 ++++++-
src/widget/container/plate.rs | 47 +++++++++++++++++++++++++++++++++++++++++++
src/widget/mod.rs | 11 ++++++++--
3 files changed, 62 insertions(+), 3 deletions(-)
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index ba27589..dda9895 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -45,7 +45,9 @@ fn paint_node(ui: &UiContext, ptr: ElemPtr, pc: &mut PaintCtx) {
}
// Legacy subtree painters (e.g. TreeList) render their own geometry AND their children via
- // a recursive all_rounded_quads/all_quads; emit those directly and stop descending.
+ // a recursive all_rounded_quads/all_quads; emit those directly and stop descending. Their
+ // text comes from the recursive bounded getter for the same reason — the walk never
+ // reaches the subtree's widgets, so the aggregate is the subtree's text, emitted once.
if (*ptr).renders_own_subtree() {
for (x, y, w, h, r, c, corners) in (*ptr).all_rounded_quads(ui) {
pc.rounded_rect(Rect { x, y, width: w, height: h }, r, corners, c);
@@ -59,6 +61,9 @@ fn paint_node(ui: &UiContext, ptr: ElemPtr, pc: &mut PaintCtx) {
for (cx, cy, r, c) in (*ptr).extra_circles() {
pc.circle(cx, cy, r, c);
}
+ for (tl, font, bounds) in (*ptr).text_labels_with_font_and_bounds(ui) {
+ pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
+ }
return;
}
diff --git a/src/widget/container/plate.rs b/src/widget/container/plate.rs
index a505982..16c6cc1 100644
--- a/src/widget/container/plate.rs
+++ b/src/widget/container/plate.rs
@@ -143,6 +143,53 @@ impl Element for Plate {
self.selected = selected;
}
+ /// Walk emission: the legacy default now emits NO text for containers (Plate's
+ /// `text_labels` aggregates children, which the walk reaches itself), but a Plate's OWN
+ /// label lives here — geometry identical to the default, plus that one label (plain text,
+ /// matching the legacy `widget_font: None`).
+ fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) {
+ use crate::scene::layout::Rect;
+ let (x, y, w, h) = self.rect();
+ let rect = Rect { x, y, width: w, height: h };
+ let color = Element::color(self);
+
+ if self.children(ui).is_empty() {
+ for (qx, qy, qw, qh, r, c, corners) in self.all_rounded_quads(ui) {
+ ctx.rounded_rect(Rect { x: qx, y: qy, width: qw, height: qh }, r, corners, c);
+ }
+ } else {
+ let cr = self.corner_radii();
+ let radii = (cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left);
+ if let Some(depth) = self.plate_bevel() {
+ ctx.bevel(rect, radii, color, depth);
+ } else if let Some((border_color, thickness)) = Element::solid_border(self) {
+ ctx.border(rect, radii, color, border_color, thickness);
+ } else if color[3].abs() > 0.001 {
+ let (r1, r2, r3, r4) = Element::rounded_corners(self);
+ if r1 || r2 || r3 || r4 {
+ ctx.rounded_rect(rect, Element::corner_radius(self), (r1, r2, r3, r4), color);
+ }
+ }
+ }
+
+ for (qx, qy, qw, qh, c) in self.all_quads(ui) {
+ ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, c);
+ }
+ for (cx, cy, r, t, start, end, c) in self.extra_arcs() {
+ ctx.arc(cx, cy, r, t, start, end, c);
+ }
+ for (cx, cy, r, c) in self.extra_circles() {
+ ctx.circle(cx, cy, r, c);
+ }
+
+ if self.visible {
+ if let Some(ref label) = self.base.base.label {
+ let (_, font_size) = crate::layout::control_label_font_parsed();
+ ctx.text(label.clone(), self.base.base.x, self.base.base.y, font_size, colors::control_label_color_u8());
+ }
+ }
+ }
+
fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
for &child_ptr in &self.base.children {
unsafe {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index eddfa22..7876a23 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -519,8 +519,15 @@ pub trait Element {
for (cx, cy, r, c) in self.extra_circles() {
ctx.circle(cx, cy, r, c);
}
- for tl in self.text_labels() {
- ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+ // Text: leaves emit their own labels; containers emit NONE — the legacy container
+ // text_labels overrides (Backplate/Layer/Page/SplitBox/Plate) AGGREGATE their
+ // children's labels, and the walk reaches those children itself, so emitting the
+ // aggregate here would double-draw every descendant's text (the Phase 6d trap). A
+ // legacy container with OWN text overrides paint_self to add it (Plate's label).
+ if self.children(ui).is_empty() {
+ for tl in self.text_labels() {
+ ctx.text(tl.text, tl.x, tl.y, tl.font_size, tl.color);
+ }
}
}