git.lucas.co / cce-gallery
widget gallery and compositor test bench

commit370be91645caa1e054b8acdb39da020e242792ba
parent069d57e4eb
authorLucas Galante <[email protected]>
date2026-07-12 10:00
refactor: gallery's four raw impls onto the narrow traits (Phase 6az part 6)

ControlPanel, Plate, SectionContainer, and Backplate convert to
Layout/Paint/Input in Adapted — cce-test-interface is raw-impl-free.
ControlPanel is the subtree-container shape: the old all_rounded_quads/
extra_quads/extra_arcs/popover_rect overrides survive verbatim as inherent
aggregates that paint composes (paints_own_subtree; dummy ctx — Adapted
children read nothing from it); the set_rect arrangement becomes
Layout::arrange_children with the adapter as the children's parent; the
scroll_box/children event routing moves to on_event (gates_presses off,
matching the ungated broadcast) and tick_ctx; drags ride the Input drag
surface. inline_label keeps the adapter's label machinery out of all four
(CP's rect was never label-inflated; Plate/SectionContainer paint their own
labels from a sync_label copy — the adapter base label still serves CP's
match-children-by-label arrangement). The two raw `as *mut ControlPanel`
casts become as_any downcasts (Adapted::as_any returns the inner model —
the raw cast would have silently pointed at the wrapper); by-value model
builders that can't cross Deref become constructor args (Plate blur) or
&mut setters (Backplate set_bevel).

