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

commit96241d6bf351d46b60aac5f940086baf75088ffe
parenta9c9145626
authorLucas Galante <[email protected]>
date2026-07-13 11:13
refactor(panel)!: the ControlPanel endgame — the panel dissolves to scroll chrome

The gallery's last stored-child-pointer container is gone: the panel model
keeps only its ScrollBox and bg/border paint; the app lays the child slots
out at SCREEN coordinates (arrange_control_panel, scroll applied at layout
time, re-run per frame), emits their geometry/text clamped to the panel
viewport with the legacy partial-clip and border-inset rules, and
dispatches them as ordinary routed roots behind cp_gate (panel rect or an
open child popover — without it, fold-hidden slots stole chrome clicks).
The panel takes the wheel before its children; keys stay focused-path-only.
Legacy's double-drawn scrollbar (rounded AND plain aggregates) is single-
drawn now. Roster grew per-slot draggable/is_dragging matches (gallery +
child mode) for the trait shrink.

Verified: Windows page A/B vs pre-dissolution baseline across static/
scrolled/popover states (masks empty except the scrollbar single-draw
strip); dropdown select -> Surface Info update; spinbox increment; wheel
scroll; page switching both ways; child mode alive. Thumb drag: user
spot-check.

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

 src/main.rs       | 481 ++++++++++++++++++++++++++++++++++++++++++-----
 src/ti_widgets.rs | 552 +++---------------------------------------------------
 2 files changed, 462 insertions(+), 571 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index c1d1267..6005209 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -79,6 +79,127 @@ pub struct GallerySlots {
 pub const GALLERY_COUNT: usize = 53;
 
 impl GallerySlots {
+
+    // Per-slot drag queries (the ControlPanel endgame took `draggable`/`is_dragging`
+    // off `WidgetHost`).
+    pub fn draggable(&self, idx: usize) -> bool {
+        match idx {
+            0 => self.menu_bar.draggable(),
+            1 => self.paginator.draggable(),
+            2 => self.status_bar.draggable(),
+            3 => self.button_demo.draggable(),
+            4 => self.label4.draggable(),
+            5 => self.label5.draggable(),
+            6 => self.label6.draggable(),
+            7 => self.label7.draggable(),
+            8 => self.label8.draggable(),
+            9 => self.checkbox_demo.draggable(),
+            10 => self.toggle_demo.draggable(),
+            11 => self.progress_demo.draggable(),
+            12 => self.slider_demo.draggable(),
+            13 => self.spinbox_demo.draggable(),
+            14 => self.panel_demo.draggable(),
+            15 => self.create_window_btn.draggable(),
+            16 => self.tile_windows_btn.draggable(),
+            17 => self.opacity_toggle.draggable(),
+            18 => self.label18.draggable(),
+            19 => self.transparency_slider.draggable(),
+            20 => self.transparency_label.draggable(),
+            21 => self.window_type_dd.draggable(),
+            22 => self.window_shape_dd.draggable(),
+            23 => self.enable_toggle.draggable(),
+            24 => self.label24.draggable(),
+            25 => self.width_spin.draggable(),
+            26 => self.height_spin.draggable(),
+            27 => self.surface_plate.draggable(),
+            28 => self.surface_info_label.draggable(),
+            29 => self.surface_desc_label.draggable(),
+            30 => self.range_slider_demo.draggable(),
+            31 => self.trackpad_demo.draggable(),
+            32 => self.portal_panel.draggable(),
+            33 => self.portal_label.draggable(),
+            34 => self.open_dialog_btn.draggable(),
+            35 => self.save_dialog_btn.draggable(),
+            36 => self.textbox_demo.draggable(),
+            37 => self.plate_demo.draggable(),
+            38 => self.backplate_toggle.draggable(),
+            39 => self.menubar_toggle.draggable(),
+            40 => self.statusbar_toggle.draggable(),
+            41 => self.border_section.draggable(),
+            42 => self.bevel_toggle.draggable(),
+            43 => self.border_width_spin.draggable(),
+            44 => self.bevel_depth_spin.draggable(),
+            45 => self.elements_section.draggable(),
+            46 => self.page_selector.draggable(),
+            47 => self.color_ramp_btn.draggable(),
+            48 => self.bevel_shape_btn.draggable(),
+            49 => self.bevel_ramp.draggable(),
+            50 => self.ramp_btn.draggable(),
+            51 => self.control_panel.draggable(),
+            52 => self.layout_dd.draggable(),
+            _ => panic!("gallery slot index out of range: {idx}"),
+        }
+    }
+
+    pub fn is_dragging(&self, idx: usize) -> bool {
+        match idx {
+            0 => self.menu_bar.is_dragging(),
+            1 => self.paginator.is_dragging(),
+            2 => self.status_bar.is_dragging(),
+            3 => self.button_demo.is_dragging(),
+            4 => self.label4.is_dragging(),
+            5 => self.label5.is_dragging(),
+            6 => self.label6.is_dragging(),
+            7 => self.label7.is_dragging(),
+            8 => self.label8.is_dragging(),
+            9 => self.checkbox_demo.is_dragging(),
+            10 => self.toggle_demo.is_dragging(),
+            11 => self.progress_demo.is_dragging(),
+            12 => self.slider_demo.is_dragging(),
+            13 => self.spinbox_demo.is_dragging(),
+            14 => self.panel_demo.is_dragging(),
+            15 => self.create_window_btn.is_dragging(),
+            16 => self.tile_windows_btn.is_dragging(),
+            17 => self.opacity_toggle.is_dragging(),
+            18 => self.label18.is_dragging(),
+            19 => self.transparency_slider.is_dragging(),
+            20 => self.transparency_label.is_dragging(),
+            21 => self.window_type_dd.is_dragging(),
+            22 => self.window_shape_dd.is_dragging(),
+            23 => self.enable_toggle.is_dragging(),
+            24 => self.label24.is_dragging(),
+            25 => self.width_spin.is_dragging(),
+            26 => self.height_spin.is_dragging(),
+            27 => self.surface_plate.is_dragging(),
+            28 => self.surface_info_label.is_dragging(),
+            29 => self.surface_desc_label.is_dragging(),
+            30 => self.range_slider_demo.is_dragging(),
+            31 => self.trackpad_demo.is_dragging(),
+            32 => self.portal_panel.is_dragging(),
+            33 => self.portal_label.is_dragging(),
+            34 => self.open_dialog_btn.is_dragging(),
+            35 => self.save_dialog_btn.is_dragging(),
+            36 => self.textbox_demo.is_dragging(),
+            37 => self.plate_demo.is_dragging(),
+            38 => self.backplate_toggle.is_dragging(),
+            39 => self.menubar_toggle.is_dragging(),
+            40 => self.statusbar_toggle.is_dragging(),
+            41 => self.border_section.is_dragging(),
+            42 => self.bevel_toggle.is_dragging(),
+            43 => self.border_width_spin.is_dragging(),
+            44 => self.bevel_depth_spin.is_dragging(),
+            45 => self.elements_section.is_dragging(),
+            46 => self.page_selector.is_dragging(),
+            47 => self.color_ramp_btn.is_dragging(),
+            48 => self.bevel_shape_btn.is_dragging(),
+            49 => self.bevel_ramp.is_dragging(),
+            50 => self.ramp_btn.is_dragging(),
+            51 => self.control_panel.is_dragging(),
+            52 => self.layout_dd.is_dragging(),
+            _ => panic!("gallery slot index out of range: {idx}"),
+        }
+    }
+
     pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
         match idx {
             0 => &self.menu_bar,
@@ -232,6 +353,55 @@ pub struct ChildSlots {
 pub const CHILD_COUNT: usize = 5;
 
 impl ChildSlots {
+
+    pub fn draggable(&self, idx: usize) -> bool {
+        match idx {
+            0 => match &self.bg {
+                ChildBg::Backplate(w) => w.draggable(),
+                ChildBg::ContentBg(w) => w.draggable(),
+            },
+            1 => match &self.main {
+                ChildMain::ColorRamp(w) => w.draggable(),
+                ChildMain::Ramp(w) => w.draggable(),
+                ChildMain::Desc(w) => w.draggable(),
+            },
+            2 => self.close.draggable(),
+            3 => match &self.aux3 {
+                ChildAux3::Label(w) => w.draggable(),
+                ChildAux3::MenuBar(w) => w.draggable(),
+            },
+            4 => match &self.aux4 {
+                ChildAux4::Label(w) => w.draggable(),
+                ChildAux4::StatusBar(w) => w.draggable(),
+            },
+            _ => panic!("child slot index out of range: {idx}"),
+        }
+    }
+
+    pub fn is_dragging(&self, idx: usize) -> bool {
+        match idx {
+            0 => match &self.bg {
+                ChildBg::Backplate(w) => w.is_dragging(),
+                ChildBg::ContentBg(w) => w.is_dragging(),
+            },
+            1 => match &self.main {
+                ChildMain::ColorRamp(w) => w.is_dragging(),
+                ChildMain::Ramp(w) => w.is_dragging(),
+                ChildMain::Desc(w) => w.is_dragging(),
+            },
+            2 => self.close.is_dragging(),
+            3 => match &self.aux3 {
+                ChildAux3::Label(w) => w.is_dragging(),
+                ChildAux3::MenuBar(w) => w.is_dragging(),
+            },
+            4 => match &self.aux4 {
+                ChildAux4::Label(w) => w.is_dragging(),
+                ChildAux4::StatusBar(w) => w.is_dragging(),
+            },
+            _ => panic!("child slot index out of range: {idx}"),
+        }
+    }
+
     pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
         match idx {
             0 => match &self.bg {
@@ -303,6 +473,20 @@ impl Roster {
         }
     }
 
+    pub fn draggable(&self, idx: usize) -> bool {
+        match self {
+            Roster::Gallery(s) => s.draggable(idx),
+            Roster::Child(s) => s.draggable(idx),
+        }
+    }
+
+    pub fn is_dragging(&self, idx: usize) -> bool {
+        match self {
+            Roster::Gallery(s) => s.is_dragging(idx),
+            Roster::Child(s) => s.is_dragging(idx),
+        }
+    }
+
     pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
         match self {
             Roster::Gallery(s) => s.get_dyn_mut(idx),
@@ -581,6 +765,33 @@ fn is_control_panel_child(i: usize) -> bool {
     matches!(i, 15..=26 | 38..=45 | 48)
 }
 
+/// The panel's child slots in the legacy arrangement/aggregation order (the ControlPanel
+/// endgame: the panel no longer stores pointers to them — the app lays them out, emits
+/// them clamped to the panel viewport, and dispatches them as ordinary routed roots).
+const CP_CHILDREN: [usize; 19] = [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48];
+
+impl State {
+    /// The dissolved panel's hit gate: pointer events reach the panel's children only
+    /// inside the panel rect or an open child popover (the legacy `ControlPanel::hit` +
+    /// viewport-Y forwarding rule). Children sit at real screen rects now — without this
+    /// gate, slots clipped BELOW the visible fold would take clicks meant for the chrome
+    /// beneath them.
+    fn cp_gate(&self, x: f32, y: f32) -> bool {
+        let (px, py, pw, ph) = self.roster.gallery().control_panel.rect();
+        if x >= px && x <= px + pw && y >= py && y <= py + ph {
+            return true;
+        }
+        for &ci in CP_CHILDREN.iter() {
+            if let Some((rx, ry, rw, rh)) = self.roster.get_dyn(ci).popover_rect() {
+                if x >= rx && x <= rx + rw && y >= ry && y <= ry + rh {
+                    return true;
+                }
+            }
+        }
+        false
+    }
+}
+
 fn open_file_dialog_portal(sender: calloop::channel::Sender<String>) {
     if let Some(path) = cce_ui::file_dialog::pick_file("Open File Dialog", &[]) {
         let _ = sender.send(format!("Selected: {}", path.display()));
@@ -642,20 +853,18 @@ impl State {
             let visible = self.is_widget_visible(i);
             let pos = self.positions[i];
             if i < self.roster.len() {
-                let widget = self.roster.get_dyn_mut(i);
-                if widget.is_dragging() {
+                if self.roster.is_dragging(i) {
                     continue;
                 }
+                let widget = self.roster.get_dyn_mut(i);
                 
                 if !self.is_child && i == 46 {
                     continue;
                 }
 
-                let is_cp_child = is_control_panel_child(i);
-                if is_cp_child {
-                    if !visible {
-                        widget.set_rect(-1000.0, -1000.0, 0.0, 0.0);
-                    }
+                if is_control_panel_child(i) {
+                    // Laid out by arrange_control_panel after this loop (real screen
+                    // coordinates; every consumer gates on page visibility).
                     continue;
                 }
                 
@@ -686,6 +895,59 @@ impl State {
             let dx = sb_rect.0 + sb_rect.2 - dw - pad_x;
             let dy = sb_rect.1 + pad_y;
             self.roster.get_dyn_mut(46).set_rect(dx, dy, dw, ddh);
+
+            self.arrange_control_panel();
+        }
+    }
+
+    /// Lay the panel's child slots out at SCREEN coordinates (the ControlPanel endgame).
+    /// The legacy panel arranged them in content space and shifted at aggregate time;
+    /// applying the scroll offset at layout time means hit-testing, dispatch, text, and
+    /// popovers all see real positions. Runs from apply_layout — every rebuild — which is
+    /// also what re-arranges after a scroll (the wheel handler sets needs_rebuild).
+    fn arrange_control_panel(&mut self) {
+        let s = self.roster.gallery_mut();
+        let (x, y, w, h) = s.control_panel.rect();
+        let padding = cce_ui::layout::control_panel_padding();
+        let gap = cce_ui::layout::control_panel_gap();
+        // 12px reserved for the scrollbar track, like the legacy arrangement.
+        let mut col = cce_ui::widget::ColumnLayout::new(x, y, w - 12.0, gap, padding);
+
+        let create_p: *mut (dyn WidgetHost + 'static) = &mut s.create_window_btn;
+        let tile_p: *mut (dyn WidgetHost + 'static) = &mut s.tile_windows_btn;
+        col.add_row(&[create_p, tile_p], 28.0, 12.0);
+        let width_p: *mut (dyn WidgetHost + 'static) = &mut s.width_spin;
+        let height_p: *mut (dyn WidgetHost + 'static) = &mut s.height_spin;
+        col.add_row(&[width_p, height_p], 42.0, 12.0);
+        col.add_widget(&mut s.window_type_dd, 44.0);
+        col.add_widget(&mut s.window_shape_dd, 44.0);
+        col.add_widget(&mut s.opacity_toggle, 28.0);
+        col.add_widget(&mut s.enable_toggle, 28.0);
+        col.add_widget(&mut s.transparency_label, 12.0);
+        col.add_widget(&mut s.transparency_slider, 20.0);
+        col.add_widget(&mut s.elements_section, 20.0);
+        let bp_p: *mut (dyn WidgetHost + 'static) = &mut s.backplate_toggle;
+        let mb_p: *mut (dyn WidgetHost + 'static) = &mut s.menubar_toggle;
+        let sb_p: *mut (dyn WidgetHost + 'static) = &mut s.statusbar_toggle;
+        col.add_row(&[bp_p, mb_p, sb_p], 28.0, 10.0);
+        col.add_widget(&mut s.border_section, 20.0);
+        col.add_widget(&mut s.bevel_toggle, 28.0);
+        let bw_p: *mut (dyn WidgetHost + 'static) = &mut s.border_width_spin;
+        let bd_p: *mut (dyn WidgetHost + 'static) = &mut s.bevel_depth_spin;
+        col.add_row(&[bw_p, bd_p], 42.0, 12.0);
+        col.add_widget(&mut s.bevel_shape_btn, 28.0);
+
+        let total_h = col.current_y();
+        s.control_panel.scroll_box.update_bounds(total_h, y, h);
+
+        // Content coordinates -> screen coordinates.
+        let scroll_y = s.control_panel.scroll_box.scroll_y;
+        if scroll_y != 0.0 {
+            for &ci in CP_CHILDREN.iter() {
+                let w = self.roster.get_dyn_mut(ci);
+                let (cx, cy, cw, ch) = w.rect();
+                w.set_rect(cx, cy - scroll_y, cw, ch);
+            }
         }
     }
 
@@ -979,15 +1241,6 @@ cascades in cce."
         };
 
         if !is_child {
-            let child_ptrs: Vec<*mut (dyn WidgetHost + 'static)> = [15, 16, 21, 22, 17, 19, 20, 23, 25, 26, 38, 39, 40, 41, 42, 43, 44, 45, 48]
-                .iter()
-                .map(|&idx| state.roster.get_dyn_mut(idx) as *mut (dyn WidgetHost + 'static))
-                .collect();
-            let cp = state.roster.get_dyn_mut(51).as_any_mut().downcast_mut::<ControlPanel>().expect("widget 51 must be a ControlPanel");
-            for ptr in child_ptrs {
-                cp.add_child(ptr);
-            }
-
             // Link page selector (46) under StatusBar (2) — the old set_parent + add_child
             // pair as the one tree link it always was (6bd batch 4).
             let statusbar_ptr = state.roster.get_dyn_mut(2) as *mut (dyn WidgetHost + 'static);
@@ -1111,9 +1364,6 @@ cascades in cce."
         for i in 0..self.roster.len() {
             let w = self.roster.get_dyn_mut(i);
             if is_visible(i) {
-                if is_control_panel_child(i) {
-                    continue;
-                }
                 if w.tick(dt, &mut self.ui_context) {
                     changed = true;
                     if self.is_child && self.child_type.as_deref() == Some("Ramp") && i == 1 {
@@ -1140,6 +1390,12 @@ cascades in cce."
         // rebuild_text_items cache and its 14 invalidation call sites are gone).
         use cce_ui::scene::layout::Rect;
         self.register_roster();
+        // Panel children follow the scroll offset at LAYOUT time (the dissolved panel
+        // shifted at aggregate time): re-arrange every frame so a wheel scroll moves the
+        // content on the frame it repaints. Idempotent and cheap (~20 set_rects).
+        if !self.is_child {
+            self.arrange_control_panel();
+        }
         if (self.width - size.width as f32).abs() > 0.001 || (self.height - size.height as f32).abs() > 0.001 || (self.scale - scale).abs() > 0.001 {
             self.width = size.width as f32;
             self.height = size.height as f32;
@@ -1251,6 +1507,78 @@ cascades in cce."
             }
         }
 
+        // ── ControlPanel children (the dissolved panel's aggregate, app-side): the slots
+        // are at real screen coordinates now; the legacy clamp-to-viewport, partial-clip
+        // radius zeroing, and solid-border synthesis/inset rules apply verbatim, and the
+        // scrollbar draws last, over the children, like the aggregate did. ──
+        if !self.is_child && self.current_page == Page::Windows {
+            let (panel_x, panel_y, panel_w, panel_h) = self.roster.gallery().control_panel.rect();
+            let _ = (panel_x, panel_w);
+            let y_start = panel_y;
+            let y_end = panel_y + panel_h;
+            for &ci in CP_CHILDREN.iter() {
+                let w = self.roster.get_dyn(ci);
+                let solid_border_opt = if w.type_name() == "Toggle" { None } else { w.solid_border() };
+                let (cx, cy, cw, ch) = w.rect();
+
+                if let Some((b_color, _thickness)) = solid_border_opt {
+                    let cy_top = cy;
+                    let cy_bottom = cy + 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 (child_r, child_corners) = w.corner_style();
+                            let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
+                                0.0
+                            } else {
+                                child_r
+                            };
+                            push_rounded(&mut pc, cx, visible_top, cw, visible_h, radii_adjusted, b_color, child_corners);
+                        }
+                    }
+                }
+
+                for (qx, qy, qw, qh, qr, qc, qcorners) in w.all_rounded_quads(&self.ui_context) {
+                    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_top = ry;
+                    let qy_bottom = ry + 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
+                            };
+                            push_rounded(&mut pc, rx, visible_top, rw, visible_h, radii_adjusted, qc, qcorners);
+                        }
+                    }
+                }
+            }
+            for (sx, sy, sw, sh, sc) in self.roster.gallery().control_panel.scroll_box.extra_quads() {
+                push_rounded(&mut pc, sx, sy, sw, sh, 0.0, sc, (false, false, false, false));
+            }
+        }
+
         // ── Plain geometry (the legacy view() body) ──
         for i in 0..self.roster.len() {
             let w = self.roster.get_dyn(i);
@@ -1282,6 +1610,36 @@ cascades in cce."
             }
         }
 
+        // ── ControlPanel children, plain decorations (the dissolved aggregate_plain):
+        // the child's own rounded background is skipped like the legacy rule, the rest
+        // clamps to the panel viewport. ──
+        if !self.is_child && self.current_page == Page::Windows {
+            let (_panel_x, panel_y, _panel_w, panel_h) = self.roster.gallery().control_panel.rect();
+            let y_start = panel_y;
+            let y_end = panel_y + panel_h;
+            for &ci in CP_CHILDREN.iter() {
+                let w = self.roster.get_dyn(ci);
+                let (cx, cy, cw, ch) = w.rect();
+                let has_rounded = w.corner_style().1 != (false, false, false, false);
+                let has_bg = w.color()[3].abs() > 0.001;
+                for (qx, qy, qw, qh, qc) in w.all_quads(&self.ui_context) {
+                    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_top = qy;
+                    let qy_bottom = qy + 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 {
+                            pc.quad(Rect { x: qx, y: visible_top, width: qw, height: visible_h }, qc);
+                        }
+                    }
+                }
+            }
+        }
+
         // ── Popovers: geometry then labels, in-frame, on top of everything ──
         let mut popover_pc = cce_ui::layout::PopoverCollector::new();
         for i in 0..self.roster.len() {
@@ -1289,9 +1647,6 @@ cascades in cce."
             if !self.is_widget_visible(i) {
                 continue;
             }
-            if is_control_panel_child(i) {
-                continue;
-            }
             if w.popover_rect().is_some() {
                 w.render_popover(&mut popover_pc);
             }
@@ -1354,18 +1709,22 @@ cascades in cce."
             if !self.is_widget_visible(i) {
                 continue;
             }
-            if is_control_panel_child(i) {
-                continue;
-            }
             // Text via the paint walk (not the legacy getters): same labels with the
             // widget's content font and clip bounds; the popover cull stays on the prim
-            // coordinates. ControlPanel children are covered by the panel's walk descent,
-            // and any widget with a ui-tree parent (the page selector under the status
-            // bar) is covered by that parent's descent — walking it here too would emit
-            // its text twice.
+            // coordinates. Any widget with a ui-tree parent (the page selector under the
+            // status bar) is covered by that parent's descent — walking it here too would
+            // emit its text twice. ControlPanel children clamp their bounds to the panel
+            // viewport (the dissolved scrolled_child_labels rule, minus the shift — the
+            // slots sit at real screen coordinates now).
             if self.ui_context.tree.parent_ptr(w.base().id()).is_some() {
                 continue;
             }
+            let cp_clip = if is_control_panel_child(i) {
+                let (px_, py_, pw_, ph_) = self.roster.gallery().control_panel.rect();
+                Some([px_, py_, px_ + pw_, py_ + ph_])
+            } else {
+                None
+            };
             let mut scratch = cce_ui::scene::paint::PaintCtx::new();
             cce_ui::scene::painter::append_widget_text(&self.ui_context, w, &mut scratch);
             for item in scratch.finish().items {
@@ -1373,6 +1732,13 @@ cascades in cce."
                     if in_any_popover(x, y) {
                         continue;
                     }
+                    let bounds = match (bounds, cp_clip) {
+                        (Some([l, t, r, b]), Some([pl, pt, pr, pb])) => {
+                            Some([l.max(pl), t.max(pt), r.min(pr), b.min(pb)])
+                        }
+                        (None, Some(clip)) => Some(clip),
+                        (b, None) => b,
+                    };
                     pc.text_with(text, x, y, font_size, color, font, bounds);
                 }
             }
@@ -1582,9 +1948,6 @@ cascades in cce."
                 if !is_visible(i) {
                     continue;
                 }
-                if is_control_panel_child(i) {
-                    continue;
-                }
                 let root = self.roster.get_dyn(i).base().id();
                 if self.ui_context.propagate_event(&mv, root) {
                     changed = true;
@@ -1630,16 +1993,29 @@ cascades in cce."
 
         if state == ElementState::Pressed {
             let mut clicked_idx = None;
-            for i in (0..self.roster.len()).rev() {
-                if !is_visible(i) {
-                    continue;
-                }
-                if is_control_panel_child(i) {
-                    continue;
+            // Panel children first: they sit inside the panel's rect, so the plain
+            // reverse scan below (where the panel's higher index wins) must only see
+            // the panel when no child claims the point.
+            if !self.is_child && self.current_page == Page::Windows && self.cp_gate(lx, ly) {
+                for &ci in CP_CHILDREN.iter() {
+                    if self.roster.get_dyn_mut(ci).hit_test(lx, ly, &self.ui_context) {
+                        clicked_idx = Some(ci);
+                        break;
+                    }
                 }
-                if self.roster.get_dyn_mut(i).hit_test(lx, ly, &self.ui_context) {
-                    clicked_idx = Some(i);
-                    break;
+            }
+            if clicked_idx.is_none() {
+                for i in (0..self.roster.len()).rev() {
+                    if !is_visible(i) {
+                        continue;
+                    }
+                    if is_control_panel_child(i) {
+                        continue;
+                    }
+                    if self.roster.get_dyn_mut(i).hit_test(lx, ly, &self.ui_context) {
+                        clicked_idx = Some(i);
+                        break;
+                    }
                 }
             }
             if button == MouseButton::Left {
@@ -1661,7 +2037,7 @@ cascades in cce."
                 if press_handled {
                     changed = true;
                 }
-                if button == MouseButton::Left && !press_handled && self.roster.get_dyn(i).draggable() {
+                if button == MouseButton::Left && !press_handled && self.roster.draggable(i) {
                     let id = self.roster.get_dyn(i).base().id();
                     self.ui_context.drag_target = Some(id);
                 }
@@ -1677,11 +2053,12 @@ cascades in cce."
                 changed = true;
             }
             let ev = cce_ui::widget::Event::MouseButton { button, state, x: lx, y: ly, local_x: lx, local_y: ly };
+            let cp_release_ok = !self.is_child && self.cp_gate(lx, ly);
             for i in 0..self.roster.len() {
                 if !is_visible(i) {
                     continue;
                 }
-                if is_control_panel_child(i) {
+                if is_control_panel_child(i) && !cp_release_ok {
                     continue;
                 }
                 let root = self.roster.get_dyn(i).base().id();
@@ -1966,11 +2343,25 @@ full screen background.",
             }
         };
         let ev = cce_ui::widget::Event::MouseWheel { delta: *delta, x: lx, y: ly, local_x: lx, local_y: ly };
+        // Panel scroll first (legacy: the panel's scroll frame consumed the wheel before
+        // its children saw it); a consumed wheel never reaches the panel's children.
+        let mut cp_took_wheel = false;
+        if !self.is_child && is_visible(51) {
+            let root = self.roster.get_dyn(51).base().id();
+            if self.ui_context.propagate_event(&ev, root) {
+                changed = true;
+                cp_took_wheel = true;
+            }
+        }
+        let cp_wheel_ok = !self.is_child && self.cp_gate(lx, ly);
         for i in 0..self.roster.len() {
+            if i == 51 {
+                continue;
+            }
             if !is_visible(i) {
                 continue;
             }
-            if is_control_panel_child(i) {
+            if is_control_panel_child(i) && (cp_took_wheel || !cp_wheel_ok) {
                 continue;
             }
             let root = self.roster.get_dyn(i).base().id();
@@ -2025,6 +2416,8 @@ full screen background.",
                 if !is_visible(i) {
                     continue;
                 }
+                // Panel children take keys only through the focused path above (the
+                // legacy panel never forwarded keys).
                 if is_control_panel_child(i) {
                     continue;
                 }
diff --git a/src/ti_widgets.rs b/src/ti_widgets.rs
index cb04088..9ab6f9c 100644
--- a/src/ti_widgets.rs
+++ b/src/ti_widgets.rs
@@ -17,11 +17,16 @@ pub struct ControlPanel {
     y: f32,
     w: f32,
     h: f32,
-    pub children: Vec<*mut (dyn WidgetHost + 'static)>,
     pub scroll_box: ScrollBox,
-    pub active_drag_widget: Option<*mut (dyn WidgetHost + 'static)>,
 }
 
+/// The panel is scroll chrome only (the ControlPanel endgame): background, borders, and
+/// the ScrollBox machinery. Its former stored child pointers, label-matched arrangement,
+/// aggregate views, and dyn event/tick/drag forwarding are DISSOLVED into the app —
+/// main.rs lays the child slots out at SCREEN (scrolled) coordinates
+/// (`arrange_control_panel`), emits their geometry/text clamped to the panel viewport in
+/// `display_list`, and dispatches them as ordinary routed roots. No `*mut dyn` storage,
+/// no dummy contexts, no scroll-translated coordinates anywhere.
 impl ControlPanel {
     pub fn new() -> Adapted<ControlPanel> {
         let mut sb = ScrollBox::new();
@@ -32,234 +37,14 @@ impl ControlPanel {
             y: 0.0,
             w: 0.0,
             h: 0.0,
-            children: Vec::new(),
             scroll_box: sb,
-            active_drag_widget: None,
         })
     }
 
-    pub fn add_child(&mut self, child: *mut (dyn WidgetHost + 'static)) {
-        self.children.push(child);
-    }
-
     /// The laid-out rect, mirrored from the adapter by `Layout::rect_assigned`.
-    fn rect(&self) -> (f32, f32, f32, f32) {
+    pub fn rect(&self) -> (f32, f32, f32, f32) {
         (self.x, self.y, self.w, self.h)
     }
-
-    /// First open child popover, in scrolled (screen) coordinates — the old
-    /// `WidgetHost::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().y;
-                child.base_mut().y = old_y - scroll_y;
-                let res = child.popover_rect();
-                child.base_mut().y = old_y;
-                if res.is_some() {
-                    return res;
-                }
-            }
-        }
-        None
-    }
-
-    /// The subtree's rounded view (the old `WidgetHost::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 (child_r, child_corners) = child.corner_style();
-                            let radii_adjusted = if visible_top > cy_top || visible_bottom < cy_bottom {
-                                0.0
-                            } else {
-                                child_r
-                            };
-                            quads.push((
-                                cx,
-                                visible_top,
-                                cw,
-                                visible_h,
-                                radii_adjusted,
-                                b_color,
-                                child_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));
-                        }
-                    }
-                }
-            }
-        }
-
-        // 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)));
-        }
-
-        quads
-    }
-
-    /// The subtree's plain view (the old `WidgetHost::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 {
-                let child = &**child_ptr;
-                let (cx, cy, cw, ch) = child.rect();
-                let has_rounded = child.corner_style().1 != (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));
-                        }
-                    }
-                }
-            }
-        }
-
-        quads.extend(self.scroll_box.extra_quads());
-        quads
-    }
-
-    /// The children's arcs with the scroll shift (the old `WidgetHost::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
-    }
-
-    /// 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
-    }
 }
 
 impl cce_ui::widget::Layout for ControlPanel {
@@ -274,152 +59,7 @@ impl cce_ui::widget::Layout for ControlPanel {
         self.y = rect.y;
         self.w = rect.width;
         self.h = rect.height;
-    }
-
-    // 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. (The dummy-ctx child re-parenting onto the host adapter is gone,
-    // 6bd: every effect of it was discarded with the dummy ctx.)
-    fn arrange_children(&mut self, rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {
-        let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
-
-        self.scroll_box.set_rect(x, y, w, h);
-
-
-        let padding = cce_ui::layout::control_panel_padding();
-        let gap = cce_ui::layout::control_panel_gap();
-        let mut col = ColumnLayout::new(x, y, w - 12.0, gap, padding); // 12px reserved for scrollbar track
-
-        unsafe {
-            let mut create_btn: Option<*mut dyn WidgetHost> = None;
-            let mut tile_btn: Option<*mut dyn WidgetHost> = None;
-            let mut opacity_toggle: Option<*mut dyn WidgetHost> = None;
-            let mut enable_toggle: Option<*mut dyn WidgetHost> = None;
-            let mut slider: Option<*mut dyn WidgetHost> = None;
-            let mut slider_label: Option<*mut dyn WidgetHost> = None;
-            let mut type_dd: Option<*mut dyn WidgetHost> = None;
-            let mut shape_dd: Option<*mut dyn WidgetHost> = None;
-            let mut border_style_dd: Option<*mut dyn WidgetHost> = None;
-            let mut width_spin: Option<*mut dyn WidgetHost> = None;
-            let mut height_spin: Option<*mut dyn WidgetHost> = None;
-            let mut backplate_toggle: Option<*mut dyn WidgetHost> = None;
-            let mut menubar_toggle: Option<*mut dyn WidgetHost> = None;
-            let mut statusbar_toggle: Option<*mut dyn WidgetHost> = None;
-            let mut border_sec: Option<*mut dyn WidgetHost> = None;
-            let mut bevel_toggle: Option<*mut dyn WidgetHost> = None;
-            let mut border_width_spin: Option<*mut dyn WidgetHost> = None;
-            let mut bevel_depth_spin: Option<*mut dyn WidgetHost> = None;
-            let mut win_sec: Option<*mut dyn WidgetHost> = None;
-            let mut bevel_shape_btn: Option<*mut dyn WidgetHost> = None;
-
-            for &child_ptr in &self.children {
-                let child = &mut *child_ptr;
-                let label = child.base().label.as_ref().map(|s| s.as_str()).unwrap_or("");
-                match label {
-                    "Create Window" => create_btn = Some(child_ptr),
-                    "Tile Windows" => tile_btn = Some(child_ptr),
-                    "Opacity" => opacity_toggle = Some(child_ptr),
-                    "Enable" => enable_toggle = Some(child_ptr),
-                    "Transparency Level" => slider_label = Some(child_ptr),
-                    "Border" => border_sec = Some(child_ptr),
-                    "Window Elements" => win_sec = Some(child_ptr),
-                    "Window Type" => type_dd = Some(child_ptr),
-                    "Window Shape" => shape_dd = Some(child_ptr),
-                    "Border Style" => border_style_dd = Some(child_ptr),
-                    "Width" => width_spin = Some(child_ptr),
-                    "Height" => height_spin = Some(child_ptr),
-                    "Backplate" => backplate_toggle = Some(child_ptr),
-                    "MenuBar" => menubar_toggle = Some(child_ptr),
-                    "StatusBar" => statusbar_toggle = Some(child_ptr),
-                    "Bevel" => bevel_toggle = Some(child_ptr),
-                    "Border Width" => border_width_spin = Some(child_ptr),
-                    "Bevel Depth" => bevel_depth_spin = Some(child_ptr),
-                    "Bevel Shape..." => bevel_shape_btn = Some(child_ptr),
-                    _ => {
-                        if child.base().label.is_none() {
-                            slider = Some(child_ptr);
-                        }
-                    }
-                }
-            }
-
-            if let (Some(c), Some(t)) = (create_btn, tile_btn) {
-                col.add_row(&[c, t], 28.0, 12.0);
-            } else {
-                if let Some(c) = create_btn {
-                    col.add_widget(&mut *c, 28.0);
-                }
-                if let Some(t) = tile_btn {
-                    col.add_widget(&mut *t, 28.0);
-                }
-            }
-
-            if let (Some(w_sp), Some(h_sp)) = (width_spin, height_spin) {
-                col.add_row(&[w_sp, h_sp], 42.0, 12.0);
-            } else {
-                if let Some(w_sp) = width_spin {
-                    col.add_widget(&mut *w_sp, 42.0);
-                }
-                if let Some(h_sp) = height_spin {
-                    col.add_widget(&mut *h_sp, 42.0);
-                }
-            }
-
-            if let Some(t_dd) = type_dd {
-                col.add_widget(&mut *t_dd, 44.0);
-            }
-            if let Some(s_dd) = shape_dd {
-                col.add_widget(&mut *s_dd, 44.0);
-            }
-            if let Some(op_t) = opacity_toggle {
-                col.add_widget(&mut *op_t, 28.0);
-            }
-            if let Some(en_t) = enable_toggle {
-                col.add_widget(&mut *en_t, 28.0);
-            }
-            if let Some(sl_lbl) = slider_label {
-                col.add_widget(&mut *sl_lbl, 12.0);
-            }
-            if let Some(sl) = slider {
-                col.add_widget(&mut *sl, 20.0);
-            }
-
-            if let Some(w_s) = win_sec {
-                col.add_widget(&mut *w_s, 20.0);
-            }
-            let toggles = [backplate_toggle, menubar_toggle, statusbar_toggle];
-            let active_toggles: Vec<*mut dyn WidgetHost> = toggles.iter().filter_map(|&t| t).collect();
-            if !active_toggles.is_empty() {
-                col.add_row(&active_toggles, 28.0, 10.0);
-            }
-
-            if let Some(b_s) = border_sec {
-                col.add_widget(&mut *b_s, 20.0);
-            }
-            if let Some(bs_dd) = border_style_dd {
-                col.add_widget(&mut *bs_dd, 44.0);
-            }
-            if let Some(bev_t) = bevel_toggle {
-                col.add_widget(&mut *bev_t, 28.0);
-            }
-
-            if let (Some(bw_sp), Some(bd_sp)) = (border_width_spin, bevel_depth_spin) {
-                col.add_row(&[bw_sp, bd_sp], 42.0, 12.0);
-            } else {
-                if let Some(bw_sp) = border_width_spin {
-                    col.add_widget(&mut *bw_sp, 42.0);
-                }
-                if let Some(bd_sp) = bevel_depth_spin {
-                    col.add_widget(&mut *bd_sp, 42.0);
-                }
-            }
-            if let Some(bs_btn) = bevel_shape_btn {
-                col.add_widget(&mut *bs_btn, 28.0);
-            }
-
-            let total_h = col.current_y();
-            self.scroll_box.update_bounds(total_h, y, h);
-        }
+        self.scroll_box.set_rect(rect.x, rect.y, rect.width, rect.height);
     }
 }
 
@@ -433,162 +73,45 @@ impl cce_ui::widget::Paint for ControlPanel {
         Some((12.0, (true, true, true, true)))
     }
 
-    // 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
-    }
-
+    /// Background + borders, as radius-0 rounded prims so they ride the adapter's
+    /// rounded-tuple bridge exactly where the legacy aggregate emitted them (the app
+    /// emits the children and the scrollbar after these, in the legacy order).
     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);
-        }
-    }
-
-    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 {
-                if (**child_ptr).popover_rect().is_some() {
-                    let child = &mut **child_ptr;
-                    let old_y = child.base().y;
-                    child.base_mut().y = old_y - scroll_y;
-                    child.render_popover(pc);
-                    child.base_mut().y = old_y;
-                }
-            }
-        }
+        let (x, y, w, h) = self.rect();
+        let none = (false, false, false, false);
+        pc.rounded_rect(Rect { x, y, width: w, height: h }, 0.0, none, colors::control_panel_color());
+        let border_color = colors::control_panel_border_color();
+        pc.rounded_rect(Rect { x, y, width: w, height: 1.0 }, 0.0, none, border_color);
+        pc.rounded_rect(Rect { x, y: y + h - 1.0, width: w, height: 1.0 }, 0.0, none, border_color);
+        pc.rounded_rect(Rect { x, y, width: 1.0, height: h }, 0.0, none, border_color);
+        pc.rounded_rect(Rect { x: x + w - 1.0, y, width: 1.0, height: h }, 0.0, none, border_color);
     }
 }
 
 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
-    }
-
-    // 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 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;
-                }
-
-                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) {
-                            let ev = cce_ui::widget::Event::MouseButton { button: *button, state: *state, x: *px, y: py_translated, local_x: *px, local_y: py_translated };
-                            if (**child_ptr).handle_event(&ev, ctx) {
-                                if *state == ElementState::Pressed && (**child_ptr).draggable() {
-                                    self.active_drag_widget = Some(*child_ptr);
-                                }
-                                return true;
-                            }
-                        }
-                    }
-                }
-                false
+                self.scroll_box.mouse_input(*button, *state, *px, *py, ctx)
             }
             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 {
-                        let ev = cce_ui::widget::Event::PointerMove { x: *px, y: py_translated, local_x: *px, local_y: py_translated };
-                        if (**child_ptr).handle_event(&ev, ctx) {
-                            changed = true;
-                        }
-                    }
-                }
-                changed
+                self.scroll_box.cursor_moved(*px, *py, ctx)
             }
             Event::MouseWheel { delta, x: px, y: py, .. } => {
-                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 {
-                        let ev = cce_ui::widget::Event::MouseWheel { delta: *delta, x: *px, y: py_translated, local_x: *px, local_y: py_translated };
-                        if (**child_ptr).handle_event(&ev, ctx) {
-                            return true;
-                        }
-                    }
-                }
-                false
+                self.scroll_box.mouse_wheel(delta, *px, *py, ctx)
             }
             _ => false,
         }
     }
 
     fn draggable(&self, _rect: Rect) -> bool {
-        self.scroll_box.draggable() || self.active_drag_widget.is_some()
+        self.scroll_box.draggable()
     }
 
     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;
