Keep momentary input on the widget the cursor is on
Tests across layers, as asked, and the fifth one found a defect older than this branch: a press fired on a widget the cursor had just left, because the frame its hover ends is a frame it still gets dispatched on, and `should_run` only ever looked at the cursor. A button in the corner of a list therefore clicked when the press landed anywhere else in the row. A widget that is not under the cursor now sees a cursor with nothing momentary in it, which settles both halves of the question at once: it is not its press to receive, and not its press to take from the layers below. `CursorSense` and `CursorButton` derive `Debug`, so a failure says which sense fired rather than `left != right`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a14df2cc3
commit
71ba3723ff
2 files changed
+197
-111
No files matched your search
+15
-4
@@ -4,14 +4,14 @@ use std::{
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CursorButton {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CursorSense {
|
||||
PressStart(CursorButton),
|
||||
Pressing(CursorButton),
|
||||
@@ -205,6 +205,19 @@ impl SensorUi for UiRenderState {
|
||||
}
|
||||
hovered_here |= over;
|
||||
|
||||
// Momentary input belongs to whatever the cursor is on. A
|
||||
// widget it has just left still hears its hover ending, but
|
||||
// a press landing elsewhere is neither its press to receive
|
||||
// nor its press to take.
|
||||
let cursor = match over {
|
||||
true => cursor.clone(),
|
||||
false => CursorState {
|
||||
pos: cursor.pos,
|
||||
exists: cursor.exists,
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
for senses in rsc.events_mut().get_type::<CursorSense>().registered(*id) {
|
||||
for sense in senses.iter() {
|
||||
if matches(sense, &cursor, sensor.hover) {
|
||||
@@ -213,8 +226,6 @@ impl SensorUi for UiRenderState {
|
||||
}
|
||||
}
|
||||
|
||||
let cursor = cursor.clone();
|
||||
|
||||
let data = CursorData {
|
||||
pos: cursor.pos - region.top_left,
|
||||
size: region.bot_right - region.top_left,
|
||||
|
||||
+182
-107
@@ -1,9 +1,9 @@
|
||||
//! 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.
|
||||
//! Input across layers: what a widget takes, what passes through it, and
|
||||
//! where hovering stops. These drive `run_sensors` directly, which needs no
|
||||
//! GPU and no window.
|
||||
|
||||
use iris::prelude::*;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
struct SenseRsc {
|
||||
ui: UiData,
|
||||
@@ -41,140 +41,215 @@ impl HasEvents for SenseRsc {
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_at(pos: Vec2) -> CursorState {
|
||||
CursorState {
|
||||
pos,
|
||||
exists: true,
|
||||
buttons: Default::default(),
|
||||
scroll_delta: Vec2::ZERO,
|
||||
const WINDOW: f32 = 100.0;
|
||||
|
||||
/// Every sense that has fired on one widget since it was last read.
|
||||
#[derive(Default, Clone)]
|
||||
struct Fired(Rc<RefCell<Vec<CursorSense>>>);
|
||||
|
||||
impl Fired {
|
||||
fn take(&self) -> Vec<CursorSense> {
|
||||
std::mem::take(&mut self.0.borrow_mut())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
};
|
||||
struct Ui {
|
||||
rsc: SenseRsc,
|
||||
render: UiRenderState,
|
||||
state: (),
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
impl Ui {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
rsc: SenseRsc {
|
||||
ui: UiData::default(),
|
||||
events: EventManager::default(),
|
||||
},
|
||||
render: UiRenderState::new(),
|
||||
state: (),
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
fn listen<W: Widget + ?core::marker::Sized + 'static>(
|
||||
&mut self,
|
||||
widget: &StrongWidget<W>,
|
||||
senses: impl Into<CursorSenses>,
|
||||
) -> Fired {
|
||||
let fired = Fired::default();
|
||||
let sink = fired.clone();
|
||||
self.rsc
|
||||
.register_event(widget.weak(), senses.into(), move |ctx, _rsc| {
|
||||
sink.0.borrow_mut().push(ctx.data.sense)
|
||||
});
|
||||
fired
|
||||
}
|
||||
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((100.0, 100.0));
|
||||
render.update(&root, &mut rsc);
|
||||
/// Stacks the widgets bottom first, each on its own layer, and lays them
|
||||
/// out in a square window.
|
||||
fn stack(&mut self, children: Vec<StrongWidget>) {
|
||||
let root = self
|
||||
.rsc
|
||||
.ui
|
||||
.widgets
|
||||
.add_strong(Stack {
|
||||
children,
|
||||
size: StackSize::default(),
|
||||
})
|
||||
.any();
|
||||
self.render.resize((WINDOW, WINDOW));
|
||||
self.render.update(&root, &mut self.rsc);
|
||||
}
|
||||
|
||||
(render, rsc, scrolled, clicked)
|
||||
fn cursor(&mut self, at: (f32, f32)) -> CursorState {
|
||||
CursorState {
|
||||
pos: at.into(),
|
||||
exists: true,
|
||||
buttons: Default::default(),
|
||||
scroll_delta: Vec2::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&mut self, cursor: CursorState) {
|
||||
self.render.run_sensors(
|
||||
&mut self.rsc,
|
||||
&mut self.state,
|
||||
cursor,
|
||||
(WINDOW, WINDOW).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn full(ui: &mut Ui) -> StrongWidget<Rect> {
|
||||
rect(UiColor::WHITE).add_strong(&mut ui.rsc)
|
||||
}
|
||||
|
||||
#[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 = ();
|
||||
fn hover_stops_at_the_topmost_widget() {
|
||||
let mut ui = Ui::new();
|
||||
let (bottom, middle, top) = (full(&mut ui), full(&mut ui), full(&mut ui));
|
||||
let bottom_hover = ui.listen(&bottom, CursorSense::HoverStart);
|
||||
let middle_hover = ui.listen(&middle, CursorSense::HoverStart);
|
||||
let top_hover = ui.listen(&top, CursorSense::HoverStart);
|
||||
ui.stack(vec![bottom.any(), middle.any(), top.any()]);
|
||||
|
||||
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());
|
||||
let cursor = ui.cursor((50.0, 50.0));
|
||||
ui.run(cursor);
|
||||
|
||||
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"
|
||||
assert_eq!(top_hover.take(), [CursorSense::HoverStart]);
|
||||
assert_eq!(
|
||||
middle_hover.take(),
|
||||
[],
|
||||
"hover is not shared with a layer below"
|
||||
);
|
||||
assert_eq!(bottom_hover.take(), []);
|
||||
}
|
||||
|
||||
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());
|
||||
#[test]
|
||||
fn a_scroll_passes_through_every_widget_that_does_not_want_it() {
|
||||
let mut ui = Ui::new();
|
||||
let (list, button, overlay) = (full(&mut ui), full(&mut ui), full(&mut ui));
|
||||
let scrolled = ui.listen(&list, CursorSense::Scroll);
|
||||
let clicked = ui.listen(&button, CursorSense::click());
|
||||
let overlay_clicked = ui.listen(&overlay, CursorSense::click());
|
||||
ui.stack(vec![list.any(), button.any(), overlay.any()]);
|
||||
|
||||
assert!(
|
||||
clicked.get(),
|
||||
"the button on top must still receive an actual click"
|
||||
let mut cursor = ui.cursor((50.0, 50.0));
|
||||
cursor.scroll_delta = (0.0, 10.0).into();
|
||||
ui.run(cursor);
|
||||
|
||||
assert_eq!(
|
||||
scrolled.take(),
|
||||
[CursorSense::Scroll],
|
||||
"two layers of click-only widgets do not stop a scroll"
|
||||
);
|
||||
assert_eq!(clicked.take(), []);
|
||||
assert_eq!(overlay_clicked.take(), []);
|
||||
}
|
||||
|
||||
#[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 ui = Ui::new();
|
||||
let (list, button) = (full(&mut ui), full(&mut ui));
|
||||
let scrolled = ui.listen(&list, CursorSense::Scroll);
|
||||
let clicked = ui.listen(&button, CursorSense::click());
|
||||
ui.stack(vec![list.any(), button.any()]);
|
||||
|
||||
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());
|
||||
let mut cursor = ui.cursor((50.0, 50.0));
|
||||
cursor.scroll_delta = (0.0, 10.0).into();
|
||||
cursor.buttons.left = ActivationState::Start;
|
||||
ui.run(cursor);
|
||||
|
||||
assert!(
|
||||
clicked.get(),
|
||||
"the button takes the click it registered for"
|
||||
);
|
||||
assert!(
|
||||
scrolled.get(),
|
||||
assert_eq!(clicked.take(), [CursorSense::click()]);
|
||||
assert_eq!(
|
||||
scrolled.take(),
|
||||
[CursorSense::Scroll],
|
||||
"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);
|
||||
});
|
||||
}
|
||||
fn only_the_topmost_listener_takes_a_press() {
|
||||
let mut ui = Ui::new();
|
||||
let (below, above) = (full(&mut ui), full(&mut ui));
|
||||
let below_clicked = ui.listen(&below, CursorSense::click());
|
||||
let above_clicked = ui.listen(&above, CursorSense::click());
|
||||
ui.stack(vec![below.any(), above.any()]);
|
||||
|
||||
let root = widget.any();
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((100.0, 100.0));
|
||||
render.update(&root, &mut rsc);
|
||||
let mut cursor = ui.cursor((50.0, 50.0));
|
||||
cursor.buttons.left = ActivationState::Start;
|
||||
ui.run(cursor);
|
||||
|
||||
let mut state = ();
|
||||
render.run_sensors(
|
||||
&mut rsc,
|
||||
&mut state,
|
||||
cursor_at((50.0, 50.0).into()),
|
||||
(100.0, 100.0).into(),
|
||||
assert_eq!(above_clicked.take(), [CursorSense::click()]);
|
||||
assert_eq!(below_clicked.take(), [], "one press goes to one widget");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_beside_the_button_reaches_the_layer_below() {
|
||||
let mut ui = Ui::new();
|
||||
let list = full(&mut ui);
|
||||
// The row above the list covers it, but only its left half is the button.
|
||||
let button = full(&mut ui);
|
||||
let gap = full(&mut ui);
|
||||
let list_clicked = ui.listen(&list, CursorSense::click());
|
||||
let button_clicked = ui.listen(&button, CursorSense::click());
|
||||
let row = ui.rsc.ui.widgets.add_strong(Span {
|
||||
children: vec![button.any(), gap.any()],
|
||||
dir: Dir::RIGHT,
|
||||
gap: 0.0,
|
||||
});
|
||||
ui.stack(vec![list.any(), row.any()]);
|
||||
|
||||
let mut on_button = ui.cursor((20.0, 50.0));
|
||||
on_button.buttons.left = ActivationState::Start;
|
||||
ui.run(on_button);
|
||||
assert_eq!(button_clicked.take(), [CursorSense::click()]);
|
||||
assert_eq!(list_clicked.take(), []);
|
||||
|
||||
let mut beside_it = ui.cursor((80.0, 50.0));
|
||||
beside_it.buttons.left = ActivationState::Start;
|
||||
ui.run(beside_it);
|
||||
assert_eq!(button_clicked.take(), [], "the cursor is not on the button");
|
||||
assert_eq!(
|
||||
list_clicked.take(),
|
||||
[CursorSense::click()],
|
||||
"a press beside the button belongs to what is under it"
|
||||
);
|
||||
assert!(!ended.get(), "the cursor is still on it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_a_widget_still_ends_its_hover() {
|
||||
let mut ui = Ui::new();
|
||||
let widget = full(&mut ui);
|
||||
let hover = ui.listen(&widget, CursorSense::HoverStart | CursorSense::HoverEnd);
|
||||
ui.stack(vec![widget.any()]);
|
||||
|
||||
let cursor = ui.cursor((50.0, 50.0));
|
||||
ui.run(cursor);
|
||||
assert_eq!(hover.take(), [CursorSense::HoverStart]);
|
||||
|
||||
let mut gone = cursor_at((50.0, 50.0).into());
|
||||
let mut gone = ui.cursor((50.0, 50.0));
|
||||
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");
|
||||
ui.run(gone);
|
||||
assert_eq!(hover.take(), [CursorSense::HoverEnd]);
|
||||
}
|
||||
Reference in new issue
Block a user