Verified: gallery A/B AE=321 with an empty 8% mask; cross-build captures
after an identical panel wheel + click sequence structurally identical;
child-window (--child --type Ramp --backplate) A/B AE=288, empty mask.
PRE-EXISTING (baseline-reproduced): plain `--child` without `--type Ramp`
panics at the unconditional Ramp downcast (main.rs "child ramp widget").

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

 src/main.rs       |  23 +-
 src/ti_widgets.rs | 872 +++++++++++++++++++++++++++++-------------------------
 2 files changed, 476 insertions(+), 419 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 7aa5382..4956ceb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -270,10 +270,8 @@ impl State {
         }
         
         if !self.is_child {
-            let cp_ptr = self.widgets[51].as_ptr_mut();
-            let cp = unsafe { &mut *(cp_ptr as *mut ControlPanel) };
             let (x, y, w, h) = self.positions[51];
-            cp.set_rect(x, y, w, h);
+            self.widgets[51].set_rect(x, y, w, h);
 
             let sb_rect = self.widgets[2].rect();
             let ddh = cce_ui::layout::dropdown_height();
@@ -369,9 +367,9 @@ impl cce_ui::engine::Application for State {
                 None => "This is an active simulated window in the window manager.".to_string(),
             };
             let bg: Box<dyn Element> = if use_backplate {
-                let mut bp = Backplate::new(0.0, 0.0, c_w, c_h).with_movable(false);
+                let mut bp = Backplate::new(0.0, 0.0, c_w, c_h);
                 if border_bevel {
-                    bp = bp.with_bevel(true, border_width);
+                    bp.set_bevel(border_width);
                 }
                 Box::new(bp)
             } else {
@@ -468,7 +466,7 @@ impl cce_ui::engine::Application for State {
                 Box::new(Label::new("").with_font_size(12.0)), // 24
                 Box::new(Spinbox::new(400, 100, 2000, 10).with_label("Width")), // 25
                 Box::new(Spinbox::new(250, 100, 2000, 10).with_label("Height")), // 26
-                Box::new(Plate::new(0.0, 0.0, 190.0, 250.0).with_blur(true)), // 27
+                Box::new(Plate::new(0.0, 0.0, 190.0, 250.0, true)), // 27
                 Box::new(Label::new("Surface Info").with_font_size(12.0)), // 28
                 Box::new(Label::new(
                     "A standard application\n\
@@ -487,7 +485,7 @@ cascades in cce."
                 Box::new(Button::new(0.0, 0.0, 180.0, 40.0).with_label("Save File Dialog")), // 35
                 
                 Box::new(TextBox::new("Interactive TextBox".to_string())), // 36
-                Box::new(Plate::new(0.0, 0.0, 120.0, 120.0).with_label("Plate").with_blur(true)), // 37
+                Box::new(Plate::new(0.0, 0.0, 120.0, 120.0, true).with_label("Plate")), // 37
                 Box::new({
                     let mut t = Toggle::new().with_label("Backplate");
                     t.set_toggled(true);
@@ -580,10 +578,13 @@ cascades in cce."
         };
 
         if !is_child {
-            let cp_ptr = state.widgets[51].as_ptr_mut();
-            let cp = unsafe { &mut *(cp_ptr as *mut ControlPanel) };
-            for idx in [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48] {
-                cp.add_child(state.widgets[idx].as_ptr_mut());
+            let child_ptrs: Vec<*mut (dyn Element + 'static)> = [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48]
+                .iter()
+                .map(|&idx| state.widgets[idx].as_ptr_mut())
+                .collect();
+            let cp = state.widgets[51].as_any_mut().downcast_mut::<ControlPanel>().expect("widget 51 must be a ControlPanel");
+            for ptr in child_ptrs {
+                cp.add_child(ptr);
             }
 
             // Set page selector (46) parent to StatusBar (2)
diff --git a/src/ti_widgets.rs b/src/ti_widgets.rs
index eff943c..06d6aa1 100644
--- a/src/ti_widgets.rs
+++ b/src/ti_widgets.rs
@@ -3,31 +3,39 @@
 //! Backplate, and it needs them only as demo chrome — the panel verbatim, the other
 //! three as passive lookalikes replicating the legacy types' exact emission surfaces
 //! (color rules, corner radius/rounding, borders, separators, labels, hit shapes).
+//! Phase 6az: all four are on the narrow traits wrapped in `Adapted<W>`; the gallery
+//! roster keeps them as `Box<dyn Element>` and reaches the models through `as_any`.
 
 use cce_ui::colors;
+use cce_ui::scene::layout::Rect;
+use cce_ui::scene::paint::PaintCtx;
 use cce_ui::widget::*;
 use cce_ui::widget::ScrollBox;
 
 pub struct ControlPanel {
-    pub base: Widget,
+    x: f32,
+    y: f32,
+    w: f32,
+    h: f32,
     pub children: Vec<*mut (dyn Element + 'static)>,
-    pub parent: Option<*mut (dyn Element + 'static)>,
     pub scroll_box: ScrollBox,
     pub active_drag_widget: Option<*mut (dyn Element + 'static)>,
 }
 
 impl ControlPanel {
-    pub fn new() -> Self {
+    pub fn new() -> Adapted<ControlPanel> {
         let mut sb = ScrollBox::new();
         sb.show_border = false;
         sb.show_background = false;
-        Self {
-            base: Widget::new(),
+        Adapted::new(Self {
+            x: 0.0,
+            y: 0.0,
+            w: 0.0,
+            h: 0.0,
             children: Vec::new(),
-            parent: None,
             scroll_box: sb,
             active_drag_widget: None,
-        }
+        })
     }
 
     pub fn add_child(&mut self, child: *mut (dyn Element + 'static)) {
@@ -35,83 +43,254 @@ impl ControlPanel {
         self.scroll_box.children.push(child);
     }
 
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
+    /// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned`.
+    fn rect(&self) -> (f32, f32, f32, f32) {
+        (self.x, self.y, self.w, self.h)
     }
-}
 
-impl Element for ControlPanel {
-    cce_ui::impl_widget_base!(ControlPanel);
-
-    fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
-        if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
-            return false;
+    /// First open child popover, in scrolled (screen) coordinates — the old
+    /// `Element::popover_rect` override, verbatim (the temporary base-y shift reaches the
+    /// children through their raw pointers).
+    fn popover_scan(&self) -> Option<(f32, f32, f32, f32)> {
+        let scroll_y = self.scroll_box.scroll_y;
+        unsafe {
+            for child_ptr in &self.children {
+                let child = &mut **child_ptr;
+                let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
+                if let Some(b) = child.base_mut() {
+                    b.y = old_y - scroll_y;
+                }
+                let res = child.popover_rect();
+                if let Some(b) = child.base_mut() {
+                    b.y = old_y;
+                }
+                if res.is_some() {
+                    return res;
+                }
+            }
         }
+        None
+    }
 
-        // Check if cursor is inside child popovers first (scrolled coordinates)
-        if let Some(pop_rect) = self.popover_rect() {
-            if px >= pop_rect.0 && px <= pop_rect.0 + pop_rect.2 && py >= pop_rect.1 && py <= pop_rect.1 + pop_rect.3 {
-                return true;
+    /// The subtree's rounded view (the old `Element::all_rounded_quads` override):
+    /// background, borders, children with the scroll shift + viewport clamp + border
+    /// inset, scrollbar. External readers get it through the adapter's reverse bridge.
+    fn aggregate_rounded(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+        let mut quads = Vec::new();
+        let (x, y, w, h) = self.rect();
+
+        // 1. Background
+        quads.push((x, y, w, h, 0.0, colors::control_panel_color(), (false, false, false, false)));
+
+        // 2. Borders
+        let border_color = colors::control_panel_border_color();
+        quads.push((x, y, w, 1.0, 0.0, border_color, (false, false, false, false)));
+        quads.push((x, y + h - 1.0, w, 1.0, 0.0, border_color, (false, false, false, false)));
+        quads.push((x, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+        quads.push((x + w - 1.0, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+
+        // 3. Child elements clipped to viewport bounds
+        let scroll_y = self.scroll_box.scroll_y;
+        let y_start = y;
+        let y_end = y + h;
+
+        unsafe {
+            for child_ptr in &self.children {
+                let child = &**child_ptr;
+                let solid_border_opt = if child.type_name() == "Toggle" { None } else { child.solid_border() };
+                let (cx, cy, cw, ch) = child.rect();
+
+                if let Some((b_color, _thickness)) = solid_border_opt {
+                    let cy_shifted = cy - scroll_y;
+                    let cy_top = cy_shifted;
+                    let cy_bottom = cy_shifted + ch;
+                    if cy_bottom > y_start && cy_top < y_end {
+                        let visible_top = cy_top.max(y_start);
+                        let visible_bottom = cy_bottom.min(y_end);
+                        let visible_h = visible_bottom - visible_top;
+                        if visible_h > 0.0 {
+                            let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
+                                0.0
+                            } else {
+                                child.corner_radius()
+                            };
+                            quads.push((
+                                cx,
+                                visible_top,
+                                cw,
+                                visible_h,
+                                radii_adjusted,
+                                b_color,
+                                child.rounded_corners(),
+                            ));
+                        }
+                    }
+                }
+
+                for (qx, qy, qw, qh, qr, qc, qcorners) in child.all_rounded_quads(ctx) {
+                    let mut rx = qx;
+                    let mut ry = qy;
+                    let mut rw = qw;
+                    let mut rh = qh;
+                    let mut rqr = qr;
+
+                    if let Some((_, thickness)) = solid_border_opt {
+                        if (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
+                            rx += thickness;
+                            ry += thickness;
+                            rw -= 2.0 * thickness;
+                            rh -= 2.0 * thickness;
+                            rqr = (qr - thickness).max(0.0);
+                        }
+                    }
+
+                    let qy_shifted = ry - scroll_y;
+                    let qy_top = qy_shifted;
+                    let qy_bottom = qy_shifted + rh;
+                    if qy_bottom > y_start && qy_top < y_end {
+                        let visible_top = qy_top.max(y_start);
+                        let visible_bottom = qy_bottom.min(y_end);
+                        let visible_h = visible_bottom - visible_top;
+                        if visible_h > 0.0 {
+                            let radii_adjusted = if visible_top > qy_top || visible_bottom < qy_bottom {
+                                0.0
+                            } else {
+                                rqr
+                            };
+                            quads.push((rx, visible_top, rw, visible_h, radii_adjusted, qc, qcorners));
+                        }
+                    }
+                }
             }
         }
 
-        // Otherwise check the panel itself
-        let (x, y, w, h) = self.rect();
-        if w <= 0.0 || h <= 0.0 {
-            return false;
+        // 4. Scrollbar
+        for (sx, sy, sw, sh, sc) in self.scroll_box.extra_quads() {
+            quads.push((sx, sy, sw, sh, 0.0, sc, (false, false, false, false)));
         }
-        px >= x && px <= x + w && py >= y && py <= y + h
+
+        quads
     }
 
-    fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
-        let mut changed = self.scroll_box.tick(dt, ctx);
+    /// The subtree's plain view (the old `Element::extra_quads` override): children's
+    /// decoration quads with the scroll shift + viewport clamp, plus the scrollbar.
+    fn aggregate_plain(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let mut quads = Vec::new();
+        let (_x, y, _w, h) = self.rect();
+        let scroll_y = self.scroll_box.scroll_y;
+        let y_start = y;
+        let y_end = y + h;
+        let ctx_dummy = cce_ui::context::UiContext::new();
+
         unsafe {
             for child_ptr in &self.children {
-                if (**child_ptr).tick(dt, ctx) {
-                    changed = true;
+                let child = &**child_ptr;
+                let (cx, cy, cw, ch) = child.rect();
+                let has_rounded = child.rounded_corners() != (false, false, false, false);
+                let has_bg = child.color()[3].abs() > 0.001;
+
+                for (qx, qy, qw, qh, qc) in child.all_quads(&ctx_dummy) {
+                    if has_rounded && has_bg && (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
+                        continue;
+                    }
+                    let qy_shifted = qy - scroll_y;
+                    let qy_top = qy_shifted;
+                    let qy_bottom = qy_shifted + qh;
+                    if qy_bottom > y_start && qy_top < y_end {
+                        let visible_top = qy_top.max(y_start);
+                        let visible_bottom = qy_bottom.min(y_end);
+                        let visible_h = visible_bottom - visible_top;
+                        if visible_h > 0.0 {
+                            quads.push((qx, visible_top, qw, visible_h, qc));
+                        }
+                    }
                 }
             }
         }
-        changed
-    }
 
-    fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> { self.parent }
-    fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
-        self.parent = parent;
+        quads.extend(self.scroll_box.extra_quads());
+        quads
     }
 
-    fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
-        self.children.clone()
+    /// The children's arcs with the scroll shift (the old `Element::extra_arcs` override).
+    fn aggregate_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
+        let mut arcs = Vec::new();
+        let scroll_y = self.scroll_box.scroll_y;
+        let y_start = self.y;
+        let y_end = self.y + self.h;
+
+        unsafe {
+            for child_ptr in &self.children {
+                let child = &**child_ptr;
+                for (cx, cy, r, t, start, end, color) in child.extra_arcs() {
+                    let cy_shifted = cy - scroll_y;
+                    if cy_shifted + r > y_start && cy_shifted - r < y_end {
+                        arcs.push((cx, cy_shifted, r, t, start, end, color));
+                    }
+                }
+            }
+        }
+        arcs
     }
 
-    // ControlPanel is a legacy scroll frame: children are laid out UNSCROLLED and the
-    // scroll offset is applied at aggregate time (all_* / the fonted getter). The walk
-    // must emit those aggregates and not descend — descending would paint the children
-    // unshifted, desyncing text from geometry as soon as the panel scrolls.
-    fn renders_own_subtree(&self) -> bool {
-        true
+    /// Children's walk text with the panel's scroll shift and viewport clamp — what the
+    /// deleted fonted getter served: children are laid out UNSCROLLED and the offset is
+    /// an aggregate-time transform.
+    pub(crate) fn scrolled_child_labels(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
+        let scroll_y = self.scroll_box.scroll_y;
+        let (x, y, w, h) = self.rect();
+        let mut labels = Vec::new();
+        for &child_ptr in &self.children {
+            let mut scratch = cce_ui::scene::paint::PaintCtx::new();
+            cce_ui::scene::painter::append_widget_text(ctx, unsafe { &*child_ptr }, &mut scratch);
+            for item in scratch.finish().items {
+                if let cce_ui::scene::paint::Prim::Text { text, x: lx, y: ly, font_size, color, font, bounds, .. } = item.prim {
+                    let new_bounds = if let Some([l, t, r, b]) = bounds {
+                        let nl = l.max(x);
+                        let nt = (t - scroll_y).max(y);
+                        let nr = r.min(x + w);
+                        let nb = (b - scroll_y).min(y + h);
+                        Some([nl, nt, nr, nb])
+                    } else {
+                        Some([x, y, x + w, y + h])
+                    };
+                    labels.push((
+                        TextLabel { text, x: lx, y: ly - scroll_y, font_size, color },
+                        font,
+                        new_bounds,
+                    ));
+                }
+            }
+        }
+        labels
     }
+}
 
-    fn paint_self(&self, ui: &UiContext, ctx: &mut cce_ui::scene::paint::PaintCtx) {
-        cce_ui::scene::painter::paint_legacy_leaf(self, ui, ctx, self.scrolled_child_labels(ui));
+impl cce_ui::widget::Layout for ControlPanel {
+    // The panel's own label ("ControlPanel") never rendered on the legacy paths and its
+    // rect was never label-inflated — keep the adapter out of both.
+    fn inline_label(&self) -> bool {
+        true
     }
 
-    fn color(&self) -> [f32; 4] {
-        colors::control_panel_color()
+    fn rect_assigned(&mut self, rect: Rect) {
+        self.x = rect.x;
+        self.y = rect.y;
+        self.w = rect.width;
+        self.h = rect.height;
     }
 
-    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
-        self.base.x = x;
-        self.base.y = y;
-        self.base.w = w;
-        self.base.h = h;
+    // The old `set_rect` override's arrangement: children keep their label-matched slots
+    // in a ColumnLayout, laid out UNSCROLLED; the scroll offset is an aggregate-time
+    // transform. `host` (the adapter) becomes the children's parent, as the legacy panel
+    // set itself.
+    fn arrange_children(&mut self, rect: Rect, host: *mut (dyn Element + 'static)) {
+        let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
 
         self.scroll_box.set_rect(x, y, w, h);
 
-        let self_ptr = self.as_ptr_mut();
         let mut dummy = cce_ui::context::UiContext::new();
-        let self_ptr_option = Some(self_ptr);
+        let self_ptr_option = Some(host);
 
         let padding = cce_ui::layout::control_panel_padding();
         let gap = cce_ui::layout::control_panel_gap();
@@ -249,211 +428,160 @@ impl Element for ControlPanel {
             self.scroll_box.update_bounds(total_h, y, h);
         }
     }
+}
 
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        (true, true, true, true)
+impl cce_ui::widget::Paint for ControlPanel {
+    fn color(&self) -> [f32; 4] {
+        colors::control_panel_color()
     }
 
-    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 (x, y, w, h) = self.rect();
+    fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+        // Legacy: rounded_corners all-true with the Element-default 12.0 radius.
+        Some((12.0, (true, true, true, true)))
+    }
 
-        // 1. Background
-        quads.push((x, y, w, h, 0.0, self.color(), (false, false, false, false)));
+    // ControlPanel is a legacy scroll frame: children are laid out UNSCROLLED and the
+    // scroll offset is applied at aggregate time. The walk must emit these aggregates
+    // and not descend — descending would paint the children unshifted, desyncing text
+    // from geometry as soon as the panel scrolls.
+    fn paints_own_subtree(&self) -> bool {
+        true
+    }
 
-        // 2. Borders
-        let border_color = colors::control_panel_border_color();
-        quads.push((x, y, w, 1.0, 0.0, border_color, (false, false, false, false)));
-        quads.push((x, y + h - 1.0, w, 1.0, 0.0, border_color, (false, false, false, false)));
-        quads.push((x, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
-        quads.push((x + w - 1.0, y, 1.0, h, 0.0, border_color, (false, false, false, false)));
+    fn paint(&self, _rect: Rect, pc: &mut PaintCtx) {
+        // The children are Adapted widgets whose aggregate getters read nothing from the
+        // routing context; a fresh one stands in for the ctx `Paint::paint` doesn't carry.
+        let dummy = cce_ui::context::UiContext::new();
+        for (x, y, qw, qh, r, c, corners) in self.aggregate_rounded(&dummy) {
+            pc.rounded_rect(Rect { x, y, width: qw, height: qh }, r, corners, c);
+        }
+        for (x, y, qw, qh, c) in self.aggregate_plain() {
+            pc.quad(Rect { x, y, width: qw, height: qh }, c);
+        }
+        for (cx, cy, r, t, s, e, c) in self.aggregate_arcs() {
+            pc.arc(cx, cy, r, t, s, e, c);
+        }
+        for (tl, font, bounds) in self.scrolled_child_labels(&dummy) {
+            pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
+        }
+    }
 
-        // 3. Child elements clipped to viewport bounds
-        let scroll_y = self.scroll_box.scroll_y;
-        let y_start = y;
-        let y_end = y + h;
+    fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
+        self.popover_scan()
+    }
 
+    fn draw_popover(&self, _rect: Rect, pc: &mut dyn cce_ui::layout::RenderTarget) {
+        let scroll_y = self.scroll_box.scroll_y;
         unsafe {
             for child_ptr in &self.children {
-                let child = &**child_ptr;
-                let solid_border_opt = if child.type_name() == "Toggle" { None } else { child.solid_border() };
-                let (cx, cy, cw, ch) = child.rect();
-
-                if let Some((b_color, _thickness)) = solid_border_opt {
-                    let cy_shifted = cy - scroll_y;
-                    let cy_top = cy_shifted;
-                    let cy_bottom = cy_shifted + ch;
-                    if cy_bottom > y_start && cy_top < y_end {
-                        let visible_top = cy_top.max(y_start);
-                        let visible_bottom = cy_bottom.min(y_end);
-                        let visible_h = visible_bottom - visible_top;
-                        if visible_h > 0.0 {
-                            let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
-                                0.0
-                            } else {
-                                child.corner_radius()
-                            };
-                            quads.push((
-                                cx,
-                                visible_top,
-                                cw,
-                                visible_h,
-                                radii_adjusted,
-                                b_color,
-                                child.rounded_corners(),
-                            ));
-                        }
-                    }
-                }
-
-                for (qx, qy, qw, qh, qr, qc, qcorners) in child.all_rounded_quads(ctx) {
-                    let mut rx = qx;
-                    let mut ry = qy;
-                    let mut rw = qw;
-                    let mut rh = qh;
-                    let mut rqr = qr;
-
-                    if let Some((_, thickness)) = solid_border_opt {
-                        if (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
-                            rx += thickness;
-                            ry += thickness;
-                            rw -= 2.0 * thickness;
-                            rh -= 2.0 * thickness;
-                            rqr = (qr - thickness).max(0.0);
-                        }
+                if (**child_ptr).popover_rect().is_some() {
+                    let child = &mut **child_ptr;
+                    let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
+                    if let Some(b) = child.base_mut() {
+                        b.y = old_y - scroll_y;
                     }
-
-                    let qy_shifted = ry - scroll_y;
-                    let qy_top = qy_shifted;
-                    let qy_bottom = qy_shifted + rh;
-                    if qy_bottom > y_start && qy_top < y_end {
-                        let visible_top = qy_top.max(y_start);
-                        let visible_bottom = qy_bottom.min(y_end);
-                        let visible_h = visible_bottom - visible_top;
-                        if visible_h > 0.0 {
-                            let radii_adjusted = if visible_top > qy_top || visible_bottom < qy_bottom {
-                                0.0
-                            } else {
-                                rqr
-                            };
-                            quads.push((rx, visible_top, rw, visible_h, radii_adjusted, qc, qcorners));
-                        }
+                    child.render_popover(pc);
+                    if let Some(b) = child.base_mut() {
+                        b.y = old_y;
                     }
                 }
             }
         }
+    }
+}
 
-        // 4. Scrollbar
-        for (sx, sy, sw, sh, sc) in self.scroll_box.extra_quads() {
-            quads.push((sx, sy, sw, sh, 0.0, sc, (false, false, false, false)));
+impl cce_ui::widget::Input for ControlPanel {
+    /// The legacy hit shape: an open child popover (scrolled coordinates) or the panel rect.
+    fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
+        if let Some(pop_rect) = self.popover_scan() {
+            if x >= pop_rect.0 && x <= pop_rect.0 + pop_rect.2 && y >= pop_rect.1 && y <= pop_rect.1 + pop_rect.3 {
+                return true;
+            }
         }
+        if rect.width <= 0.0 || rect.height <= 0.0 {
+            return false;
+        }
+        x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height
+    }
 
-        quads
+    // The legacy direct-dispatch broadcast reached mouse_input ungated: the panel resets
+    // its drag target on every press and lets children with open popovers see off-panel
+    // presses.
+    fn gates_presses(&self) -> bool {
+        false
     }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = Vec::new();
-        let (_x, y, _w, h) = self.rect();
-        let scroll_y = self.scroll_box.scroll_y;
-        let y_start = y;
-        let y_end = y + h;
-        let ctx_dummy = cce_ui::context::UiContext::new();
+    fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+        let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
+        match event {
+            Event::MouseButton { button, state, x: px, y: py, .. } => {
+                if *state == ElementState::Pressed {
+                    self.active_drag_widget = None;
+                }
 
-        unsafe {
-            for child_ptr in &self.children {
-                let child = &**child_ptr;
-                let (cx, cy, cw, ch) = child.rect();
-                let has_rounded = child.rounded_corners() != (false, false, false, false);
-                let has_bg = child.color()[3].abs() > 0.001;
+                if self.scroll_box.mouse_input(*button, *state, *px, *py, ctx) {
+                    return true;
+                }
 
-                for (qx, qy, qw, qh, qc) in child.all_quads(&ctx_dummy) {
-                    if has_rounded && has_bg && (qx - cx).abs() < 0.1 && (qy - cy).abs() < 0.1 && (qw - cw).abs() < 0.1 && (qh - ch).abs() < 0.1 {
-                        continue;
-                    }
-                    let qy_shifted = qy - scroll_y;
-                    let qy_top = qy_shifted;
-                    let qy_bottom = qy_shifted + qh;
-                    if qy_bottom > y_start && qy_top < y_end {
-                        let visible_top = qy_top.max(y_start);
-                        let visible_bottom = qy_bottom.min(y_end);
-                        let visible_h = visible_bottom - visible_top;
-                        if visible_h > 0.0 {
-                            quads.push((qx, visible_top, qw, visible_h, qc));
+                let scroll_y = self.scroll_box.scroll_y;
+                let py_translated = *py + scroll_y;
+                let (_x, y, _w, h) = self.rect();
+
+                unsafe {
+                    for child_ptr in &self.children {
+                        let has_popover = (**child_ptr).popover_rect().is_some();
+                        // Only dispatch if the click Y is inside the viewport or the child has an active popover
+                        if has_popover || (*py >= y && *py <= y + h) {
+                            if (**child_ptr).mouse_input(*button, *state, *px, py_translated, ctx) {
+                                if *state == ElementState::Pressed && (**child_ptr).draggable() {
+                                    self.active_drag_widget = Some(*child_ptr);
+                                }
+                                return true;
+                            }
                         }
                     }
                 }
+                false
             }
-        }
-
-        quads.extend(self.scroll_box.extra_quads());
-        quads
-    }
-
-    fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
-        let mut arcs = Vec::new();
-        let scroll_y = self.scroll_box.scroll_y;
-        let y_start = self.base.y;
-        let y_end = self.base.y + self.base.h;
-
-        unsafe {
-            for child_ptr in &self.children {
-                let child = &**child_ptr;
-                for (cx, cy, r, t, start, end, color) in child.extra_arcs() {
-                    let cy_shifted = cy - scroll_y;
-                    if cy_shifted + r > y_start && cy_shifted - r < y_end {
-                        arcs.push((cx, cy_shifted, r, t, start, end, color));
+            Event::PointerMove { x: px, y: py, .. } => {
+                let mut changed = self.scroll_box.cursor_moved(*px, *py, ctx);
+
+                let scroll_y = self.scroll_box.scroll_y;
+                let py_translated = *py + scroll_y;
+                unsafe {
+                    for child_ptr in &self.children {
+                        if (**child_ptr).cursor_moved(*px, py_translated, ctx) {
+                            changed = true;
+                        }
                     }
                 }
+                changed
             }
-        }
-        arcs
-    }
-
-    fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
-        let scroll_y = self.scroll_box.scroll_y;
-        unsafe {
-            for child_ptr in &self.children {
-                let child = &mut **child_ptr;
-                let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
-                if let Some(b) = child.base_mut() {
-                    b.y = old_y - scroll_y;
-                }
-                let res = child.popover_rect();
-                if let Some(b) = child.base_mut() {
-                    b.y = old_y;
-                }
-                if res.is_some() {
-                    return res;
+            Event::MouseWheel { delta, x: px, y: py, .. } => {
+                if self.scroll_box.mouse_wheel(delta, *px, *py, ctx) {
+                    return true;
                 }
-            }
-        }
-        None
-    }
 
-    fn render_popover(&self, pc: &mut dyn cce_ui::layout::RenderTarget) {
-        let scroll_y = self.scroll_box.scroll_y;
-        unsafe {
-            for child_ptr in &self.children {
-                if (**child_ptr).popover_rect().is_some() {
-                    let child = &mut **child_ptr;
-                    let old_y = child.base().map(|b| b.y).unwrap_or(0.0);
-                    if let Some(b) = child.base_mut() {
-                        b.y = old_y - scroll_y;
-                    }
-                    child.render_popover(pc);
-                    if let Some(b) = child.base_mut() {
-                        b.y = old_y;
+                let scroll_y = self.scroll_box.scroll_y;
+                let py_translated = *py + scroll_y;
+                unsafe {
+                    for child_ptr in &self.children {
+                        if (**child_ptr).mouse_wheel(delta, *px, py_translated, ctx) {
+                            return true;
+                        }
                     }
                 }
+                false
             }
+            _ => false,
         }
     }
 
-    fn draggable(&self) -> bool {
+    fn draggable(&self, _rect: Rect) -> bool {
         self.scroll_box.draggable() || self.active_drag_widget.is_some()
     }
 
-    fn drag_begin(&mut self, px: f32, py: f32) {
+    fn drag_begin(&mut self, px: f32, py: f32, _rect: Rect) {
         if self.scroll_box.hit_test_scrollbar(px, py) {
             self.scroll_box.drag_begin(px, py);
             return;
@@ -468,7 +596,7 @@ impl Element for ControlPanel {
         }
     }
 
-    fn drag_update(&mut self, px: f32, py: f32) -> bool {
+    fn drag_update(&mut self, px: f32, py: f32, _rect: Rect) -> bool {
         if self.scroll_box.draggable() {
             return self.scroll_box.drag_update(px, py);
         }
@@ -493,144 +621,37 @@ impl Element for ControlPanel {
         }
     }
 
-    fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if state == ElementState::Pressed {
-            self.active_drag_widget = None;
-        }
-
-        if self.scroll_box.mouse_input(button, state, px, py, ctx) {
-            return true;
-        }
-
-        let scroll_y = self.scroll_box.scroll_y;
-        let py_translated = py + scroll_y;
-        let (_x, y, _w, h) = self.rect();
-
-        unsafe {
-            for child_ptr in &self.children {
-                let has_popover = (**child_ptr).popover_rect().is_some();
-                // Only dispatch if the click Y is inside the viewport or the child has an active popover
-                if has_popover || (py >= y && py <= y + h) {
-                    if (**child_ptr).mouse_input(button, state, px, py_translated, ctx) {
-                        if state == ElementState::Pressed && (**child_ptr).draggable() {
-                            self.active_drag_widget = Some(*child_ptr);
-                        }
-                        return true;
-                    }
-                }
-            }
-        }
-        false
-    }
-
-    fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        let mut changed = self.scroll_box.cursor_moved(px, py, ctx);
-
-        let scroll_y = self.scroll_box.scroll_y;
-        let py_translated = py + scroll_y;
+    fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
+        let Some(ctx) = ectx.ui.as_deref_mut() else { return false };
+        let mut changed = self.scroll_box.tick(dt, ctx);
         unsafe {
             for child_ptr in &self.children {
-                if (**child_ptr).cursor_moved(px, py_translated, ctx) {
+                if (**child_ptr).tick(dt, ctx) {
                     changed = true;
                 }
             }
         }
         changed
     }
-
-    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        if self.scroll_box.mouse_wheel(delta, px, py, ctx) {
-            return true;
-        }
-
-        let scroll_y = self.scroll_box.scroll_y;
-        let py_translated = py + scroll_y;
-        unsafe {
-            for child_ptr in &self.children {
-                if (**child_ptr).mouse_wheel(delta, px, py_translated, ctx) {
-                    return true;
-                }
-            }
-        }
-        false
-    }
-
-}
-
-impl Drop for ControlPanel {
-    fn drop(&mut self) {
-        clear_widget_references(self);
-    }
-}
-
-impl ControlPanel {
-    /// Children's walk text with the panel's scroll shift and viewport clamp — what the
-    /// deleted fonted getter served: children are laid out UNSCROLLED and the offset is
-    /// an aggregate-time transform.
-    pub(crate) fn scrolled_child_labels(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
-        let scroll_y = self.scroll_box.scroll_y;
-        let (x, y, w, h) = self.rect();
-        let mut labels = Vec::new();
-        for &child_ptr in &self.children {
-            let mut scratch = cce_ui::scene::paint::PaintCtx::new();
-            cce_ui::scene::painter::append_widget_text(ctx, unsafe { &*child_ptr }, &mut scratch);
-            for item in scratch.finish().items {
-                if let cce_ui::scene::paint::Prim::Text { text, x: lx, y: ly, font_size, color, font, bounds, .. } = item.prim {
-                    let new_bounds = if let Some([l, t, r, b]) = bounds {
-                        let nl = l.max(x);
-                        let nt = (t - scroll_y).max(y);
-                        let nr = r.min(x + w);
-                        let nb = (b - scroll_y).min(y + h);
-                        Some([nl, nt, nr, nb])
-                    } else {
-                        Some([x, y, x + w, y + h])
-                    };
-                    labels.push((
-                        TextLabel { text, x: lx, y: ly - scroll_y, font_size, color },
-                        font,
-                        new_bounds,
-                    ));
-                }
-            }
-        }
-        labels
-    }
 }
 
 /// Gallery exhibit lookalike of the dissolved cce-ui `Plate`: default plate color rule
 /// (config plate color, else page color) at plate opacity, alpha negated under blur,
 /// plate corner radius/rounding, config border, detached-top label handled like the
-/// legacy plate (background shifted below the label, label drawn in paint_self).
+/// legacy plate (background shifted below the label, label drawn from `paint`).
 pub struct Plate {
-    base: Widget,
     blur: bool,
+    label: Option<String>,
 }
 
 impl Plate {
-    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
-        let mut base = Widget::new();
-        base.x = x;
-        base.y = y;
-        base.w = w;
-        base.h = h;
-        Self { base, blur: false }
-    }
-
-    pub fn with_blur(mut self, blur: bool) -> Self {
-        self.blur = blur;
-        self
+    pub fn new(x: f32, y: f32, w: f32, h: f32, blur: bool) -> Adapted<Plate> {
+        let mut plate = Adapted::new(Self { blur, label: None });
+        plate.set_rect(x, y, w, h);
+        plate
     }
 
-    pub fn with_label(mut self, label: &str) -> Self {
-        self.base.label = Some(label.to_string());
-        self
-    }
-}
-
-impl Element for Plate {
-    cce_ui::impl_widget_base!(Plate);
-
-    fn color(&self) -> [f32; 4] {
+    fn plate_color(&self) -> [f32; 4] {
         let mut c = if let Some(c) = colors::plate_color() {
             c
         } else {
@@ -643,131 +664,166 @@ impl Element for Plate {
         c
     }
 
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        let r = cce_ui::layout::plate_corner_radius();
-        if r > 0.0 { (true, true, true, true) } else { (false, false, false, false) }
+    /// The legacy `Widget::label_offset` rule for the model-held label copy.
+    fn label_offset(&self) -> f32 {
+        if cce_ui::layout::control_label_layout() == "side" {
+            return 0.0;
+        }
+        if self.label.is_some() {
+            let (_, font_size) = cce_ui::layout::control_label_font_detached_parsed();
+            font_size + cce_ui::layout::control_label_margin()
+        } else {
+            0.0
+        }
     }
+}
 
-    fn corner_radius(&self) -> f32 {
-        cce_ui::layout::plate_corner_radius()
+impl cce_ui::widget::Layout for Plate {
+    // The legacy plate never inflated its rect for the label; it shifted its background
+    // below it instead (see `paint`).
+    fn inline_label(&self) -> bool {
+        true
+    }
+}
+
+impl cce_ui::widget::Paint for Plate {
+    fn color(&self) -> [f32; 4] {
+        self.plate_color()
+    }
+
+    fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+        let r = cce_ui::layout::plate_corner_radius();
+        let on = r > 0.0;
+        Some((r, (on, on, on, on)))
     }
 
     fn solid_border(&self) -> Option<([f32; 4], f32)> {
         colors::plate_border_color().map(|bc| (bc, colors::plate_border_thickness()))
     }
 
-    fn all_rounded_quads(&self, _ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
-        let (r1, r2, r3, r4) = self.rounded_corners();
-        if !(r1 || r2 || r3 || r4) {
-            return Vec::new();
-        }
-        let (x, y, w, h) = self.rect();
-        let label_off = self.base.label_offset();
-        let c = self.color();
-        if c[3].abs() > 0.001 {
-            vec![(x, y + label_off, w, h - label_off, self.corner_radius(), c, (r1, r2, r3, r4))]
-        } else {
-            Vec::new()
-        }
+    fn sync_label(&mut self, label: &str) {
+        self.label = Some(label.to_string());
     }
 
-    fn paint_self(&self, _ui: &UiContext, ctx: &mut cce_ui::scene::paint::PaintCtx) {
-        if let Some(ref label) = self.base.label {
+    fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
+        // Rounded background below the label region — the legacy all_rounded_quads
+        // override: emitted only while corners are on and the color has alpha.
+        let radius = cce_ui::layout::plate_corner_radius();
+        let c = self.plate_color();
+        if radius > 0.0 && c[3].abs() > 0.001 {
+            let label_off = self.label_offset();
+            pc.rounded_rect(
+                Rect { x: rect.x, y: rect.y + label_off, width: rect.width, height: rect.height - label_off },
+                radius,
+                (true, true, true, true),
+                c,
+            );
+        }
+        if let Some(ref label) = self.label {
             let (_, font_size) = cce_ui::layout::control_label_font_parsed();
-            ctx.text(label.clone(), self.base.x, self.base.y, font_size, colors::control_label_color_u8());
+            pc.text(label.clone(), rect.x, rect.y, font_size, colors::control_label_color_u8());
         }
     }
 }
 
+impl cce_ui::widget::Input for Plate {}
+
 /// Gallery lookalike of the dissolved cce-ui `SectionContainer` as the ControlPanel uses
 /// it: a childless section header row — the title label plus the SectionHeader separator
-/// line. The panel matches sections by the base label.
+/// line. The panel matches sections by the adapter's base label; the model keeps a copy
+/// (via `sync_label`) for its own painting.
 pub struct SectionContainer {
-    base: Widget,
+    title: String,
 }
 
 impl SectionContainer {
-    pub fn new(title: &str) -> Self {
-        let mut base = Widget::new();
-        base.label = Some(title.to_string());
-        Self { base }
+    pub fn new(title: &str) -> Adapted<SectionContainer> {
+        Adapted::new(Self { title: title.to_string() }).with_label(title)
     }
 }
 
-impl Element for SectionContainer {
-    cce_ui::impl_widget_base!(SectionContainer);
-
-    fn blocks_backplate_drag(&self) -> bool { false }
+impl cce_ui::widget::Layout for SectionContainer {
+    // The title is painted by the model at its legacy position; keep the adapter's
+    // detached-label machinery (offset inflation + fallback label) out of it.
+    fn inline_label(&self) -> bool {
+        true
+    }
+}
 
+impl cce_ui::widget::Paint for SectionContainer {
     fn color(&self) -> [f32; 4] {
         [0.0, 0.0, 0.0, 0.0]
     }
 
-    fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        vec![(self.base.x + 8.0, self.base.y + 22.0, self.base.w - 16.0, 1.0, [0.18, 0.18, 0.27, 1.0])]
+    fn sync_label(&mut self, label: &str) {
+        self.title = label.to_string();
     }
 
-    fn paint_self(&self, _ui: &UiContext, ctx: &mut cce_ui::scene::paint::PaintCtx) {
-        for (qx, qy, qw, qh, qc) in self.extra_quads() {
-            ctx.quad(cce_ui::scene::layout::Rect { x: qx, y: qy, width: qw, height: qh }, qc);
-        }
-        ctx.text(
-            self.base.label.clone().unwrap_or_default(),
-            self.base.x + 12.0,
-            self.base.y,
-            14.0,
-            [212, 212, 212],
+    fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
+        pc.quad(
+            Rect { x: rect.x + 8.0, y: rect.y + 22.0, width: rect.width - 16.0, height: 1.0 },
+            [0.18, 0.18, 0.27, 1.0],
         );
+        pc.text(self.title.clone(), rect.x + 12.0, rect.y, 14.0, [212, 212, 212]);
+    }
+}
+
+impl cce_ui::widget::Input for SectionContainer {
+    fn blocks_backplate_drag(&self) -> bool {
+        false
     }
 }
 
 /// Child-window background lookalike of the dissolved cce-ui `Backplate`: the base color
 /// forced to the configured backplate opacity, backplate corner radius, immovable.
 pub struct Backplate {
-    base: Widget,
     bevel: Option<f32>,
 }
 
 impl Backplate {
-    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
-        let mut base = Widget::new();
-        base.x = x;
-        base.y = y;
-        base.w = w;
-        base.h = h;
-        Self { base, bevel: None }
-    }
-
-    pub fn with_movable(self, _movable: bool) -> Self {
-        self
+    pub fn new(x: f32, y: f32, w: f32, h: f32) -> Adapted<Backplate> {
+        let mut bp = Adapted::new(Self { bevel: None });
+        bp.set_rect(x, y, w, h);
+        bp
     }
 
-    pub fn with_bevel(mut self, bevel: bool, thickness: f32) -> Self {
-        self.bevel = if bevel { Some(thickness) } else { None };
-        self
+    pub fn set_bevel(&mut self, thickness: f32) {
+        self.bevel = Some(thickness);
     }
-}
 
-impl Element for Backplate {
-    cce_ui::impl_widget_base!(Backplate);
-
-    fn color(&self) -> [f32; 4] {
+    fn backplate_color(&self) -> [f32; 4] {
         let mut c = cce_ui::color::page_low_color();
         if c[3] > 0.001 {
             c[3] = cce_ui::color::active_backplate_opacity();
         }
         c
     }
+}
+
+impl cce_ui::widget::Layout for Backplate {}
 
-    fn corner_radius(&self) -> f32 {
-        cce_ui::color::backplate_corner_radius()
+impl cce_ui::widget::Paint for Backplate {
+    fn color(&self) -> [f32; 4] {
+        self.backplate_color()
     }
 
-    fn rounded_corners(&self) -> (bool, bool, bool, bool) {
-        if self.corner_radius() > 0.1 { (true, true, true, true) } else { (false, false, false, false) }
+    fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+        let r = cce_ui::color::backplate_corner_radius();
+        let on = r > 0.1;
+        Some((r, (on, on, on, on)))
     }
 
-    fn plate_bevel(&self) -> Option<f32> {
-        self.bevel
+    fn paint(&self, rect: Rect, pc: &mut PaintCtx) {
+        // The legacy rounded Backplate emitted its background only through
+        // all_rounded_quads (nothing when the radius is off) — same shape here. The bevel
+        // field is carried for the --border-bevel flag but, like the legacy lookalike, has
+        // no reader on the gallery's display path.
+        let radius = cce_ui::color::backplate_corner_radius();
+        let c = self.backplate_color();
+        if radius > 0.1 && c[3].abs() > 0.001 {
+            pc.rounded_rect(rect, radius, (true, true, true, true), c);
+        }
     }
 }
+
+impl cce_ui::widget::Input for Backplate {}