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

commita78f0b2d6ae0a20ee7fe5e5e92e44605b249fc8f
parent80783b2185
authorLucas Galante <[email protected]>
date2026-07-12 18:44
refactor(widget)!: second Element shrink batch — 4 methods off (6bd)

Element 73 -> 69, census-driven toward the WidgetHost blueprint:

- `hovered`/`set_hovered` deleted: the state is the base `Widget::hovered`
  flag, now read/written directly by the `cursor_moved`/`on_cursor_moved`/
  `highlight_quad` defaults and `serialize.rs`. Button/Checkbox keep their
  inherent accessors for immediate-mode hosts (cloud json_layout resolves
  to those already).
- `corner_radius` + `rounded_corners` replaced by ONE `corner_style()
  -> (f32, (bool,bool,bool,bool))` (mirrors the narrow `Paint::corner_style`).
  NOT folded into `corner_radii`: the radius is meaningful with all corners
  off — Menu/StatusBar report their parent's radius to children through the
  flags-off channel, and breadcrumb can be flags-true/radius-0.
- `clear_children` moved to an inherent `Adapted<W>` method (every caller
  is a concrete Adapted field in cce-files).

Drive-by: two pre-existing unused-mut warnings in slider.rs closures.

Verified: 163 tests pass; workspace builds; settings audio render stream
byte-identical; cce-files A/B AE=0; TI gallery empty 8% amplitude mask.

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

 src/layout.rs                    |  8 ++----
 src/scene/painter.rs             |  7 ++---
 src/widget/container/menu.rs     |  2 +-
 src/widget/display/serialize.rs  |  2 +-
 src/widget/display/status_bar.rs |  4 +--
 src/widget/input/slider.rs       |  4 +--
 src/widget/input/text_box.rs     |  2 +-
 src/widget/mod.rs                | 56 +++++++++++++++++-----------------------
 src/widget/model.rs              | 26 +++++++++----------
 9 files changed, 47 insertions(+), 64 deletions(-)

diff --git a/src/layout.rs b/src/layout.rs
index 1944aa4..30be521 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -3147,12 +3147,8 @@ pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T,
         ctx.register_widget(w_id, w.as_ptr_mut());
     }
     w.layout(crate::widget::Point { x, y }, crate::widget::LayoutConstraints::new(ww, ww, wh, wh), ctx);
