GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(widget)!: the direct-dispatch block collapses into handle_event (6bd)
WidgetHost sheds eight methods (67 -> 59 series count): cursor_moved,
on_cursor_moved, mouse_input, mouse_wheel, keyboard_input, drag_begin,
drag_update, drag_end. Every event delivery now goes through handle_event —
the entry points live on as inherent Adapted<W> methods (in-crate composite
forwards to CONCRETE embedded children resolve unchanged: ramp,
parameters_bg, treelist, dropdown, menu, paginator, breadcrumb,
scroll_box); dyn callers rewrote as handle_event(&Event::...), identical by
construction since the entry points were handle_event forwards and the
drag-fix maps Event::Drag* onto the Input drag hooks.
Also landed, per the census design: keyboard's !visible() gate moved from
the entry point into handle_event's KeyInput arm — hidden focused widgets
now drop keys on the ROUTED path too (previously a hole); the deleted trait
cursor_moved default (coverage-gated hover) became the inherent
Adapted::cursor_moved; the trait's default handle_event now serves test
shims only (base hover on moves + tick).
Verified: 164 tests; workspace builds; settings audio render stream
byte-identical; TI gallery four-state interactive A/B all empty 8% masks;
designer /state identical before and after a canvas click, pixels AE=14.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk
src/widget/container/spreadsheet.rs | 12 +--
src/widget/display/float3.rs | 4 +-
src/widget/display/graph.rs | 4 +-
src/widget/display/node.rs | 2 +-
src/widget/input/slider.rs | 34 ++++----
src/widget/input/spinbox.rs | 4 +-
src/widget/mod.rs | 58 +++----------
src/widget/model.rs | 159 +++++++++++++++++++-----------------
8 files changed, 127 insertions(+), 150 deletions(-)
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 3bab1c2..b63f04a 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -420,16 +420,16 @@ mod tests {
assert!(WidgetHost::draggable(&s));
// Press on the scrollbar track (x >= 200-6-2-4): thumb jumps, drag engages.
- WidgetHost::drag_begin(&mut s, 195.0, 80.0);
+ s.drag_begin(195.0, 80.0);
assert!(WidgetHost::is_dragging(&s));
- assert!(WidgetHost::drag_update(&mut s, 195.0, 110.0), "thumb drag scrolls");
+ 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);
- WidgetHost::drag_end(&mut s);
+ s.drag_end();
assert!(!WidgetHost::is_dragging(&s));
// A body press (left of the scrollbar) engages no drag.
- WidgetHost::drag_begin(&mut s, 50.0, 60.0);
+ s.drag_begin(50.0, 60.0);
assert!(!WidgetHost::is_dragging(&s), "body press is not a scrollbar drag");
// End key jumps to max; Home returns to zero. (Keys route via keyboard_input.)
@@ -441,12 +441,12 @@ mod tests {
ctrl: false,
shift: false,
};
- assert!(WidgetHost::keyboard_input(&mut s, &end, &mut ctx));
+ assert!(s.keyboard_input(&end, &mut ctx));
let g = s.inner().geom(rect).unwrap();
assert_eq!(g.scroll, g.max_scroll);
// Hidden: the focused-widget keyboard path must not consume keys.
s.set_visible(false);
- assert!(!WidgetHost::keyboard_input(&mut s, &end, &mut ctx), "hidden widget ignores keys");
+ assert!(!s.keyboard_input(&end, &mut ctx), "hidden widget ignores keys");
}
}
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 7a74186..2a95a01 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -353,9 +353,9 @@ mod tests {
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));
- WidgetHost::drag_update(&mut f, 260.0, track_y);
+ f.drag_update(260.0, track_y);
assert!(f.values[0] > 0.5, "drag right raises the value");
- WidgetHost::drag_end(&mut f);
+ f.drag_end();
assert!(!WidgetHost::is_dragging(&f));
}
}
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index dbe2d90..030078c 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -1056,8 +1056,8 @@ mod tests {
// 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.
- WidgetHost::drag_begin(&mut g, 110.0, 120.0);
- assert!(WidgetHost::drag_update(&mut g, 210.0, 120.0));
+ 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_eq!(g.get_nodes()[0].position, (1.0, 0.0));
diff --git a/src/widget/display/node.rs b/src/widget/display/node.rs
index d86d644..183032b 100644
--- a/src/widget/display/node.rs
+++ b/src/widget/display/node.rs
@@ -268,7 +268,7 @@ 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!(WidgetHost::drag_update(&mut node, 150.0, 130.0));
+ 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));
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 99b4c1c..7235c6e 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -699,20 +699,20 @@ mod tests {
assert_eq!(rs.values(), (0.2, 0.8));
// Thumb size 18, range 182; low center = 55.4.
- WidgetHost::drag_begin(&mut rs, 55.4, 20.0);
+ rs.drag_begin(55.4, 20.0);
assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
- assert!(WidgetHost::drag_update(&mut rs, 100.9, 20.0));
+ assert!(rs.drag_update(100.9, 20.0));
assert!((rs.values().0 - 0.45).abs() < 0.01);
assert_eq!(rs.values().1, 0.8);
- WidgetHost::drag_end(&mut rs);
+ rs.drag_end();
assert_eq!(rs.active_thumb, None);
// High thumb 0.8 -> 0.6.
- WidgetHost::drag_begin(&mut rs, 164.6, 20.0);
+ rs.drag_begin(164.6, 20.0);
assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
- assert!(WidgetHost::drag_update(&mut rs, 128.2, 20.0));
+ assert!(rs.drag_update(128.2, 20.0));
assert!((rs.values().1 - 0.6).abs() < 0.01);
- WidgetHost::drag_end(&mut rs);
+ rs.drag_end();
}
#[test]
@@ -720,18 +720,18 @@ mod tests {
let mut rs = RangeSlider::new().with_values(0.5, 0.5);
WidgetHost::set_rect(&mut rs, 10.0, 10.0, 200.0, 20.0);
- WidgetHost::drag_begin(&mut rs, 109.0, 20.0);
+ rs.drag_begin(109.0, 20.0);
assert_eq!(rs.active_thumb, Some(ActiveThumb::Low));
- WidgetHost::drag_end(&mut rs);
+ rs.drag_end();
- WidgetHost::drag_begin(&mut rs, 111.0, 20.0);
+ rs.drag_begin(111.0, 20.0);
assert_eq!(rs.active_thumb, Some(ActiveThumb::High));
- WidgetHost::drag_end(&mut rs);
+ rs.drag_end();
- WidgetHost::drag_begin(&mut rs, 110.0, 20.0);
- WidgetHost::drag_update(&mut rs, 150.0, 20.0);
+ rs.drag_begin(110.0, 20.0);
+ rs.drag_update(150.0, 20.0);
assert_eq!(rs.values().0, 0.5, "low constrained to high");
- WidgetHost::drag_end(&mut rs);
+ rs.drag_end();
}
#[test]
@@ -763,16 +763,14 @@ fn probe_slider_bridge() {
ptr,
));
assert!(WidgetHost::is_dragging(&sl));
- assert!(WidgetHost::drag_update(&mut sl, 80.0, 10.0));
+ assert!(sl.drag_update(80.0, 10.0));
assert!(sl.inner().value() > 0.5);
- WidgetHost::drag_end(&mut sl);
+ sl.drag_end();
// Wheel adjusts value when the gesture starts fresh.
ctx.scroll_gesture_new = true;
let before = sl.inner().value();
- assert!(WidgetHost::mouse_wheel(
- &mut sl,
- &MouseScrollDelta::LineDelta(0.0, 1.0),
+ assert!(sl.mouse_wheel(&MouseScrollDelta::LineDelta(0.0, 1.0),
50.0,
10.0,
&mut ctx,
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 023c254..a80de47 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -451,12 +451,12 @@ mod tests {
WidgetHost::set_rect(&mut sb, 10.0, 20.0, 100.0, 26.0);
// Legacy test: click at (75, 33) lands in the decrement zone.
- assert!(WidgetHost::mouse_input(&mut sb, MouseButton::Left, ElementState::Pressed, 75.0, 33.0, &mut ctx));
+ assert!(sb.mouse_input(MouseButton::Left, ElementState::Pressed, 75.0, 33.0, &mut ctx));
assert_eq!(sb.value, -1);
assert!(WidgetHost::take_change(&mut sb));
// Increment zone (past 77.5% of the width).
- assert!(WidgetHost::mouse_input(&mut sb, MouseButton::Left, ElementState::Pressed, 92.0, 33.0, &mut ctx));
+ assert!(sb.mouse_input(MouseButton::Left, ElementState::Pressed, 92.0, 33.0, &mut ctx));
assert_eq!(sb.value, 0);
}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 9484c36..ba26f8d 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -224,18 +224,17 @@ pub trait WidgetHost {
fn as_ptr_mut(&mut self) -> *mut (dyn WidgetHost + 'static);
fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
+ // The default serves test shims only (Adapted overrides this): base hover
+ // bookkeeping on moves, tick forwarding, everything else inert — the old
+ // per-method dispatch died with the direct-dispatch entry points (6bd collapse).
match event {
Event::PointerMove { x, y, .. } => {
- self.cursor_moved(*x, *y, ctx)
- }
- Event::MouseButton { button, state, x, y, .. } => {
- self.mouse_input(*button, *state, *x, *y, ctx)
- }
- Event::MouseWheel { delta, x, y, .. } => {
- self.mouse_wheel(delta, *x, *y, ctx)
- }
- Event::KeyInput(key_event) => {
- self.keyboard_input(key_event, ctx)
+ let (px, py) = (*x, *y);
+ ctx.set_cursor_pos(px, py);
+ let was = self.base().hovered;
+ let is_hit = self.hit_test(px, py, ctx);
+ self.base_mut().hovered = is_hit;
+ was != is_hit
}
Event::Tick(dt) => {
self.tick(*dt, ctx)
@@ -309,37 +308,10 @@ pub trait WidgetHost {
px >= hx && px <= hx + hw && py >= y && py <= y + h
}
- fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- ctx.set_cursor_pos(px, py);
- if ctx.is_coordinate_covered(self.base().id(), px, py) {
- let was = self.base().hovered;
- if was {
- self.base_mut().hovered = false;
- self.handle_event(&Event::MouseLeave, ctx);
- }
- return was;
- }
- self.on_cursor_moved(px, py, ctx)
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- let was = self.base().hovered;
- let is_hit = self.hit_test(px, py, ctx);
- self.base_mut().hovered = is_hit;
- if was != is_hit {
- if is_hit {
- self.handle_event(&Event::MouseEnter, ctx);
- } else {
- self.handle_event(&Event::MouseLeave, ctx);
- }
- true
- } else {
- false
- }
- }
-
- fn mouse_input(&mut self, _button: MouseButton, _state: ElementState, _px: f32, _py: f32, _ctx: &mut UiContext) -> bool { false }
- fn mouse_wheel(&mut self, _delta: &MouseScrollDelta, _px: f32, _py: f32, _ctx: &mut UiContext) -> bool { false }
+ // The direct-dispatch entry points (`cursor_moved`, `on_cursor_moved`, `mouse_input`,
+ // `mouse_wheel`, `keyboard_input`, `drag_begin`/`drag_update`/`drag_end`) are GONE from
+ // the trait (6bd collapse): every event delivery goes through `handle_event` — the entry
+ // points live on as inherent `Adapted<W>` methods for concrete in-crate forwards.
// `hovered`/`set_hovered` are GONE from the trait (6bd batch 2): the state is the base
// `Widget::hovered` flag, read/written directly by the defaults above; Button/Checkbox
@@ -367,9 +339,6 @@ pub trait WidgetHost {
fn plate_bevel(&self) -> Option<f32> { None }
fn is_dragging(&self) -> bool { false }
- fn drag_update(&mut self, _px: f32, _py: f32) -> bool { false }
- fn drag_begin(&mut self, _px: f32, _py: f32) {}
- fn drag_end(&mut self) {}
fn take_click(&mut self) -> bool { false }
fn draggable(&self) -> bool { false }
@@ -515,7 +484,6 @@ pub trait WidgetHost {
}
fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem) {}
fn set_selected(&mut self, _selected: bool) {}
- fn keyboard_input(&mut self, _event: &KeyEvent, _ctx: &mut UiContext) -> bool { false }
fn set_visible(&mut self, _visible: bool) {}
fn visible(&self) -> bool { true }
diff --git a/src/widget/model.rs b/src/widget/model.rs
index c63db56..a45551b 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -689,6 +689,86 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
ctx.clear_children_ids(self.base.id());
}
+ // --- The direct-dispatch entry points, inherent since the 6bd collapse. In-crate
+ // composites forward to their CONCRETE embedded children through these; dyn callers
+ // and the router go through `handle_event`, which these forward to (the two paths
+ // are identical by construction — including Drag*, which handle_event maps onto the
+ // Input drag hooks).
+
+ pub fn mouse_input(&mut self, button: crate::widget::MouseButton, state: crate::widget::ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ self.handle_event(
+ &Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py },
+ ctx,
+ )
+ }
+ pub fn mouse_wheel(&mut self, delta: &crate::widget::MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ self.handle_event(
+ &Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py },
+ ctx,
+ )
+ }
+ pub fn keyboard_input(&mut self, event: &crate::widget::KeyEvent, ctx: &mut UiContext) -> bool {
+ self.handle_event(&Event::KeyInput(event.clone()), ctx)
+ }
+ pub fn drag_begin(&mut self, px: f32, py: f32) {
+ let rect = self.content_rect();
+ Input::drag_begin(&mut self.inner, px, py, rect)
+ }
+ pub fn drag_update(&mut self, px: f32, py: f32) -> bool {
+ let rect = self.content_rect();
+ if let Some((nx, ny)) = Input::drag_reposition(&mut self.inner, px, py, rect) {
+ self.base.x = nx;
+ self.base.y = ny;
+ return true;
+ }
+ Input::drag_update(&mut self.inner, px, py, rect)
+ }
+ pub fn drag_end(&mut self) {
+ Input::drag_end(&mut self.inner)
+ }
+
+ /// The deleted trait default: coverage-gated hover dispatch (an open popover covering
+ /// the point clears the hover instead of recomputing it).
+ pub fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ ctx.set_cursor_pos(px, py);
+ if ctx.is_coordinate_covered(self.base.id(), px, py) {
+ let was = self.base.hovered;
+ if was {
+ self.base.hovered = false;
+ self.handle_event(&Event::MouseLeave, ctx);
+ }
+ return was;
+ }
+ self.on_cursor_moved(px, py, ctx)
+ }
+
+ /// Ungated pointer moves (no popover-coverage check): offer the raw move to the widget,
+ /// then fall back to the base hover bookkeeping, mirroring `handle_event`'s `PointerMove`
+ /// arm. Not routed *through* `handle_event`, because the routed path reaches this method
+ /// too (via `cursor_moved`) and would recurse; on that path `on_event` sees the same
+ /// unconsumed move twice, which is fine — a hover recompute is idempotent (anything
+ /// that changed on the first call consumed it there).
+ pub fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+ let rect = self.content_rect();
+ let id = self.base.id();
+ let self_ptr = self.as_ptr_mut();
+ let mut ectx = EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) };
+ let event = Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
+ if Input::on_event(&mut self.inner, &event, &mut ectx) {
+ return true;
+ }
+ let was = self.base.hovered;
+ let is_hit = self.hit_test(px, py, ctx);
+ self.base.hovered = is_hit;
+ if was != is_hit {
+ let transition = if is_hit { Event::MouseEnter } else { Event::MouseLeave };
+ self.handle_event(&transition, ctx);
+ true
+ } else {
+ false
+ }
+ }
+
/// Register + link a child under this widget (off `WidgetHost` in 6bd batch 4; dyn callers
/// went to `focus::link_parent_child`/tree ops).
pub fn add_child(&mut self, child: *mut (dyn WidgetHost + 'static), ctx: &mut UiContext) {
@@ -1340,80 +1420,6 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
fn is_scrollable(&self) -> bool {
Input::scrollable(&self.inner)
}
- fn drag_begin(&mut self, px: f32, py: f32) {
- let rect = self.content_rect();
- Input::drag_begin(&mut self.inner, px, py, rect)
- }
- fn drag_update(&mut self, px: f32, py: f32) -> bool {
- let rect = self.content_rect();
- if let Some((nx, ny)) = Input::drag_reposition(&mut self.inner, px, py, rect) {
- self.base.x = nx;
- self.base.y = ny;
- return true;
- }
- Input::drag_update(&mut self.inner, px, py, rect)
- }
- fn drag_end(&mut self) {
- Input::drag_end(&mut self.inner)
- }
- // --- Legacy direct-dispatch entry points. Hosts (treelist's add-key button, parameters_bg's
- // checkboxes, app pages) call these ON the widget instead of routing an Event through
- // `propagate_event`; without these overrides they'd hit the inert WidgetHost defaults and the
- // widget would go deaf on those paths. Route them into `handle_event` so the hit-gating /
- // context-menu / on_event pipeline applies identically on both paths.
-
- fn mouse_input(&mut self, button: crate::widget::MouseButton, state: crate::widget::ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- self.handle_event(
- &Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py },
- ctx,
- )
- }
- fn mouse_wheel(&mut self, delta: &crate::widget::MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- self.handle_event(
- &Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py },
- ctx,
- )
- }
- fn keyboard_input(&mut self, event: &crate::widget::KeyEvent, ctx: &mut UiContext) -> bool {
- // A widget hidden while still holding focus (the designer keys into `focused_widget`;
- // hiding a pane doesn't unfocus it) must not consume keys — the legacy visibility-toggled
- // widgets gated their keyboard_input overrides on `visible` themselves.
- if !self.visible() {
- return false;
- }
- self.handle_event(&Event::KeyInput(event.clone()), ctx)
- }
-
- /// Direct-dispatch pointer moves (hosts call `w.on_cursor_moved(..)` instead of routing a
- /// `PointerMove` — cce-files drives its breadcrumbs this way): offer the raw move to the
- /// widget, then fall back to the base hover bookkeeping, mirroring `handle_event`'s
- /// `PointerMove` arm. Not routed *through* `handle_event`, because the routed path reaches
- /// this method too (via `cursor_moved`) and would recurse; on that path `on_event` sees the
- /// same unconsumed move twice, which is fine — a hover recompute is idempotent (anything
- /// that changed on the first call consumed it there).
- fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- let rect = self.content_rect();
- let id = self.base.id();
- let self_ptr = self.as_ptr_mut();
- let mut ectx = EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) };
- let event = Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
- if Input::on_event(&mut self.inner, &event, &mut ectx) {
- return true;
- }
- // The legacy default `WidgetHost::on_cursor_moved` body: base hover flag + synthesized
- // MouseEnter/MouseLeave (which re-enter `handle_event` and reach `on_event`).
- let was = self.base.hovered;
- let is_hit = self.hit_test(px, py, ctx);
- self.base.hovered = is_hit;
- if was != is_hit {
- let transition = if is_hit { Event::MouseEnter } else { Event::MouseLeave };
- self.handle_event(&transition, ctx);
- true
- } else {
- false
- }
- }
-
/// Focus set/cleared directly (hosts call `w.focus()`/`w.unfocus()`): keep the base flag
/// (unless the widget opts out — [`Input::tracks_base_focus`], TextBox's legacy `focus`
/// never set it) and tell the widget via the same `FocusIn`/`FocusOut` events the router
@@ -1560,6 +1566,11 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
Input::drag_end(&mut self.inner);
true
}
+ // A widget hidden while still holding focus (the designer keys into
+ // `focused_widget`; hiding a pane doesn't unfocus it) must not consume keys —
+ // formerly the `keyboard_input` entry point's gate, now on the one funnel
+ // (which also closes the routed path's missing-gate hole).
+ Event::KeyInput(_) if !self.visible() => false,
// Everything else (KeyInput, Tick, Enter/Leave, Focus*) forwards directly —
// the legacy default dispatch would route these to leaf handlers Adapted never
// overrides, so there is no behavior to fall back to.