Reviewing this against the process we agreed: the title claimed per-kind routing and the code decided it once for the whole frame. A scroll and a click in the same frame both went to the button, because a widget that matched any momentary sense consumed everything. Consumption is now removing an input from the cursor the layers below see. `CursorSense::take` states what each sense takes -- exhaustively, so a new sense has to answer the question rather than inherit a default -- and `is_momentary` is gone with the enumeration it was written on. `should_run` and consumption share one matcher instead of two copies of the table. Two tests, each checked to fail without the change: a click and a scroll in one frame reach different widgets, and leaving a widget still ends its hover. The second is a regression this review caught in its own first draft, where the skip condition used `is_off`, which counts `End` -- the one frame a hover-end handler has to run on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
181 lines
5.0 KiB
Rust
181 lines
5.0 KiB
Rust
//! A widget takes only what it answers to: a button over a list takes the
|
|
//! click and leaves the scroll. These drive `run_sensors` directly, which
|
|
//! needs no GPU and no window.
|
|
|
|
use iris::prelude::*;
|
|
use std::{cell::Cell, rc::Rc};
|
|
|
|
struct SenseRsc {
|
|
ui: UiData,
|
|
events: EventManager<SenseRsc>,
|
|
}
|
|
|
|
impl UiRsc for SenseRsc {
|
|
fn ui(&self) -> &UiData {
|
|
&self.ui
|
|
}
|
|
fn ui_mut(&mut self) -> &mut UiData {
|
|
&mut self.ui
|
|
}
|
|
fn on_draw(&mut self, active: &ActiveData) {
|
|
self.events.draw(active);
|
|
}
|
|
fn on_undraw(&mut self, active: &ActiveData) {
|
|
self.events.undraw(active);
|
|
}
|
|
fn on_remove(&mut self, id: WidgetId) {
|
|
self.events.remove(id);
|
|
}
|
|
}
|
|
|
|
impl HasState for SenseRsc {
|
|
type State = ();
|
|
}
|
|
|
|
impl HasEvents for SenseRsc {
|
|
fn events(&self) -> &EventManager<Self> {
|
|
&self.events
|
|
}
|
|
fn events_mut(&mut self) -> &mut EventManager<Self> {
|
|
&mut self.events
|
|
}
|
|
}
|
|
|
|
fn cursor_at(pos: Vec2) -> CursorState {
|
|
CursorState {
|
|
pos,
|
|
exists: true,
|
|
buttons: Default::default(),
|
|
scroll_delta: Vec2::ZERO,
|
|
}
|
|
}
|
|
|
|
/// A button covering a list, on the layer above it: the list scrolls, the
|
|
/// button clicks, and the returned flags say which fired.
|
|
fn button_over_list() -> (UiRenderState, SenseRsc, Rc<Cell<bool>>, Rc<Cell<bool>>) {
|
|
let mut rsc = SenseRsc {
|
|
ui: UiData::default(),
|
|
events: EventManager::default(),
|
|
};
|
|
|
|
// Both cover the whole window: the button "sitting over" the list.
|
|
let list = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
|
let list_weak = list.weak();
|
|
let button = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
|
|
let button_weak = button.weak();
|
|
|
|
let scrolled = Rc::new(Cell::new(false));
|
|
let clicked = Rc::new(Cell::new(false));
|
|
{
|
|
let scrolled = scrolled.clone();
|
|
rsc.register_event(list_weak, CursorSense::Scroll, move |_ctx, _rsc| {
|
|
scrolled.set(true);
|
|
});
|
|
}
|
|
{
|
|
let clicked = clicked.clone();
|
|
rsc.register_event(button_weak, CursorSense::click(), move |_ctx, _rsc| {
|
|
clicked.set(true);
|
|
});
|
|
}
|
|
|
|
// A Stack draws each child on its own layer, in order.
|
|
let root = rsc
|
|
.ui
|
|
.widgets
|
|
.add_strong(Stack {
|
|
children: vec![list.any(), button.any()],
|
|
size: StackSize::default(),
|
|
})
|
|
.any();
|
|
|
|
let mut render = UiRenderState::new();
|
|
render.resize((100.0, 100.0));
|
|
render.update(&root, &mut rsc);
|
|
|
|
(render, rsc, scrolled, clicked)
|
|
}
|
|
|
|
#[test]
|
|
fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
|
|
let (render, mut rsc, scrolled, clicked) = button_over_list();
|
|
let mut state = ();
|
|
|
|
let mut scroll = cursor_at((50.0, 50.0).into());
|
|
scroll.scroll_delta = (0.0, 10.0).into();
|
|
render.run_sensors(&mut rsc, &mut state, scroll, (100.0, 100.0).into());
|
|
|
|
assert!(
|
|
scrolled.get(),
|
|
"a scroll over the button must still reach the list underneath it"
|
|
);
|
|
assert!(
|
|
!clicked.get(),
|
|
"a scroll is not a click; the button must not have fired"
|
|
);
|
|
|
|
let mut click = cursor_at((50.0, 50.0).into());
|
|
click.buttons.left = ActivationState::Start;
|
|
render.run_sensors(&mut rsc, &mut state, click, (100.0, 100.0).into());
|
|
|
|
assert!(
|
|
clicked.get(),
|
|
"the button on top must still receive an actual click"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_click_and_a_scroll_in_one_frame_go_to_different_widgets() {
|
|
let (render, mut rsc, scrolled, clicked) = button_over_list();
|
|
let mut state = ();
|
|
|
|
let mut both = cursor_at((50.0, 50.0).into());
|
|
both.scroll_delta = (0.0, 10.0).into();
|
|
both.buttons.left = ActivationState::Start;
|
|
render.run_sensors(&mut rsc, &mut state, both, (100.0, 100.0).into());
|
|
|
|
assert!(
|
|
clicked.get(),
|
|
"the button takes the click it registered for"
|
|
);
|
|
assert!(
|
|
scrolled.get(),
|
|
"taking the click must not take the scroll with it"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn leaving_a_widget_still_ends_its_hover() {
|
|
let mut rsc = SenseRsc {
|
|
ui: UiData::default(),
|
|
events: EventManager::default(),
|
|
};
|
|
let widget = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
|
let ended = Rc::new(Cell::new(false));
|
|
{
|
|
let ended = ended.clone();
|
|
rsc.register_event(widget.weak(), CursorSense::HoverEnd, move |_ctx, _rsc| {
|
|
ended.set(true);
|
|
});
|
|
}
|
|
|
|
let root = widget.any();
|
|
let mut render = UiRenderState::new();
|
|
render.resize((100.0, 100.0));
|
|
render.update(&root, &mut rsc);
|
|
|
|
let mut state = ();
|
|
render.run_sensors(
|
|
&mut rsc,
|
|
&mut state,
|
|
cursor_at((50.0, 50.0).into()),
|
|
(100.0, 100.0).into(),
|
|
);
|
|
assert!(!ended.get(), "the cursor is still on it");
|
|
|
|
let mut gone = cursor_at((50.0, 50.0).into());
|
|
gone.exists = false;
|
|
render.run_sensors(&mut rsc, &mut state, gone, (100.0, 100.0).into());
|
|
assert!(ended.get(), "leaving a widget ends its hover");
|
|
}
|