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