Take input per kind, rather than deciding it once a frame

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>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-13 19:09:50 -04:00
1 parent f62131eecf
commit 0e7076a01c
3 files changed
+138 -76

No files matched your search

+75 -16
View File
@@ -1,8 +1,6 @@
//! A widget that registered only `click()` used to block a `Scroll` meant
//! for whatever is behind it: `run_sensors` decided "consumed, stop looking
//! at lower layers" from mere hover rather than from anything actually
//! matching the input. These drive `run_sensors` directly, which needs no
//! GPU and no window.
//! 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};
@@ -52,8 +50,9 @@ fn cursor_at(pos: Vec2) -> CursorState {
}
}
#[test]
fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
/// 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(),
@@ -80,9 +79,7 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
});
}
// A Stack draws its children on separate layers in order, which is
// exactly the "one thing drawn over another" shape `run_sensors`
// walks top layer first.
// A Stack draws each child on its own layer, in order.
let root = rsc
.ui
.widgets
@@ -96,10 +93,17 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
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 = cursor_at((50.0, 50.0).into());
scroll_cursor.scroll_delta = (0.0, 10.0).into();
render.run_sensors(&mut rsc, &mut state, scroll_cursor, (100.0, 100.0).into());
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(),
@@ -110,12 +114,67 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
"a scroll is not a click; the button must not have fired"
);
let mut click_cursor = cursor_at((50.0, 50.0).into());
click_cursor.buttons.left = ActivationState::Start;
render.run_sensors(&mut rsc, &mut state, click_cursor, (100.0, 100.0).into());
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");
}