use std::{cell::Cell, cell::RefCell, collections::HashMap, collections::HashSet, rc::Rc};
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::{Position, Rect};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum EventResult {
#[default]
Ignored,
Consumed,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
pub enum EventPriority {
Low = 0,
#[default]
Normal = 1,
High = 2,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[doc(hidden)]
pub struct LayerId(u64);
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct InputLayer {
pub(crate) id: LayerId,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum EventScope {
Current,
Layer(InputLayer),
Global,
}
#[derive(Clone, Copy, Default)]
pub struct EventOptions {
pub hit_test: bool,
}
#[derive(Clone, Copy)]
pub(crate) struct CurrentLayer(pub(crate) LayerId);
struct LayerEntry {
id: LayerId,
blocks_lower: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) struct PointerToken(u64);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Stamp {
pub(crate) area: Rect,
pub(crate) screen_area: Rect,
pub(crate) delta_x: i32,
pub(crate) delta_y: i32,
pub(crate) frame: u64,
pub(crate) draw_seq: u64,
}
impl Stamp {
pub(crate) fn contains(self, position: Position) -> bool {
self.frame == draw_clock_frame() && rect_contains(self.screen_area, position.x, position.y)
}
pub(crate) fn local_position(self, position: Position) -> Position {
Position::new(
(i32::from(position.x) - self.delta_x).clamp(0, i32::from(u16::MAX)) as u16,
(i32::from(position.y) - self.delta_y).clamp(0, i32::from(u16::MAX)) as u16,
)
}
pub(crate) fn same_geometry(self, other: Self) -> bool {
self.area == other.area
&& self.screen_area == other.screen_area
&& self.delta_x == other.delta_x
&& self.delta_y == other.delta_y
}
}
pub(crate) type StampHandle = Rc<Cell<Stamp>>;
struct DrawClock {
frame: u64,
draw_seq: u64,
}
thread_local! {
static DRAW_CLOCK: RefCell<DrawClock> = const { RefCell::new(DrawClock { frame: 0, draw_seq: 0 }) };
}
fn draw_clock_begin_frame() {
DRAW_CLOCK.with_borrow_mut(|c| {
c.frame += 1;
c.draw_seq = 0;
});
}
pub(crate) fn draw_clock_stamp(area: Rect) -> Stamp {
DRAW_CLOCK.with_borrow_mut(|c| {
c.draw_seq += 1;
Stamp {
area,
screen_area: area,
delta_x: 0,
delta_y: 0,
frame: c.frame,
draw_seq: c.draw_seq,
}
})
}
pub(crate) fn draw_clock_frame() -> u64 {
DRAW_CLOCK.with_borrow(|c| c.frame)
}
struct HandlerEntry {
layer: Option<LayerId>,
priority: EventPriority,
order: usize,
options: EventOptions,
stamp: StampHandle,
pointer: Option<PointerToken>,
motion_observer: bool,
cancel: Option<Box<dyn FnMut(MouseButton)>>,
deactivate: Option<Box<dyn FnMut() -> bool>>,
sync_motion: Option<Box<dyn FnMut(MouseEvent) -> bool>>,
f: Box<dyn FnMut(Event) -> EventResult>,
}
#[derive(Default)]
pub(crate) struct InputRuntime {
layers: Vec<LayerEntry>,
handlers: Vec<HandlerEntry>,
next_layer_id: u64,
root_layer: Option<LayerId>,
gestures: [Option<PointerToken>; 3],
hover_trackers: Vec<(LayerId, PointerToken, StampHandle)>,
hover_inside: HashSet<PointerToken>,
motion_requested: bool,
motion_enabled: bool,
last_pointer: Option<MouseEvent>,
}
pub(crate) fn mint_pointer_token() -> PointerToken {
thread_local! {
static NEXT: Cell<u64> = const { Cell::new(0) };
}
NEXT.with(|c| {
let next = c.get() + 1;
c.set(next);
PointerToken(next)
})
}
fn button_index(button: MouseButton) -> usize {
match button {
MouseButton::Left => 0,
MouseButton::Right => 1,
MouseButton::Middle => 2,
}
}
fn pressed_button(kind: &MouseEventKind) -> Option<MouseButton> {
match kind {
MouseEventKind::Drag(b) | MouseEventKind::Up(b) => Some(*b),
_ => None,
}
}
pub(crate) fn rect_contains(area: Rect, column: u16, row: u16) -> bool {
column >= area.x
&& column < area.x.saturating_add(area.width)
&& row >= area.y
&& row < area.y.saturating_add(area.height)
}
impl InputRuntime {
pub(crate) fn begin_frame(&mut self) {
self.layers.clear();
self.handlers.clear();
self.hover_trackers.clear();
let root = self.mint_layer_id();
self.root_layer = Some(root);
self.layers.push(LayerEntry {
id: root,
blocks_lower: false,
});
draw_clock_begin_frame();
}
pub(crate) fn root_layer(&self) -> LayerId {
self.root_layer
.expect("`begin_frame` was not called before `root_layer`")
}
fn mint_layer_id(&mut self) -> LayerId {
let id = LayerId(self.next_layer_id);
self.next_layer_id = self.next_layer_id.wrapping_add(1);
id
}
pub(crate) fn push_layer(&mut self, open: bool, blocks_lower: bool) -> InputLayer {
let id = self.mint_layer_id();
if open {
self.layers.push(LayerEntry { id, blocks_lower });
}
InputLayer { id }
}
pub(crate) fn register_handler(
&mut self,
layer: Option<LayerId>,
priority: EventPriority,
options: EventOptions,
stamp: StampHandle,
pointer: Option<PointerToken>,
f: Box<dyn FnMut(Event) -> EventResult>,
) {
let order = self.handlers.len();
self.handlers.push(HandlerEntry {
layer,
priority,
order,
options,
stamp,
pointer,
motion_observer: false,
cancel: None,
deactivate: None,
sync_motion: None,
f,
});
}
pub(crate) fn register_pointer_handler(
&mut self,
layer: LayerId,
stamp: StampHandle,
token: PointerToken,
f: Box<dyn FnMut(Event) -> EventResult>,
cancel: Box<dyn FnMut(MouseButton)>,
) {
self.register_handler(
Some(layer),
EventPriority::Normal,
EventOptions { hit_test: true },
stamp,
Some(token),
f,
);
self.handlers.last_mut().unwrap().cancel = Some(cancel);
}
pub(crate) fn finish_frame(&mut self) -> bool {
let mut repaint = false;
let cut = self
.layers
.iter()
.rposition(|layer| layer.blocks_lower)
.unwrap_or(0);
let frame = draw_clock_frame();
for (index, button) in [MouseButton::Left, MouseButton::Right, MouseButton::Middle]
.into_iter()
.enumerate()
{
let Some(token) = self.gestures[index] else {
continue;
};
let target = self
.handlers
.iter_mut()
.find(|handler| handler.pointer == Some(token));
let valid = target.as_ref().is_some_and(|handler| {
handler.stamp.get().frame == frame
&& !handler.stamp.get().screen_area.is_empty()
&& handler
.layer
.is_some_and(|id| self.layers[cut..].iter().any(|layer| layer.id == id))
});
if !valid {
self.gestures[index] = None;
if let Some(cancel) = target.and_then(|handler| handler.cancel.as_mut()) {
cancel(button);
repaint = true;
}
}
}
for handler in self
.handlers
.iter_mut()
.filter(|handler| handler.motion_observer)
{
let valid = handler.stamp.get().frame == frame
&& handler
.layer
.is_some_and(|id| self.layers[cut..].iter().any(|layer| layer.id == id));
if !valid && let Some(deactivate) = handler.deactivate.as_mut() {
repaint |= deactivate();
} else if let Some(mouse) = self.last_pointer
&& let Some(sync) = handler.sync_motion.as_mut()
{
repaint |= sync(mouse);
}
}
let new_inside: HashSet<_> = self
.hover_trackers
.iter()
.filter(|(layer, _, stamp)| {
stamp.get().frame == frame
&& self.layers[cut..].iter().any(|entry| entry.id == *layer)
&& self.last_pointer.is_some_and(|mouse| {
stamp.get().contains(Position::new(mouse.column, mouse.row))
})
})
.map(|(_, token, _)| *token)
.collect();
repaint |= new_inside != self.hover_inside;
self.hover_inside = new_inside;
repaint
}
pub(crate) fn register_motion_observer(
&mut self,
layer: LayerId,
stamp: StampHandle,
f: Box<dyn FnMut(Event) -> EventResult>,
deactivate: Box<dyn FnMut() -> bool>,
sync_motion: Box<dyn FnMut(MouseEvent) -> bool>,
) {
self.request_motion();
self.register_handler(
Some(layer),
EventPriority::Normal,
EventOptions { hit_test: false },
stamp,
None,
f,
);
let handler = self.handlers.last_mut().unwrap();
handler.motion_observer = true;
handler.deactivate = Some(deactivate);
handler.sync_motion = Some(sync_motion);
}
pub(crate) fn track_hover(&mut self, layer: LayerId, token: PointerToken, stamp: StampHandle) {
self.motion_requested = true;
self.hover_trackers.push((layer, token, stamp));
}
pub(crate) fn hover_contains(&self, token: PointerToken) -> bool {
self.hover_inside.contains(&token)
}
pub(crate) fn needs_any_motion_upgrade(&self) -> bool {
self.motion_requested && !self.motion_enabled
}
pub(crate) fn mark_any_motion_enabled(&mut self) {
self.motion_enabled = true;
}
pub(crate) fn request_motion(&mut self) {
self.motion_requested = true;
}
pub(crate) fn dispatch(&mut self, event: Event) -> bool {
let mut handlers = std::mem::take(&mut self.handlers);
let repaint = self.dispatch_inner(&mut handlers, event);
self.handlers = handlers;
repaint
}
fn dispatch_inner(&mut self, handlers: &mut [HandlerEntry], event: Event) -> bool {
let cut = self
.layers
.iter()
.rposition(|e| e.blocks_lower)
.unwrap_or(0);
let active: HashMap<LayerId, usize> = self.layers[cut..]
.iter()
.enumerate()
.map(|(off, e)| (e.id, cut + off))
.collect();
let is_mouse = matches!(event, Event::Mouse(_));
if let Event::Mouse(mouse) = event {
self.last_pointer = Some(mouse);
}
let frame = draw_clock_frame();
let hover_changed = if is_mouse
&& let Event::Mouse(m) = &event
&& matches!(m.kind, MouseEventKind::Moved)
{
let new_inside: HashSet<PointerToken> = self
.hover_trackers
.iter()
.filter(|(layer, _, stamp)| {
let s = stamp.get();
active.contains_key(layer)
&& s.frame == frame
&& rect_contains(s.screen_area, m.column, m.row)
})
.map(|(_, token, _)| *token)
.collect();
let changed = new_inside != self.hover_inside;
self.hover_inside = new_inside;
changed
} else {
false
};
let mut motion_changed = false;
if matches!(event, Event::Mouse(m) if matches!(m.kind, MouseEventKind::Moved)) {
for handler in handlers.iter_mut().filter(|handler| {
handler.motion_observer
&& handler
.layer
.is_some_and(|layer| active.contains_key(&layer))
}) {
motion_changed |= (handler.f)(event.clone()) == EventResult::Consumed;
}
}
if is_mouse
&& let Event::Mouse(m) = &event
&& let Some(button) = pressed_button(&m.kind)
{
let idx = button_index(button);
if let Some(token) = self.gestures[idx] {
let target = (0..handlers.len()).find(|&i| {
handlers[i].pointer == Some(token)
&& handlers[i].stamp.get().frame == frame
&& handlers[i].layer.is_some_and(|l| active.contains_key(&l))
});
match target {
Some(i) => {
let _ = (handlers[i].f)(event.clone());
if matches!(m.kind, MouseEventKind::Up(_)) {
self.gestures[idx] = None;
}
return true;
}
None => self.gestures[idx] = None,
}
}
}
let mut global_idx: Vec<usize> = (0..handlers.len())
.filter(|&i| handlers[i].layer.is_none() && !handlers[i].motion_observer)
.collect();
global_idx.sort_by(|&a, &b| {
handlers[b]
.priority
.cmp(&handlers[a].priority)
.then(handlers[a].order.cmp(&handlers[b].order))
});
if Self::run_handlers(handlers, &global_idx, &event, frame).is_some() {
return true;
}
let mut layer_idx: Vec<usize> = (0..handlers.len())
.filter(|&i| {
!handlers[i].motion_observer
&& handlers[i].layer.is_some_and(|l| active.contains_key(&l))
})
.collect();
if is_mouse {
layer_idx.sort_by(|&a, &b| {
let za = active[&handlers[a].layer.unwrap()];
let zb = active[&handlers[b].layer.unwrap()];
zb.cmp(&za) .then(handlers[b].priority.cmp(&handlers[a].priority)) .then(
handlers[b]
.stamp
.get()
.draw_seq
.cmp(&handlers[a].stamp.get().draw_seq),
) .then(handlers[a].order.cmp(&handlers[b].order)) });
} else {
layer_idx.sort_by(|&a, &b| {
let za = active[&handlers[a].layer.unwrap()];
let zb = active[&handlers[b].layer.unwrap()];
zb.cmp(&za)
.then(handlers[b].priority.cmp(&handlers[a].priority))
.then(handlers[a].order.cmp(&handlers[b].order))
});
}
let consumed_by = Self::run_handlers(handlers, &layer_idx, &event, frame);
let consumed = consumed_by.is_some();
if let (Some(i), Event::Mouse(m)) = (consumed_by, &event)
&& let MouseEventKind::Down(button) = m.kind
&& handlers[i].pointer.is_some()
&& handlers[i].options.hit_test
{
self.gestures[button_index(button)] = handlers[i].pointer;
}
if matches!(event, Event::Mouse(m) if matches!(m.kind, MouseEventKind::Moved)) {
return consumed || hover_changed || motion_changed;
}
true
}
fn run_handlers(
handlers: &mut [HandlerEntry],
order: &[usize],
event: &Event,
frame: u64,
) -> Option<usize> {
order
.iter()
.copied()
.find(|&i| Self::call_handler(&mut handlers[i], event, frame) == EventResult::Consumed)
}
fn call_handler(h: &mut HandlerEntry, event: &Event, frame: u64) -> EventResult {
if h.options.hit_test
&& let Event::Mouse(m) = event
{
let s = h.stamp.get();
if s.frame != frame || !rect_contains(s.screen_area, m.column, m.row) {
return EventResult::Ignored;
}
}
(h.f)(event.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
use std::cell::RefCell;
type Log = Rc<RefCell<Vec<&'static str>>>;
fn key() -> Event {
Event::Key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE))
}
fn mouse(kind: MouseEventKind, col: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind,
column: col,
row,
modifiers: KeyModifiers::NONE,
})
}
fn mouse_at(col: u16, row: u16) -> Event {
mouse(MouseEventKind::Down(MouseButton::Left), col, row)
}
fn stamp(area: Rect) -> StampHandle {
let h: StampHandle = Rc::new(Cell::new(Stamp::default()));
h.set(draw_clock_stamp(area));
h
}
fn undrawn(area: Rect) -> StampHandle {
Rc::new(Cell::new(Stamp {
area,
frame: 0,
draw_seq: 0,
..Stamp::default()
}))
}
fn handler(
log: &Log,
tag: &'static str,
result: EventResult,
) -> Box<dyn FnMut(Event) -> EventResult> {
let log = log.clone();
Box::new(move |_| {
log.borrow_mut().push(tag);
result
})
}
fn opts(hit_test: bool) -> EventOptions {
EventOptions { hit_test }
}
#[allow(clippy::too_many_arguments)]
fn register(
rt: &mut InputRuntime,
layer: Option<LayerId>,
options: EventOptions,
stamp: StampHandle,
token: Option<PointerToken>,
log: &Log,
tag: &'static str,
result: EventResult,
) {
rt.register_handler(
layer,
EventPriority::Normal,
options,
stamp,
token,
handler(log, tag, result),
);
}
#[test]
fn blocks_lower_truncates_background() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"bg",
EventResult::Ignored,
);
let modal = rt.push_layer(true, true);
register(
&mut rt,
Some(modal.id),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"modal",
EventResult::Ignored,
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["modal"]);
}
#[test]
fn nested_blocks_lower_activates_only_top() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"root",
EventResult::Ignored,
);
let l1 = rt.push_layer(true, true);
register(
&mut rt,
Some(l1.id),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"l1",
EventResult::Ignored,
);
let l2 = rt.push_layer(true, true);
register(
&mut rt,
Some(l2.id),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"l2",
EventResult::Ignored,
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["l2"]);
}
#[test]
fn non_blocking_layers_above_blocker_remain_active() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"root",
EventResult::Ignored,
);
let modal = rt.push_layer(true, true);
register(
&mut rt,
Some(modal.id),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"modal",
EventResult::Ignored,
);
let toast = rt.push_layer(true, false);
register(
&mut rt,
Some(toast.id),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"toast",
EventResult::Ignored,
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["toast", "modal"]);
}
#[test]
fn consumed_stops_subsequent() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"first",
EventResult::Consumed,
);
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"second",
EventResult::Ignored,
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["first"]);
}
#[test]
fn ignored_continues_propagation() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"first",
EventResult::Ignored,
);
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"second",
EventResult::Ignored,
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["first", "second"]);
}
#[test]
fn layer_z_order_beats_priority() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
rt.register_handler(
Some(root),
EventPriority::High,
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
handler(&log, "bg_high", EventResult::Ignored),
);
let top = rt.push_layer(true, false); rt.register_handler(
Some(top.id),
EventPriority::Normal,
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
handler(&log, "top_normal", EventResult::Ignored),
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["top_normal", "bg_high"]);
}
#[test]
fn global_phase_first_and_can_consume() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
None,
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"global",
EventResult::Consumed,
);
rt.register_handler(
Some(root),
EventPriority::High,
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
handler(&log, "layer", EventResult::Ignored),
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["global"]);
}
#[test]
fn global_ignored_does_not_truncate() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
None,
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"global",
EventResult::Ignored,
);
register(
&mut rt,
Some(root),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"layer",
EventResult::Ignored,
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["global", "layer"]);
}
#[test]
fn inactive_layer_handler_skipped() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let inactive = rt.push_layer(false, true); register(
&mut rt,
Some(inactive.id),
opts(false),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"inactive",
EventResult::Ignored,
);
rt.dispatch(key());
assert!(log.borrow().is_empty());
}
#[test]
fn hit_test_skips_outside_area() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
None,
&log,
"hit",
EventResult::Consumed,
);
rt.dispatch(mouse_at(50, 50)); assert!(log.borrow().is_empty());
rt.begin_frame(); let root2 = rt.root_layer();
register(
&mut rt,
Some(root2),
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
None,
&log,
"hit",
EventResult::Consumed,
);
rt.dispatch(mouse_at(5, 5)); assert_eq!(*log.borrow(), ["hit"]);
}
#[test]
fn mouse_child_before_parent() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"parent",
EventResult::Ignored,
);
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(10, 10, 5, 5)),
None,
&log,
"child",
EventResult::Ignored,
);
rt.dispatch(mouse_at(12, 12));
assert_eq!(*log.borrow(), ["child", "parent"]);
}
#[test]
fn mouse_overlapping_later_drawn_sibling_first() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 20, 5)),
None,
&log,
"early",
EventResult::Ignored,
);
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(10, 0, 20, 5)),
None,
&log,
"late",
EventResult::Ignored,
);
rt.dispatch(mouse_at(15, 2));
assert_eq!(*log.borrow(), ["late", "early"]);
}
#[test]
fn keyboard_order_unchanged_by_draw_seq() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"first",
EventResult::Ignored,
);
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"second",
EventResult::Ignored,
);
rt.dispatch(key());
assert_eq!(*log.borrow(), ["first", "second"]);
}
#[test]
fn stale_frame_stamp_never_hits() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
let stale = stamp(Rect::new(0, 0, 10, 10));
register(
&mut rt,
Some(root),
opts(true),
stale.clone(),
None,
&log,
"ghost",
EventResult::Consumed,
);
rt.begin_frame(); let root2 = rt.root_layer();
register(
&mut rt,
Some(root2),
opts(true),
stale,
None,
&log,
"ghost",
EventResult::Consumed,
);
rt.dispatch(mouse_at(5, 5));
assert!(log.borrow().is_empty());
}
#[test]
fn never_drawn_stamp_never_hits() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(true),
undrawn(Rect::new(0, 0, 10, 10)),
None,
&log,
"ghost",
EventResult::Consumed,
);
rt.dispatch(mouse_at(5, 5));
assert!(log.borrow().is_empty());
}
#[test]
fn scrim_click_outside_panel_bubbles() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"scrim",
EventResult::Consumed,
);
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(40, 40, 20, 5)),
None,
&log,
"panel",
EventResult::Consumed,
);
rt.dispatch(mouse_at(50, 42)); assert_eq!(*log.borrow(), ["panel"]);
log.borrow_mut().clear();
rt.begin_frame();
let root2 = rt.root_layer();
register(
&mut rt,
Some(root2),
opts(true),
stamp(Rect::new(0, 0, 100, 100)),
None,
&log,
"scrim",
EventResult::Consumed,
);
register(
&mut rt,
Some(root2),
opts(true),
stamp(Rect::new(40, 40, 20, 5)),
None,
&log,
"panel",
EventResult::Consumed,
);
rt.dispatch(mouse_at(5, 5)); assert_eq!(*log.borrow(), ["scrim"]);
}
#[test]
fn drag_capture_delivers_outside_area() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
let token = mint_pointer_token();
let rebuild = |rt: &mut InputRuntime| {
rt.begin_frame();
let root = rt.root_layer();
register(
rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
Some(token),
&log,
"captor",
EventResult::Consumed,
);
register(
rt,
Some(root),
opts(true),
stamp(Rect::new(40, 40, 20, 20)),
None,
&log,
"bystander",
EventResult::Consumed,
);
};
rebuild(&mut rt);
rt.dispatch(mouse_at(5, 5)); assert_eq!(*log.borrow(), ["captor"]);
log.borrow_mut().clear();
rebuild(&mut rt);
rt.dispatch(mouse(MouseEventKind::Drag(MouseButton::Left), 50, 50));
assert_eq!(*log.borrow(), ["captor"]);
log.borrow_mut().clear();
rebuild(&mut rt);
rt.dispatch(mouse(MouseEventKind::Up(MouseButton::Left), 50, 50));
assert_eq!(*log.borrow(), ["captor"]);
log.borrow_mut().clear();
rebuild(&mut rt);
rt.dispatch(mouse(MouseEventKind::Drag(MouseButton::Left), 50, 50));
assert_eq!(*log.borrow(), ["bystander"]);
}
#[test]
fn unmounted_captor_releases_capture() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
let token = mint_pointer_token();
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
Some(token),
&log,
"captor",
EventResult::Consumed,
);
rt.dispatch(mouse_at(5, 5));
assert_eq!(*log.borrow(), ["captor"]);
log.borrow_mut().clear();
rt.begin_frame();
let root2 = rt.root_layer();
register(
&mut rt,
Some(root2),
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
None,
&log,
"bystander",
EventResult::Consumed,
);
rt.dispatch(mouse(MouseEventKind::Up(MouseButton::Left), 5, 5));
assert_eq!(*log.borrow(), ["bystander"]);
}
#[test]
fn hidden_captor_is_cancelled_after_draw_and_never_revives() {
let mut runtime = InputRuntime::default();
let token = mint_pointer_token();
let cancelled = Rc::new(Cell::new(0));
let calls = Rc::new(Cell::new(0));
let rebuild = |runtime: &mut InputRuntime, drawn: bool, blocked: bool| {
runtime.begin_frame();
let root = runtime.root_layer();
let cancel_count = cancelled.clone();
let call_count = calls.clone();
runtime.register_pointer_handler(
root,
if drawn {
stamp(Rect::new(0, 0, 10, 10))
} else {
undrawn(Rect::new(0, 0, 10, 10))
},
token,
Box::new(move |_| {
call_count.set(call_count.get() + 1);
EventResult::Consumed
}),
Box::new(move |_| cancel_count.set(cancel_count.get() + 1)),
);
runtime.push_layer(blocked, true);
runtime.finish_frame();
};
for (drawn, blocked) in [(false, false), (true, true)] {
rebuild(&mut runtime, true, false);
runtime.dispatch(mouse_at(5, 5));
rebuild(&mut runtime, drawn, blocked);
assert!(runtime.gestures[0].is_none());
let before = calls.get();
rebuild(&mut runtime, true, false);
runtime.dispatch(mouse(MouseEventKind::Drag(MouseButton::Left), 50, 50));
assert_eq!(calls.get(), before);
}
assert_eq!(cancelled.get(), 2);
}
#[test]
fn unmounted_pointer_releases_capture_without_calling_disposed_callback() {
let mut runtime = InputRuntime::default();
runtime.begin_frame();
let cancelled = Rc::new(Cell::new(false));
let flag = cancelled.clone();
runtime.register_pointer_handler(
runtime.root_layer(),
stamp(Rect::new(0, 0, 10, 10)),
mint_pointer_token(),
Box::new(|_| EventResult::Consumed),
Box::new(move |_| flag.set(true)),
);
runtime.dispatch(mouse_at(5, 5));
runtime.begin_frame();
runtime.finish_frame();
assert!(!cancelled.get());
assert!(runtime.gestures[0].is_none());
}
#[test]
fn two_buttons_capture_independently() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
let token_a = mint_pointer_token();
let token_b = mint_pointer_token();
let rebuild = |rt: &mut InputRuntime| {
rt.begin_frame();
let root = rt.root_layer();
register(
rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
Some(token_a),
&log,
"a",
EventResult::Consumed,
);
register(
rt,
Some(root),
opts(true),
stamp(Rect::new(40, 40, 10, 10)),
Some(token_b),
&log,
"b",
EventResult::Consumed,
);
};
rebuild(&mut rt);
rt.dispatch(mouse(MouseEventKind::Down(MouseButton::Left), 5, 5));
rebuild(&mut rt);
rt.dispatch(mouse(MouseEventKind::Down(MouseButton::Right), 45, 45));
log.borrow_mut().clear();
rebuild(&mut rt);
rt.dispatch(mouse(MouseEventKind::Drag(MouseButton::Left), 45, 45));
assert_eq!(*log.borrow(), ["a"]);
log.borrow_mut().clear();
rebuild(&mut rt);
rt.dispatch(mouse(MouseEventKind::Drag(MouseButton::Right), 5, 5));
assert_eq!(*log.borrow(), ["b"]);
}
#[test]
fn hover_enter_leave_updates_state() {
let mut rt = InputRuntime::default();
rt.begin_frame();
let token = mint_pointer_token();
let tracker_area = stamp(Rect::new(0, 0, 10, 10));
rt.track_hover(rt.root_layer(), token, tracker_area);
let repaint = rt.dispatch(mouse(MouseEventKind::Moved, 5, 5));
assert!(repaint); assert!(rt.hover_contains(token));
let repaint = rt.dispatch(mouse(MouseEventKind::Moved, 50, 50));
assert!(repaint); assert!(!rt.hover_contains(token));
let repaint = rt.dispatch(mouse(MouseEventKind::Moved, 6, 6));
assert!(repaint); assert!(rt.hover_contains(token));
let repaint = rt.dispatch(mouse(MouseEventKind::Moved, 7, 7));
assert!(!repaint);
assert!(rt.hover_contains(token));
}
#[test]
fn blocking_modal_layer_clears_background_hover() {
let mut rt = InputRuntime::default();
let background = mint_pointer_token();
let modal = mint_pointer_token();
let area = Rect::new(0, 0, 10, 10);
rt.begin_frame();
rt.track_hover(rt.root_layer(), background, stamp(area));
rt.dispatch(mouse(MouseEventKind::Moved, 5, 5));
assert!(rt.hover_contains(background));
rt.begin_frame();
rt.track_hover(rt.root_layer(), background, stamp(area));
let layer = rt.push_layer(true, true);
rt.track_hover(layer.id, modal, stamp(area));
assert!(rt.dispatch(mouse(MouseEventKind::Moved, 5, 5)));
assert!(!rt.hover_contains(background));
assert!(rt.hover_contains(modal));
rt.begin_frame();
rt.track_hover(rt.root_layer(), background, stamp(area));
let layer = rt.push_layer(false, true);
rt.track_hover(layer.id, modal, stamp(area));
assert!(rt.dispatch(mouse(MouseEventKind::Moved, 5, 5)));
assert!(rt.hover_contains(background));
assert!(!rt.hover_contains(modal));
}
#[test]
fn global_consumer_does_not_prevent_hover_updates() {
let mut rt = InputRuntime::default();
rt.begin_frame();
let token = mint_pointer_token();
rt.track_hover(rt.root_layer(), token, stamp(Rect::new(0, 0, 10, 10)));
let log: Log = Default::default();
register(
&mut rt,
None,
opts(false),
stamp(Rect::default()),
None,
&log,
"global",
EventResult::Consumed,
);
assert!(rt.dispatch(mouse(MouseEventKind::Moved, 5, 5)));
assert!(rt.hover_contains(token));
assert!(rt.dispatch(mouse(MouseEventKind::Moved, 50, 50)));
assert!(!rt.hover_contains(token));
}
#[test]
fn moved_without_trackers_skips_repaint() {
let mut rt = InputRuntime::default();
rt.begin_frame();
assert!(!rt.dispatch(mouse(MouseEventKind::Moved, 5, 5)));
}
#[test]
fn moved_consumed_repaints() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
register(
&mut rt,
Some(root),
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
None,
&log,
"observer",
EventResult::Consumed,
);
assert!(rt.dispatch(mouse(MouseEventKind::Moved, 5, 5)));
}
#[test]
fn any_motion_upgrade_ratchet() {
let mut rt = InputRuntime::default();
rt.begin_frame();
assert!(!rt.needs_any_motion_upgrade());
let token = mint_pointer_token();
rt.track_hover(rt.root_layer(), token, stamp(Rect::new(0, 0, 10, 10)));
assert!(rt.needs_any_motion_upgrade());
rt.mark_any_motion_enabled();
assert!(!rt.needs_any_motion_upgrade());
}
#[test]
fn moved_skip_repaint_keeps_handler_registry() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
let token = mint_pointer_token();
let click_log = log.clone();
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(true),
stamp(Rect::new(0, 0, 10, 10)),
Some(token),
Box::new(move |event| {
if matches!(
event,
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
..
})
) {
return EventResult::Ignored;
}
click_log.borrow_mut().push("btn");
EventResult::Consumed
}),
);
assert!(!rt.dispatch(mouse(MouseEventKind::Moved, 5, 5)));
rt.dispatch(mouse_at(5, 5));
assert_eq!(*log.borrow(), ["btn"]);
}
#[test]
fn non_mouse_event_always_repaints() {
let mut rt = InputRuntime::default();
rt.begin_frame();
assert!(rt.dispatch(key()));
}
}