GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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);
}
}
}