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

commit6c3d74e4c826ecc32dc37efe4f64d72b3c7bdd26
parent079c469b2d
authorLucas Galante <[email protected]>
date2026-07-13 11:13
refactor(widget)!: draggable/is_dragging leave WidgetHost — 49->47 (the CP endgame)

The ControlPanel dissolution removed their last stored-child-pointer
consumer; the drag queries are inherent Adapted<W> reads now, and the
index-driven rosters route them through per-slot matches.

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

 docs/rfc-core-rebuild.md            | 36 ++++++++++++++++++++++++++++++++++++
 src/context.rs                      |  4 ++--
 src/widget/container/spreadsheet.rs |  8 ++++----
 src/widget/display/float3.rs        |  4 ++--
 src/widget/display/graph.rs         |  4 ++--
 src/widget/display/node.rs          |  4 ++--
 src/widget/input/slider.rs          |  2 +-
 src/widget/mod.rs                   |  6 ++++--
 src/widget/model.rs                 | 17 +++++++++++------
 9 files changed, 64 insertions(+), 21 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 383a76b..81030b2 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -1917,6 +1917,42 @@ Constraint respected: **each crate still builds standalone** — the new core is
        `draggable`/`is_dragging` trait methods still have this
        cascade + TI's ControlPanel as dyn consumers — they leave the
        trait with the CP endgame.
+       **THE CONTROLPANEL ENDGAME — DONE (2026-07-13). The last
+       stored child-pointer surface is gone, and `draggable`/
+       `is_dragging` left the trait (49→47).** TI's ControlPanel is
+       DISSOLVED to scroll chrome (~120 lines: ScrollBox + bg/border
+       paint + drag hooks): its `Vec<*mut dyn>` children, label-
+       matched arrangement, aggregate views, dummy-ctx event/tick/
+       drag forwarding, and scroll-translated coordinates are all
+       deleted. The app owns the panel now: `arrange_control_panel`
+       lays the child slots at SCREEN coordinates (scroll offset
+       applied at layout time, re-run every frame — the wheel moves
+       content on the frame it repaints); `display_list` emits child
+       geometry/text clamped to the panel viewport with the legacy
+       partial-clip radius-zeroing and border-inset rules; children
+       dispatch as ordinary routed roots. THE GATE THE DISSOLUTION
+       REQUIRES: children at real rects are hit-testable even when
+       clipped below the fold — `cp_gate` (panel rect ∪ open child
+       popovers, the legacy `ControlPanel::hit`) gates the press
+       pre-scan, release broadcast, and wheel; keys stay
+       focused-path-only; the panel takes the wheel before its
+       children (legacy scroll-frame order). BUG THE GATE FIXED
+       LIVE: a fold-hidden StatusBar toggle stole the page-selector
+       press. FOUND: the legacy panel double-drew its scrollbar
+       (rounded AND plain aggregate views — the 6p/6v class);
+       single-drawn now, thumb correctly dimmer. `draggable`/
+       `is_dragging` became inherent `Adapted` reads; the two
+       index-driven rosters (TI 53+5 slots incl. child-mode enum
+       variants, designer 17) route them through generated per-slot
+       matches; 14 UFCS test forms became dot calls. Verified live:
+       Windows page A/B vs pre-dissolution baseline (static +
+       scrolled + popover states — masks empty except the scrollbar
+       single-draw strip), dropdown item select updates the Surface
+       Info description end-to-end, spinbox +/- increments, wheel
+       scrolls with content following, page switching intact both
+       ways, Controls page unchanged, child mode alive; 28-target
+       suite. Scrollbar thumb drag: user spot-check (held drags not
+       headless-drivable).
     5. window_runner render plumbing + remaining `as_ptr` sites; then
        the `Element` + `Adapted` endgame (own design pass).
     Stored-pointer state remaining after slices 1–3, all deliberate:
diff --git a/src/context.rs b/src/context.rs
index 04a15da..d3458ee 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -853,7 +853,7 @@ mod tests {
             local_y: 15.0,
         };
         assert!(ctx.propagate_event(&press, id), "press in the track arms the drag");
-        assert!(WidgetHost::is_dragging(&slider));
+        assert!(slider.is_dragging());
         let v0 = slider.value;
 
         // First move past the 3px threshold starts the drag; the next one updates it.
@@ -877,7 +877,7 @@ mod tests {
             local_y: 15.0,
         };
         ctx.propagate_event(&release, id);
-        assert!(!WidgetHost::is_dragging(&slider), "DragEnd reached Input::drag_end");
+        assert!(!slider.is_dragging(), "DragEnd reached Input::drag_end");
         assert!(!ctx.is_dragging);
     }
 
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index b63f04a..e7f9b37 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -417,20 +417,20 @@ mod tests {
         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 124.0 };
 
         // content 1200, viewport 100 -> overflowing, so the host may drag it.
-        assert!(WidgetHost::draggable(&s));
+        assert!(s.draggable());
 
         // Press on the scrollbar track (x >= 200-6-2-4): thumb jumps, drag engages.
         s.drag_begin(195.0, 80.0);
-        assert!(WidgetHost::is_dragging(&s));
+        assert!(s.is_dragging());
         assert!(s.drag_update(195.0, 110.0), "thumb drag scrolls");
         let dragged_to = s.inner().geom(rect).unwrap().scroll;
         assert!(dragged_to > 0.0);
         s.drag_end();
-        assert!(!WidgetHost::is_dragging(&s));
+        assert!(!s.is_dragging());
 
         // A body press (left of the scrollbar) engages no drag.
         s.drag_begin(50.0, 60.0);
