use std::{cell::Cell, collections::HashMap, rc::Rc};
use crossterm::event::Event;
use ratatui::layout::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,
}
struct HandlerEntry {
layer: Option<LayerId>,
priority: EventPriority,
order: usize,
options: EventOptions,
area: Rc<Cell<Rect>>,
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>,
}
impl InputRuntime {
pub(crate) fn begin_frame(&mut self) {
self.layers.clear();
self.handlers.clear();
let root = self.mint_layer_id();
self.root_layer = Some(root);
self.layers.push(LayerEntry {
id: root,
blocks_lower: false,
});
}
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,
area: Rc<Cell<Rect>>,
f: Box<dyn FnMut(Event) -> EventResult>,
) {
let order = self.handlers.len();
self.handlers.push(HandlerEntry {
layer,
priority,
order,
options,
area,
f,
});
}
pub(crate) fn dispatch(&mut self, event: Event) {
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 mut handlers = std::mem::take(&mut self.handlers);
let mut global_idx: Vec<usize> = (0..handlers.len())
.filter(|&i| handlers[i].layer.is_none())
.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(&mut handlers, &global_idx, &event) {
return;
}
let mut layer_idx: Vec<usize> = (0..handlers.len())
.filter(|&i| handlers[i].layer.is_some_and(|l| active.contains_key(&l)))
.collect();
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)) });
Self::run_handlers(&mut handlers, &layer_idx, &event);
}
fn run_handlers(handlers: &mut [HandlerEntry], order: &[usize], event: &Event) -> bool {
for &i in order {
if Self::call_handler(&mut handlers[i], event) == EventResult::Consumed {
return true;
}
}
false
}
fn call_handler(h: &mut HandlerEntry, event: &Event) -> EventResult {
if h.options.hit_test
&& let Event::Mouse(m) = event
{
let a = h.area.get();
let hit = m.column >= a.x
&& m.column < a.x.saturating_add(a.width)
&& m.row >= a.y
&& m.row < a.y.saturating_add(a.height);
if !hit {
return EventResult::Ignored;
}
}
(h.f)(event.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{
KeyCode, KeyEvent, KeyModifiers, MouseButton, 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_at(col: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: col,
row,
modifiers: KeyModifiers::NONE,
})
}
fn full_area() -> Rc<Cell<Rect>> {
Rc::new(Cell::new(Rect::new(0, 0, 100, 100)))
}
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 }
}
#[test]
fn blocks_lower_truncates_background() {
let log: Log = Default::default();
let mut rt = InputRuntime::default();
rt.begin_frame();
let root = rt.root_layer();
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "bg", EventResult::Ignored),
);
let modal = rt.push_layer(true, true);
rt.register_handler(
Some(modal.id),
EventPriority::Normal,
opts(false),
full_area(),
handler(&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();
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "root", EventResult::Ignored),
);
let l1 = rt.push_layer(true, true);
rt.register_handler(
Some(l1.id),
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "l1", EventResult::Ignored),
);
let l2 = rt.push_layer(true, true);
rt.register_handler(
Some(l2.id),
EventPriority::Normal,
opts(false),
full_area(),
handler(&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();
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "root", EventResult::Ignored),
);
let modal = rt.push_layer(true, true);
rt.register_handler(
Some(modal.id),
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "modal", EventResult::Ignored),
);
let toast = rt.push_layer(true, false);
rt.register_handler(
Some(toast.id),
EventPriority::Normal,
opts(false),
full_area(),
handler(&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();
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "first", EventResult::Consumed),
);
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&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();
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "first", EventResult::Ignored),
);
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&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),
full_area(),
handler(&log, "bg_high", EventResult::Ignored),
);
let top = rt.push_layer(true, false); rt.register_handler(
Some(top.id),
EventPriority::Normal,
opts(false),
full_area(),
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();
rt.register_handler(
None,
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "global", EventResult::Consumed),
);
rt.register_handler(
Some(root),
EventPriority::High,
opts(false),
full_area(),
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();
rt.register_handler(
None,
EventPriority::Normal,
opts(false),
full_area(),
handler(&log, "global", EventResult::Ignored),
);
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(false),
full_area(),
handler(&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); rt.register_handler(
Some(inactive.id),
EventPriority::Normal,
opts(false),
full_area(),
handler(&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();
let area = Rc::new(Cell::new(Rect::new(0, 0, 10, 10)));
rt.register_handler(
Some(root),
EventPriority::Normal,
opts(true),
area,
handler(&log, "hit", EventResult::Consumed),
);
rt.dispatch(mouse_at(50, 50)); assert!(log.borrow().is_empty());
rt.begin_frame(); let root2 = rt.root_layer();
let area2 = Rc::new(Cell::new(Rect::new(0, 0, 10, 10)));
rt.register_handler(
Some(root2),
EventPriority::Normal,
opts(true),
area2,
handler(&log, "hit", EventResult::Consumed),
);
rt.dispatch(mouse_at(5, 5)); assert_eq!(*log.borrow(), ["hit"]);
}
}