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

commit5bc009637f01482a87dda94a22104a8402819505
parent6be8bc3681
authorLucas Galante <[email protected]>
date2026-07-11 16:04
refactor(scene): consolidate the walk's text-getter use to one fonted default (Phase 6aq)

- painter.rs: the renders_own_subtree branch is now just paint_self + no
  descent — TreeList and JsonLayout carry paint_self overrides emitting
  their own recursive aggregates (geometry + bounded text). JsonLayout is
  newly marked renders_own_subtree: descending it would draw inactive
  pages' widgets and miss the checkbox side-labels.
- mod.rs default paint_self: the leaf text drain moves from plain
  text_labels() to the FONTED getter (font + scroll-viewport bounds) — the
  same labels every legacy tuple consumer served; composites (Ramp) only
  aggregate their internal field widgets there. This is now the ONE
  remaining trait-getter use in the paint path; it is deleted together
  with the getters once every live legacy widget carries its own
  paint_self.
- Ramp: own control labels (Preset / Line Type / Value) extracted to an
  inherent helper shared by the fonted getter and a new paint_self — a
  container's own text is dropped by the walk's aggregate rule (the same
  class as Plate's label / display-manager's LoginCard).

Verified: data-editor loaded-tree A/B AE=0 (TreeList via paint_self);
designer + settings audio unchanged; cloud fuzzel + json standalone A/B
AE=0; test-interface AE=5 after its flip; 176 tests.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk

 src/scene/painter.rs             | 23 ++--------
 src/widget/container/treelist.rs | 22 +++++++++
 src/widget/input/ramp.rs         | 98 +++++++++++++++++++++++++++++-----------
 src/widget/json_layout.rs        | 27 +++++++++++
 src/widget/mod.rs                | 17 ++++---
 5 files changed, 136 insertions(+), 51 deletions(-)

diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index ae257f3..75214cc 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -71,26 +71,11 @@ fn paint_node(ui: &UiContext, ptr: ElemPtr, pc: &mut PaintCtx) {
             return;
         }
 
-        // 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. 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.
+        // Legacy subtree painters (e.g. TreeList) render their own geometry AND their children
+        // through their own recursive aggregates, exposed via a paint_self override (see
+        // TreeList::paint_self) — emit that and stop; the walk must not also descend.
         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);
-            }
-            for (x, y, w, h, c) in (*ptr).all_quads(ui) {
-                pc.quad(Rect { x, y, width: w, height: h }, c);
-            }
-            for (cx, cy, r, t, s, e, c) in (*ptr).extra_arcs() {
-                pc.arc(cx, cy, r, t, s, e, c);
-            }
-            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);
-            }
+            (*ptr).paint_self(ui, pc);
             return;
         }
 
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 7c5574c..8c9281c 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -1202,6 +1202,28 @@ impl Element for TreeList {
         true
     }
 
+    /// The whole subtree — geometry via the recursive `all_*` aggregates, text via the
+    /// recursive bounded getter — emitted here so the paint walk's `renders_own_subtree`
+    /// branch is just `paint_self` + no descent, with no trait-getter use left in the walk.
+    fn paint_self(&self, ui: &UiContext, pc: &mut crate::scene::paint::PaintCtx) {
+        use crate::scene::layout::Rect;
+        for (x, y, w, h, r, c, corners) in self.all_rounded_quads(ui) {
+            pc.rounded_rect(Rect { x, y, width: w, height: h }, r, corners, c);
+        }
+        for (x, y, w, h, c) in self.all_quads(ui) {
+            pc.quad(Rect { x, y, width: w, height: h }, c);
+        }
+        for (cx, cy, r, t, s, e, c) in self.extra_arcs() {
+            pc.arc(cx, cy, r, t, s, e, c);
+        }
+        for (cx, cy, r, c) in self.extra_circles() {
+            pc.circle(cx, cy, r, c);
+        }
+        for (tl, font, bounds) in self.text_labels_with_font_and_bounds(ui) {
+            pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
+        }
+    }
+
     fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
         let mut quads = Vec::new();
         let (r1, r2, r3, r4) = self.rounded_corners();
diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index 826a0cc..723a3a1 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -992,14 +992,80 @@ impl Element for Ramp {
     }
     
     fn text_labels_with_font_and_bounds(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        let mut labels: Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> = self
+            .own_control_labels()
+            .into_iter()
+            .map(|(l, f)| (l, f, None))
+            .collect();
+
+        labels.extend(self.preset_dropdown.text_labels_with_font_and_bounds(ctx));
+        labels.extend(self.line_type_dropdown.text_labels_with_font_and_bounds(ctx));
+        if self.selected_key_idx.is_some() {
+            labels.extend(self.val_slider.text_labels_with_font_and_bounds(ctx));
+            labels.extend(self.del_button.text_labels_with_font_and_bounds(ctx));
+        }
+        labels
+    }
+
+    /// The walk drops a container's own text (assumed child aggregate) — Ramp's control
+    /// labels are its OWN; the children (dropdowns/slider/button) are painted by the
+    /// walk's descent, so only the own labels are emitted here, over the default's
+    /// container geometry.
+    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 = self.color();
+        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)) = self.solid_border() {
+            ctx.border(rect, radii, color, border_color, thickness);
+        } else if color[3].abs() > 0.001 {
+            let (r1, r2, r3, r4) = self.rounded_corners();
+            if r1 || r2 || r3 || r4 {
+                ctx.rounded_rect(rect, self.corner_radius(), (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);
+        }
+        for (tl, font) in self.own_control_labels() {
+            ctx.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, None);
+        }
+    }
+    
+    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
+        let mut items = Vec::new();
+        items.extend(self.preset_dropdown.get_text_items());
+        items.extend(self.line_type_dropdown.get_text_items());
+        if self.selected_key_idx.is_some() {
+            items.extend(self.val_slider.get_text_items());
+            items.extend(self.del_button.get_text_items());
+        }
+        items
+    }
+}
+
+impl Ramp {
+    /// The ramp's OWN control labels (Preset / Line Type / Value-when-selected), each
+    /// with its control's font — shared by the legacy fonted getter and `paint_self`.
+    fn own_control_labels(&self) -> Vec<(TextLabel, Option<String>)> {
         let mut labels = Vec::new();
-        
+
         let track_x = self.base.x + 10.0;
         let track_w = self.base.w - 20.0;
         let h = self.base.h;
         let gh = (h - 70.0).max(30.0);
         let sy = self.base.y + gh + 15.0;
-        
+
         let gap = 10.0;
         let col_w = if self.selected_key_idx.is_some() {
             let del_w = 70.0;
@@ -1008,7 +1074,7 @@ impl Element for Ramp {
         } else {
             (track_w - gap) / 2.0
         };
-        
+
         let (_, font_size) = crate::layout::control_label_font_detached_parsed();
         let label_color = colors::control_label_color_detached_u8();
 
@@ -1021,9 +1087,8 @@ impl Element for Ramp {
                 color: label_color,
             },
             self.preset_dropdown.widget_font(),
-            None,
         ));
-        
+
         labels.push((
             TextLabel {
                 text: "Line Type".to_string(),
@@ -1033,9 +1098,8 @@ impl Element for Ramp {
                 color: label_color,
             },
             self.line_type_dropdown.widget_font(),
-            None,
         ));
-        
+
         if self.selected_key_idx.is_some() {
             labels.push((
                 TextLabel {
@@ -1046,29 +1110,11 @@ impl Element for Ramp {
                     color: label_color,
                 },
                 self.val_slider.widget_font(),
-                None,
             ));
         }
-        
-        labels.extend(self.preset_dropdown.text_labels_with_font_and_bounds(ctx));
-        labels.extend(self.line_type_dropdown.text_labels_with_font_and_bounds(ctx));
-        if self.selected_key_idx.is_some() {
-            labels.extend(self.val_slider.text_labels_with_font_and_bounds(ctx));
-            labels.extend(self.del_button.text_labels_with_font_and_bounds(ctx));
-        }
+
         labels
     }
-    
-    fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
-        let mut items = Vec::new();
-        items.extend(self.preset_dropdown.get_text_items());
-        items.extend(self.line_type_dropdown.get_text_items());
-        if self.selected_key_idx.is_some() {
-            items.extend(self.val_slider.get_text_items());
-            items.extend(self.del_button.get_text_items());
-        }
-        items
-    }
 }
 
 impl Drop for Ramp {
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index 1015165..058f438 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -614,4 +614,31 @@ impl Element for JsonLayoutWidget {
     fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
         self.widgets.iter().map(|w| w.widget.as_ptr()).collect()
     }
+
+    // JsonLayout renders its whole subtree itself: page-filtered aggregates plus the
+    // checkbox side-labels that belong to the container, not to any child widget. The
+    // paint walk must emit these once and not descend (descending would draw inactive
+    // pages' widgets and miss the side-labels).
+    fn renders_own_subtree(&self) -> bool {
+        true
+    }
+
+    fn paint_self(&self, ui: &UiContext, pc: &mut crate::scene::paint::PaintCtx) {
+        use crate::scene::layout::Rect;
+        for (x, y, w, h, r, c, corners) in self.all_rounded_quads(ui) {
+            pc.rounded_rect(Rect { x, y, width: w, height: h }, r, corners, c);
+        }
+        for (x, y, w, h, c) in self.all_quads(ui) {
+            pc.quad(Rect { x, y, width: w, height: h }, c);
+        }
+        for (cx, cy, r, t, s, e, c) in self.extra_arcs() {
+            pc.arc(cx, cy, r, t, s, e, c);
+        }
+        for (cx, cy, r, c) in self.extra_circles() {
+            pc.circle(cx, cy, r, c);
+        }
+        for (tl, bounds) in self.text_labels_with_bounds(ui) {
+            pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, None, bounds);
+        }
+    }
 }
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index aa390a2..8b148b9 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -520,13 +520,18 @@ pub trait Element {
             ctx.circle(cx, cy, r, c);
         }
         // 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).
+        // text_labels overrides (Backplate/Layer/Page/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).
+        // Leaves use the FONTED getter (font + scroll-viewport bounds) — the same labels
+        // every legacy tuple consumer served; composites like Ramp only aggregate their
+        // internal field widgets here. This is the ONE remaining trait-getter use in the
+        // paint path; it is deleted together with the getters once every live legacy
+        // widget carries its own paint_self.
         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);
+            for (tl, font, bounds) in self.text_labels_with_font_and_bounds(ui) {
+                ctx.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
             }
         }
     }