-        }
-
-        let scroll_y = self.scroll_box.scroll_y;
-        let py_translated = py + scroll_y;
-        if let Some(child_ptr) = self.active_drag_widget {
-            // Dyn child: the Drag* events map onto the Input drag hooks in handle_event
-            // (6bd collapse); the ctx is unused on that path.
-            let mut dummy = cce_ui::context::UiContext::new();
-            let ev = cce_ui::widget::Event::DragStart { start_x: px, start_y: py_translated };
-            unsafe {
-                (*child_ptr).handle_event(&ev, &mut dummy);
-            }
         }
     }
 
@@ -596,41 +119,16 @@ impl cce_ui::widget::Input for ControlPanel {
         if self.scroll_box.draggable() {
             return self.scroll_box.drag_update(px, py);
         }
-
-        let scroll_y = self.scroll_box.scroll_y;
-        let py_translated = py + scroll_y;
-        if let Some(child_ptr) = self.active_drag_widget {
-            let mut dummy = cce_ui::context::UiContext::new();
-            let ev = cce_ui::widget::Event::DragUpdate { dx: 0.0, dy: 0.0, x: px, y: py_translated, local_x: px, local_y: py_translated };
-            unsafe {
-                return (*child_ptr).handle_event(&ev, &mut dummy);
-            }
-        }
         false
     }
 
     fn drag_end(&mut self) {
         self.scroll_box.drag_end();
-        if let Some(child_ptr) = self.active_drag_widget {
-            let mut dummy = cce_ui::context::UiContext::new();
-            unsafe {
-                (*child_ptr).handle_event(&cce_ui::widget::Event::DragEnd, &mut dummy);
-            }
-            self.active_drag_widget = None;
-        }
     }
 
     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).tick(dt, ctx) {
-                    changed = true;
-                }
-            }
-        }
-        changed
+        self.scroll_box.tick(dt, ctx)
     }
 }