-    let corners = w.rounded_corners();
-    let r = if corners != (false, false, false, false) {
-        w.corner_radius()
-    } else {
-        0.0
-    };
+    let (style_r, corners) = w.corner_style();
+    let r = if corners != (false, false, false, false) { style_r } else { 0.0 };
     let (wx, mut wy, www, mut whh) = w.rect();
     let top_room = crate::widget::label_offset(w);
     wy += top_room;
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index ced2d4a..be4e5ad 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -337,11 +337,8 @@ mod tests {
             fn color(&self) -> [f32; 4] {
                 [0.2, 0.4, 0.6, 1.0]
             }
-            fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-                (true, true, true, true)
-            }
-            fn corner_radius(&self) -> f32 {
-                4.0
+            fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
+                (4.0, (true, true, true, true))
             }
         }
         let mut ctx = UiContext::new();
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 3104fd6..e22589b 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -464,7 +464,7 @@ impl Paint for MenuBar {
         // Corners never round (the backplate-adjacency source is gone); the radius is still
         // reported for children that read it through the parent pointer.
         let radius = match self.parent {
-            Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+            Some(p_ptr) => unsafe { (*p_ptr).corner_style().0 },
             None => 0.0,
         };
         Some((radius, (false, false, false, false)))
diff --git a/src/widget/display/serialize.rs b/src/widget/display/serialize.rs
index 3ed1ceb..a12ee5c 100644
--- a/src/widget/display/serialize.rs
+++ b/src/widget/display/serialize.rs
@@ -4,7 +4,7 @@ fn serialize_single_widget(w: &dyn Element, json: &mut String) {
     let (x, y, width, height) = w.rect();
     let label = w.label().or_else(|| w.base().and_then(|b| b.label.clone())).unwrap_or_default();
     let focused = w.base().map_or(false, |b| b.focused);
-    let hovered = w.hovered();
+    let hovered = w.base().map_or(false, |b| b.hovered);
     let value = w.value();
     let type_name = w.type_name();
 
diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index 62f8fc7..037a8ce 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -113,7 +113,7 @@ impl Paint for StatusBar {
         // Corners never round (the backplate-adjacency source is gone); the radius is still
         // reported for children that read it through the parent pointer.
         let radius = match self.parent {
-            Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+            Some(p_ptr) => unsafe { (*p_ptr).corner_style().0 },
             None => 0.0,
         };
         Some((radius, (false, false, false, false)))
@@ -196,7 +196,7 @@ mod tests {
         // Parentless: cornerless plain bg through the plain-quad bridge, at STATUS_BG.
         let extra = Element::extra_quads(&bar);
         assert_eq!(extra.len(), 1, "cornerless bg quad");
-        assert_eq!(Element::rounded_corners(&bar), (false, false, false, false));
+        assert_eq!(Element::corner_style(&bar).1, (false, false, false, false));
         assert!(!Element::blocks_backplate_drag(&bar));
     }
 }
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 85af3a2..e3bbdc7 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -216,7 +216,7 @@ impl Paint for Slider {
         let radius = crate::layout::slider_corner_radius();
         let rounded = radius > 0.0;
         let rc = (rounded, rounded, rounded, rounded);
-        let mut rrect = |r: Rect, rad: f32, corners: (bool, bool, bool, bool), c: [f32; 4], ctx: &mut PaintCtx| {
+        let rrect = |r: Rect, rad: f32, corners: (bool, bool, bool, bool), c: [f32; 4], ctx: &mut PaintCtx| {
             if rounded {
                 ctx.rounded_rect(r, rad, corners, c);
             } else {
@@ -534,7 +534,7 @@ impl Paint for RangeSlider {
             colors::rangeslider_thumb()
         };
 
-        let mut rrect = |r: Rect, rad: f32, corners: (bool, bool, bool, bool), c: [f32; 4], ctx: &mut PaintCtx| {
+        let rrect = |r: Rect, rad: f32, corners: (bool, bool, bool, bool), c: [f32; 4], ctx: &mut PaintCtx| {
             if rounded {
                 ctx.rounded_rect(r, rad, corners, c);
             } else {
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 145ed06..f06f4a2 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -1646,7 +1646,7 @@ mod tests {
         tb.select_anchor = Some(7); // starts at "Line 2"
         tb.cursor_idx = 13;        // ends at end of "Line 2"
 
-        let has_rounded = tb.rounded_corners() != (false, false, false, false);
+        let has_rounded = Element::corner_style(&tb).1 != (false, false, false, false);
         let has_highlight = if has_rounded {
             let rounded = tb.all_rounded_quads(&dummy);
             println!("Rounded quads: {:?}", rounded);
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 11dcf84..ad60b09 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -338,9 +338,11 @@ pub trait Element {
     fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         ctx.set_cursor_pos(px, py);
         if ctx.is_coordinate_covered(self.base().map(|b| b.id()).unwrap_or(WidgetId(0)), px, py) {
-            let was = self.hovered();
+            let was = self.base().map_or(false, |b| b.hovered);
             if was {
-                self.set_hovered(false);
+                if let Some(b) = self.base_mut() {
+                    b.hovered = false;
+                }
                 self.handle_event(&Event::MouseLeave, ctx);
             }
             return was;
@@ -350,9 +352,11 @@ pub trait Element {
 
     fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         if self.base().is_some() {
-            let was = self.hovered();
+            let was = self.base().map_or(false, |b| b.hovered);
             let is_hit = self.hit_test(px, py, ctx);
-            self.set_hovered(is_hit);
+            if let Some(b) = self.base_mut() {
+                b.hovered = is_hit;
+            }
             if was != is_hit {
                 if is_hit {
                     self.handle_event(&Event::MouseEnter, ctx);
@@ -371,26 +375,16 @@ pub trait Element {
     fn mouse_input(&mut self, _button: MouseButton, _state: ElementState, _px: f32, _py: f32, _ctx: &mut UiContext) -> bool { false }
     fn mouse_wheel(&mut self, _delta: &MouseScrollDelta, _px: f32, _py: f32, _ctx: &mut UiContext) -> bool { false }
 
-    fn set_hovered(&mut self, hovered: bool) {
-        if let Some(b) = self.base_mut() {
-            b.hovered = hovered;
-        }
-    }
-
-    fn hovered(&self) -> bool {
-        if let Some(b) = self.base() {
-            b.hovered
-        } else {
-            false
-        }
-    }
+    // `hovered`/`set_hovered` are GONE from the trait (6bd batch 2): the state is the base
+    // `Widget::hovered` flag, read/written directly by the defaults above; Button/Checkbox
+    // keep inherent accessors for immediate-mode hosts.
 
     fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
         // Focus/hover highlight color, folded from the zero-override `highlight_color` (6bd).
         let is_focused = self.base().map(|b| ctx.is_focused_id(b.id())).unwrap_or(false);
         let hc = if is_focused {
             colors::highlight_primary_color()
-        } else if self.hovered() {
+        } else if self.base().map_or(false, |b| b.hovered) {
             colors::HIGHLIGHT_SECONDARY
         } else {
             return None;
@@ -473,9 +467,9 @@ pub trait Element {
             } 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();
+                let (radius, (r1, r2, r3, r4)) = self.corner_style();
                 if r1 || r2 || r3 || r4 {
-                    ctx.rounded_rect(rect, self.corner_radius(), (r1, r2, r3, r4), color);
+                    ctx.rounded_rect(rect, radius, (r1, r2, r3, r4), color);
                 }
             }
         }
@@ -513,10 +507,9 @@ pub trait Element {
             return Vec::new();
         }
         let mut quads = Vec::new();
-        let (r1, r2, r3, r4) = self.rounded_corners();
+        let (radius, (r1, r2, r3, r4)) = self.corner_style();
         if r1 || r2 || r3 || r4 {
             let (x, y, w, h) = self.rect();
-            let radius = self.corner_radius();
             let c = self.color();
             if c[3].abs() > 0.001 {
                 quads.push((x, y, w, h, radius, c, (r1, r2, r3, r4)));
@@ -618,23 +611,20 @@ pub trait Element {
         }
     }
 
-    fn clear_children(&mut self, ctx: &mut UiContext) {
-        if let Some(base) = self.base() {
-            let id = base.id();
-            ctx.clear_children_ids(id);
-        }
-    }
-
     fn z_index(&self) -> i32 { 0 }
     fn is_scrollable(&self) -> bool { false }
     fn blocks_backplate_drag(&self) -> bool { true }
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) { (false, false, false, false) }
 
-    fn corner_radius(&self) -> f32 { 12.0 }
+    /// Uniform corner radius + per-corner on-flags, in one read (6bd batch 2 — replaced the
+    /// separate `corner_radius`/`rounded_corners` getters). The radius is meaningful even with
+    /// every corner off: Menu/StatusBar report their parent's radius to children this way, so
+    /// the flags-off channel can't be folded into `corner_radii`.
+    fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
+        (12.0, (false, false, false, false))
+    }
 
     fn corner_radii(&self) -> CornerRadii {
-        let r = self.corner_radius();
-        let (tl, tr, br, bl) = self.rounded_corners();
+        let (r, (tl, tr, br, bl)) = self.corner_style();
         CornerRadii::new(
             if tl { r } else { 0.0 },
             if tr { r } else { 0.0 },
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 30fc566..6804914 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -696,6 +696,12 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
         Input::set_drag_bounds(&mut self.inner, bx, by, bw, bh)
     }
 
+    /// Unlink all tree children (off `Element` in 6bd batch 2 — every caller is a concrete
+    /// `Adapted` field).
+    pub fn clear_children(&mut self, ctx: &mut UiContext) {
+        ctx.clear_children_ids(self.base.id());
+    }
+
     /// The model's intrinsic content size (off the `Element` trait since 6bd — the concrete
     /// callers are fonts'/graph's hand-laid button/dropdown sizing).
     pub fn intrinsic_size(&self) -> Option<Size> {
@@ -901,10 +907,6 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
         }
     }
 
-    fn clear_children(&mut self, ctx: &mut UiContext) {
-        ctx.clear_children_ids(self.base.id());
-    }
-
     fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, ctx: &mut UiContext) {
         Layout::parent_changed(&mut self.inner, parent);
         // Replica of the Element default: symmetric tree link.
@@ -1094,12 +1096,10 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
     fn clips_children(&self) -> bool {
         Paint::clips_children(&self.inner)
     }
-    fn corner_radius(&self) -> f32 {
-        // 12.0 mirrors the `Element` default for widgets without a corner style.
-        Paint::corner_style(&self.inner, self.content_rect()).map_or(12.0, |(r, _)| r)
-    }
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        Paint::corner_style(&self.inner, self.content_rect()).map_or((false, false, false, false), |(_, c)| c)
+    fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
+        // 12.0 / all-off mirrors the `Element` default for widgets without a corner style.
+        Paint::corner_style(&self.inner, self.content_rect())
+            .unwrap_or((12.0, (false, false, false, false)))
     }
     fn solid_border(&self) -> Option<([f32; 4], f32)> {
         Paint::solid_border(&self.inner)
@@ -1249,7 +1249,7 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
             for child in self.visible_children() {
                 let widget = unsafe { &*child };
                 let (wx, wy, ww, wh) = widget.rect();
-                let has_rounded = widget.rounded_corners() != (false, false, false, false);
+                let has_rounded = widget.corner_style().1 != (false, false, false, false);
                 for (qx, qy, qw, qh, qc) in widget.all_quads(ctx) {
                     if has_rounded
                         && (qx - wx).abs() < 0.1
@@ -1708,10 +1708,10 @@ mod tests {
         // preserves) and sets the base hover flag; moving away synthesizes MouseLeave.
         ctx.propagate_event(&Event::PointerMove { x: 20.0, y: 15.0, local_x: 20.0, local_y: 15.0 }, ptr);
         assert_eq!(w.inner().entered, 1, "MouseEnter reached on_event");
-        assert!(unsafe { (*ptr).hovered() }, "base hover flag set through the adapter");
+        assert!(unsafe { (*ptr).base().map_or(false, |b| b.hovered) }, "base hover flag set through the adapter");
         ctx.propagate_event(&Event::PointerMove { x: 200.0, y: 200.0, local_x: 200.0, local_y: 200.0 }, ptr);
         assert_eq!(w.inner().left, 1, "MouseLeave reached on_event");
-        assert!(!unsafe { (*ptr).hovered() }, "base hover flag cleared");
+        assert!(!unsafe { (*ptr).base().map_or(false, |b| b.hovered) }, "base hover flag cleared");
     }
 
     /// A narrow widget that is also a controller: the controller trait is reached through the