Route pointer input per kind, so a scroll falls through a hovered button
`run_sensors` decided that a widget had consumed the frame's input from hover alone: if the cursor was inside its shape, no lower layer saw anything. So a button sitting over a list swallowed the list's scroll, having registered nothing but `click()`. Being in shape still runs a widget -- a hover highlight has to fire on the topmost thing under the cursor regardless -- but consuming is now judged per input kind. With nothing momentary happening the behaviour is unchanged and the topmost widget wins the hover; with a scroll or a press happening, only a widget that registered a matching momentary sense consumes it. `TypeEventManager::registered` is what makes that askable: what a widget would match is a different question from dispatching to it, and `run_fn` can only answer the second. tests/pointer_routing.rs drives `run_sensors` directly, with no GPU and no window. It fails on the unfixed code with "a scroll over the button must still reach the list underneath it".
This commit is contained in:
1 parent
0f6a28b4dd
commit
028521b419
3 files changed
+170
-1
No files matched your search
@@ -135,6 +135,15 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The event lists this widget was registered with, without running
|
||||||
|
/// anything. Asking what a widget would match is a separate question
|
||||||
|
/// from dispatching to it: input routing needs the first to decide
|
||||||
|
/// whether a widget consumes an event, and `run_fn` can only answer
|
||||||
|
/// the second.
|
||||||
|
pub fn registered(&self, id: WidgetId) -> impl Iterator<Item = &E> {
|
||||||
|
self.map.get(&id).into_iter().flatten().map(|(e, _)| e)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run_fn<'a>(
|
pub fn run_fn<'a>(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: impl IdLike,
|
id: impl IdLike,
|
||||||
|
|||||||
+40
-1
@@ -52,6 +52,18 @@ impl CursorSense {
|
|||||||
pub fn is_dragging(&self) -> bool {
|
pub fn is_dragging(&self) -> bool {
|
||||||
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
matches!(self, CursorSense::Pressing(CursorButton::Left))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True for a sense that names something happening this frame (a
|
||||||
|
/// button transitioning, a scroll), as opposed to the ambient
|
||||||
|
/// `Hover*` family that is on for as long as the cursor rests there.
|
||||||
|
/// Only a momentary sense can consume an input -- see
|
||||||
|
/// `SensorUi::run_sensors`.
|
||||||
|
pub fn is_momentary(&self) -> bool {
|
||||||
|
!matches!(
|
||||||
|
self,
|
||||||
|
CursorSense::HoverStart | CursorSense::Hovering | CursorSense::HoverEnd
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
@@ -163,6 +175,11 @@ impl SensorUi for UiRenderState {
|
|||||||
// this would probably be done through a generic parameter that adds yet another rsc /
|
// this would probably be done through a generic parameter that adds yet another rsc /
|
||||||
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
// state like thing, but local to render state, and is passed to UiRsc events so you can
|
||||||
// update it there?
|
// update it there?
|
||||||
|
// Whether anything momentary is happening this frame at all; see
|
||||||
|
// `consumed` below, which is only judged per kind when it is.
|
||||||
|
let momentary_active =
|
||||||
|
cursor.scroll_delta != Vec2::ZERO || cursor.buttons.iter().any(|(_, a)| !a.is_off());
|
||||||
|
|
||||||
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
let mut active = std::mem::take(&mut rsc.events_mut().get_type::<CursorSense>().active);
|
||||||
for layer in self.layers.indices().rev() {
|
for layer in self.layers.indices().rev() {
|
||||||
let mut sensed = false;
|
let mut sensed = false;
|
||||||
@@ -174,7 +191,29 @@ impl SensorUi for UiRenderState {
|
|||||||
if sensor.hover == ActivationState::Off {
|
if sensor.hover == ActivationState::Off {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sensed = true;
|
|
||||||
|
// A widget in shape always still runs: a hover-only
|
||||||
|
// highlight must fire on the topmost thing under the
|
||||||
|
// cursor even while a scroll passes through it. What is
|
||||||
|
// judged per input kind is whether it *consumes* that
|
||||||
|
// input, stopping a lower layer from seeing it. With
|
||||||
|
// nothing momentary happening, being in shape is
|
||||||
|
// consumption and the topmost widget wins the hover; with
|
||||||
|
// a scroll or a press happening, only a widget that
|
||||||
|
// registered a matching momentary sense consumes it, so a
|
||||||
|
// button registered for `click()` alone cannot block a
|
||||||
|
// scroll meant for the list behind it.
|
||||||
|
let consumed = if momentary_active {
|
||||||
|
rsc.events_mut()
|
||||||
|
.get_type::<CursorSense>()
|
||||||
|
.registered(*id)
|
||||||
|
.any(|senses| {
|
||||||
|
matches!(should_run(senses, &cursor, sensor.hover), Some(s) if s.is_momentary())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
};
|
||||||
|
sensed |= consumed;
|
||||||
|
|
||||||
let cursor = cursor.clone();
|
let cursor = cursor.clone();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
//! 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.
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
|
||||||
|
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 its children on separate layers in order, which is
|
||||||
|
// exactly the "one thing drawn over another" shape `run_sensors`
|
||||||
|
// walks top layer first.
|
||||||
|
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);
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
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 = 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());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
clicked.get(),
|
||||||
|
"the button on top must still receive an actual click"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user