use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent};
use ratatui::layout::Rect;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
pub trait ComponentId: Clone + Copy + Eq + Hash + Debug {
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NumericComponentId(pub u32);
impl ComponentId for NumericComponentId {
fn name(&self) -> &'static str {
"component"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EventType {
Key,
Mouse,
Scroll,
Resize,
Tick,
Global,
}
#[derive(Debug, Clone)]
pub enum EventKind {
Key(KeyEvent),
Mouse(MouseEvent),
Scroll {
column: u16,
row: u16,
delta: isize,
modifiers: KeyModifiers,
},
Resize(u16, u16),
Tick,
}
impl EventKind {
pub fn event_type(&self) -> EventType {
match self {
EventKind::Key(_) => EventType::Key,
EventKind::Mouse(_) => EventType::Mouse,
EventKind::Scroll { .. } => EventType::Scroll,
EventKind::Resize(_, _) => EventType::Resize,
EventKind::Tick => EventType::Tick,
}
}
pub fn is_global(&self) -> bool {
match self {
EventKind::Key(key) => {
use crossterm::event::KeyCode;
matches!(key.code, KeyCode::Esc)
|| (key.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(key.code, KeyCode::Char('c') | KeyCode::Char('q')))
}
EventKind::Resize(_, _) => true,
_ => false,
}
}
pub fn is_broadcast(&self) -> bool {
matches!(self, EventKind::Resize(..) | EventKind::Tick)
}
}
#[derive(Default)]
pub enum GlobalKeyPolicy {
#[default]
Default,
Keys(Vec<(KeyCode, KeyModifiers)>),
Custom(Box<dyn Fn(&EventKind) -> bool + Send + Sync>),
}
impl GlobalKeyPolicy {
pub fn none() -> Self {
Self::Keys(vec![])
}
pub fn without_esc() -> Self {
Self::Keys(vec![
(KeyCode::Char('c'), KeyModifiers::CONTROL),
(KeyCode::Char('q'), KeyModifiers::CONTROL),
])
}
pub fn keys(keys: Vec<(KeyCode, KeyModifiers)>) -> Self {
Self::Keys(keys)
}
pub fn custom(f: impl Fn(&EventKind) -> bool + Send + Sync + 'static) -> Self {
Self::Custom(Box::new(f))
}
pub fn is_global(&self, event: &EventKind) -> bool {
if matches!(event, EventKind::Resize(..)) {
return true;
}
match self {
Self::Default => event.is_global(),
Self::Keys(keys) => {
if let EventKind::Key(key) = event {
keys.iter()
.any(|(code, mods)| key.code == *code && key.modifiers.contains(*mods))
} else {
false
}
}
Self::Custom(f) => f(event),
}
}
}
#[derive(Debug, Clone)]
pub struct EventContext<C: ComponentId> {
pub mouse_position: Option<(u16, u16)>,
pub modifiers: KeyModifiers,
pub component_areas: HashMap<C, Rect>,
}
impl<C: ComponentId> Default for EventContext<C> {
fn default() -> Self {
Self {
mouse_position: None,
modifiers: KeyModifiers::NONE,
component_areas: HashMap::new(),
}
}
}
impl<C: ComponentId> EventContext<C> {
pub fn new() -> Self {
Self::default()
}
pub fn point_in_component(&self, component: C, x: u16, y: u16) -> bool {
self.component_areas
.get(&component)
.map(|area| {
x >= area.x
&& x < area.x.saturating_add(area.width)
&& y >= area.y
&& y < area.y.saturating_add(area.height)
})
.unwrap_or(false)
}
pub fn component_at(&self, x: u16, y: u16) -> Option<C> {
self.component_areas
.iter()
.find(|(_, area)| {
x >= area.x
&& x < area.x.saturating_add(area.width)
&& y >= area.y
&& y < area.y.saturating_add(area.height)
})
.map(|(id, _)| *id)
}
pub fn set_component_area(&mut self, component: C, area: Rect) {
self.component_areas.insert(component, area);
}
pub fn remove_component_area(&mut self, component: C) {
self.component_areas.remove(&component);
}
}
#[derive(Debug, Clone)]
pub struct Event<C: ComponentId> {
pub kind: EventKind,
pub context: EventContext<C>,
}
impl<C: ComponentId> Event<C> {
pub fn new(kind: EventKind, context: EventContext<C>) -> Self {
Self { kind, context }
}
pub fn event_type(&self) -> EventType {
self.kind.event_type()
}
pub fn is_global(&self) -> bool {
self.kind.is_global()
}
}