Skip to main content

tui_dispatch_core/
event.rs

1//! Event types for the pub/sub system
2
3use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent};
4use ratatui::layout::Rect;
5use std::collections::HashMap;
6use std::fmt::Debug;
7use std::hash::Hash;
8
9/// Trait for user-defined component identifiers
10///
11/// Implement this trait for your own component ID enum, or use `#[derive(ComponentId)]`
12/// from `tui-dispatch-macros` to auto-generate the implementation.
13///
14/// # Example
15/// ```ignore
16/// #[derive(ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]
17/// pub enum MyComponentId {
18///     Sidebar,
19///     MainContent,
20///     StatusBar,
21/// }
22/// ```
23pub trait ComponentId: Clone + Copy + Eq + Hash + Debug {
24    /// Get the component name as a string (for debugging/logging)
25    fn name(&self) -> &'static str;
26}
27
28/// A simple numeric component ID for basic use cases
29///
30/// Use this if you don't need named components, or use your own enum
31/// with `#[derive(ComponentId)]` for named components.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub struct NumericComponentId(pub u32);
34
35impl ComponentId for NumericComponentId {
36    fn name(&self) -> &'static str {
37        "component"
38    }
39}
40
41/// Event types that components can subscribe to
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum EventType {
44    /// Keyboard events
45    Key,
46    /// Mouse click/drag events
47    Mouse,
48    /// Scroll wheel events
49    Scroll,
50    /// Terminal resize events
51    Resize,
52    /// Periodic tick for animations
53    Tick,
54    /// Global events delivered to all components
55    Global,
56}
57
58/// The actual event payload
59#[derive(Debug, Clone)]
60pub enum EventKind {
61    /// Keyboard event
62    Key(KeyEvent),
63    /// Mouse event
64    Mouse(MouseEvent),
65    /// Scroll event with position, delta, and modifiers
66    Scroll {
67        column: u16,
68        row: u16,
69        delta: isize,
70        modifiers: KeyModifiers,
71    },
72    /// Terminal resize
73    Resize(u16, u16),
74    /// Periodic tick
75    Tick,
76}
77
78impl EventKind {
79    /// Get the event type for this event kind
80    pub fn event_type(&self) -> EventType {
81        match self {
82            EventKind::Key(_) => EventType::Key,
83            EventKind::Mouse(_) => EventType::Mouse,
84            EventKind::Scroll { .. } => EventType::Scroll,
85            EventKind::Resize(_, _) => EventType::Resize,
86            EventKind::Tick => EventType::Tick,
87        }
88    }
89
90    /// Check if this is a global event (should be delivered to all components)
91    pub fn is_global(&self) -> bool {
92        match self {
93            EventKind::Key(key) => {
94                use crossterm::event::KeyCode;
95                matches!(key.code, KeyCode::Esc)
96                    || (key.modifiers.contains(KeyModifiers::CONTROL)
97                        && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('q')))
98            }
99            EventKind::Resize(_, _) => true,
100            _ => false,
101        }
102    }
103
104    /// Check if this is a broadcast event (delivered to all subscribers, never consumed)
105    pub fn is_broadcast(&self) -> bool {
106        matches!(self, EventKind::Resize(..) | EventKind::Tick)
107    }
108}
109
110/// Context passed with every event
111///
112/// Generic over the component ID type `C` which must implement `ComponentId`.
113#[derive(Debug, Clone)]
114pub struct EventContext<C: ComponentId> {
115    /// Current mouse position (if known)
116    pub mouse_position: Option<(u16, u16)>,
117    /// Active key modifiers
118    pub modifiers: KeyModifiers,
119    /// Component areas for hit-testing
120    pub component_areas: HashMap<C, Rect>,
121}
122
123impl<C: ComponentId> Default for EventContext<C> {
124    fn default() -> Self {
125        Self {
126            mouse_position: None,
127            modifiers: KeyModifiers::NONE,
128            component_areas: HashMap::new(),
129        }
130    }
131}
132
133impl<C: ComponentId> EventContext<C> {
134    /// Create a new event context
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    /// Check if a point is within a component's area
140    pub fn point_in_component(&self, component: C, x: u16, y: u16) -> bool {
141        self.component_areas
142            .get(&component)
143            .map(|area| {
144                x >= area.x
145                    && x < area.x.saturating_add(area.width)
146                    && y >= area.y
147                    && y < area.y.saturating_add(area.height)
148            })
149            .unwrap_or(false)
150    }
151
152    /// Get the first component containing a point
153    ///
154    /// The ordering is based on the internal map iteration order and should be
155    /// treated as undefined. Prefer routing through `EventBus` when ordering
156    /// matters.
157    pub fn component_at(&self, x: u16, y: u16) -> Option<C> {
158        self.component_areas
159            .iter()
160            .find(|(_, area)| {
161                x >= area.x
162                    && x < area.x.saturating_add(area.width)
163                    && y >= area.y
164                    && y < area.y.saturating_add(area.height)
165            })
166            .map(|(id, _)| *id)
167    }
168
169    /// Update the area for a component
170    pub fn set_component_area(&mut self, component: C, area: Rect) {
171        self.component_areas.insert(component, area);
172    }
173}
174
175/// An event with its context
176///
177/// Generic over the component ID type `C` which must implement `ComponentId`.
178#[derive(Debug, Clone)]
179pub struct Event<C: ComponentId> {
180    /// The event payload
181    pub kind: EventKind,
182    /// Context at the time of the event
183    pub context: EventContext<C>,
184}
185
186impl<C: ComponentId> Event<C> {
187    /// Create a new event
188    pub fn new(kind: EventKind, context: EventContext<C>) -> Self {
189        Self { kind, context }
190    }
191
192    /// Get the event type
193    pub fn event_type(&self) -> EventType {
194        self.kind.event_type()
195    }
196
197    /// Check if this is a global event
198    pub fn is_global(&self) -> bool {
199        self.kind.is_global()
200    }
201}