-        assert!(!WidgetHost::is_dragging(&s), "body press is not a scrollbar drag");
+        assert!(!s.is_dragging(), "body press is not a scrollbar drag");
 
         // End key jumps to max; Home returns to zero. (Keys route via keyboard_input.)
         let end = crate::widget::KeyEvent {
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 2a95a01..0f7daa8 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -352,10 +352,10 @@ mod tests {
         // Track press starts a drag; drag_update moves the value; release ends it.
         let track_y = rows[0].1 + 8.0;
         assert!(f.mouse_input(MouseButton::Left, ElementState::Pressed, 150.0, track_y, &mut ctx));
-        assert!(WidgetHost::is_dragging(&f));
+        assert!(f.is_dragging());
         f.drag_update(260.0, track_y);
         assert!(f.values[0] > 0.5, "drag right raises the value");
         f.drag_end();
-        assert!(!WidgetHost::is_dragging(&f));
+        assert!(!f.is_dragging());
     }
 }
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 030078c..07ec0b6 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -1052,14 +1052,14 @@ mod tests {
         // Node a occupies (100, 100, 80, 40). Press its body (away from ports/toggle).
         assert!(g.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 120.0, &mut ctx));
         assert_eq!(g.selected_node(), Some(0));
-        assert!(WidgetHost::is_dragging(&g) && WidgetHost::draggable(&g));
+        assert!(g.is_dragging() && g.draggable());
 
         // Drag one grid step right (step_x = 100): snap puts the node at column 1, but cell
         // (1, 0) is free so it lands there.
         g.drag_begin(110.0, 120.0);
         assert!(g.drag_update(210.0, 120.0));
         assert!(g.mouse_input(MouseButton::Left, ElementState::Released, 210.0, 120.0, &mut ctx));
-        assert!(!WidgetHost::is_dragging(&g));
+        assert!(!g.is_dragging());
         assert_eq!(g.get_nodes()[0].position, (1.0, 0.0));
 
         // An empty-space press clears the selection and is NOT consumed (legacy contract).
diff --git a/src/widget/display/node.rs b/src/widget/display/node.rs
index 183032b..47858b0 100644
--- a/src/widget/display/node.rs
+++ b/src/widget/display/node.rs
@@ -267,11 +267,11 @@ mod tests {
 
         // A press outside the toggle starts a drag; reposition snaps to the drag origin.
         assert!(node.mouse_input(MouseButton::Left, ElementState::Pressed, 110.0, 110.0, &mut ctx));
-        assert!(WidgetHost::is_dragging(&node));
+        assert!(node.is_dragging());
         assert!(node.drag_update(150.0, 130.0));
         assert_eq!(WidgetHost::rect(&node), (140.0, 120.0, 120.0, 40.0), "moved by the pointer delta");
         assert!(node.mouse_input(MouseButton::Left, ElementState::Released, 150.0, 130.0, &mut ctx));
-        assert!(!WidgetHost::is_dragging(&node));
+        assert!(!node.is_dragging());
     }
 
     #[test]
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index de98dc5..f84020f 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -752,7 +752,7 @@ fn probe_slider_bridge() {
             &Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, x: 50.0, y: 10.0, local_x: 50.0, local_y: 10.0 },
             id,
         ));
-        assert!(WidgetHost::is_dragging(&sl));
+        assert!(sl.is_dragging());
         assert!(sl.drag_update(80.0, 10.0));
         assert!(sl.inner().value() > 0.5);
         sl.drag_end();
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 09927e9..74fcf71 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -315,8 +315,10 @@ pub trait WidgetHost {
     fn solid_border(&self) -> Option<([f32; 4], f32)> { None }
     fn plate_bevel(&self) -> Option<f32> { None }
 
-    fn is_dragging(&self) -> bool { false }
-    fn draggable(&self) -> bool { false }
+    // `draggable`/`is_dragging` are GONE from the trait (the ControlPanel endgame
+    // removed their last stored-child-pointer consumer): the drag queries are concrete
+    // inherent `Adapted<W>` reads; index-driven rosters (TI, designer) route them
+    // through per-slot matches like the other value drains.
 
     fn label_x_offset(&self) -> f32 {
         let name = self.type_name();
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 9e3724e..4754451 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -730,6 +730,17 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
         self as *mut Self as *mut (dyn WidgetHost + 'static)
     }
 
+    /// Whether a press here may start a drag (off `WidgetHost` — the ControlPanel
+    /// endgame; forwards to the narrow `Input` hook with the laid-out content rect).
+    pub fn draggable(&self) -> bool {
+        Input::draggable(&self.inner, self.content_rect())
+    }
+
+    /// Whether the widget's own drag is live (off `WidgetHost` with `draggable`).
+    pub fn is_dragging(&self) -> bool {
+        Input::is_dragging(&self.inner)
+    }
+
     /// Movement bounds pushed in by hosts (off the `WidgetHost` trait since 6bd — the one
     /// production caller is concrete: designer's network panel).
     pub fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
@@ -1399,12 +1410,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
         self.base.row_x = rx;
         self.base.row_w = rw;
     }
-    fn draggable(&self) -> bool {
-        Input::draggable(&self.inner, self.content_rect())
-    }
-    fn is_dragging(&self) -> bool {
-        Input::is_dragging(&self.inner)
-    }
     fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
         // Legacy value-owning containers healed their children's registry entries every tick
         // (addresses move with the owning struct); same cadence here.