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 and delta
66    Scroll { column: u16, row: u16, delta: isize },
67    /// Terminal resize
68    Resize(u16, u16),
69    /// Periodic tick
70    Tick,
71}
72
73impl EventKind {
74    /// Get the event type for this event kind
75    pub fn event_type(&self) -> EventType {
76        match self {
77            EventKind::Key(_) => EventType::Key,
78            EventKind::Mouse(_) => EventType::Mouse,
79            EventKind::Scroll { .. } => EventType::Scroll,
80            EventKind::Resize(_, _) => EventType::Resize,
81            EventKind::Tick => EventType::Tick,
82        }
83    }
84
85    /// Check if this is a global event (should be delivered to all components)
86    pub fn is_global(&self) -> bool {
87        match self {
88            EventKind::Key(key) => {
89                use crossterm::event::KeyCode;
90                matches!(key.code, KeyCode::Esc)
91                    || (key.modifiers.contains(KeyModifiers::CONTROL)
92                        && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('q')))
93            }
94            EventKind::Resize(_, _) => true,
95            _ => false,
96        }
97    }
98}
99
100/// Context passed with every event
101///
102/// Generic over the component ID type `C` which must implement `ComponentId`.
103#[derive(Debug, Clone)]
104pub struct EventContext<C: ComponentId> {
105    /// Currently focused component
106    pub focused_component: Option<C>,
107    /// Current mouse position (if known)
108    pub mouse_position: Option<(u16, u16)>,
109    /// Active key modifiers
110    pub modifiers: KeyModifiers,
111    /// Component areas for hit-testing
112    pub component_areas: HashMap<C, Rect>,
113    /// Whether a modal is currently open
114    pub is_modal_open: bool,
115    /// The active modal (if any)
116    pub active_modal: Option<C>,
117}
118
119impl<C: ComponentId> Default for EventContext<C> {
120    fn default() -> Self {
121        Self {
122            focused_component: None,
123            mouse_position: None,
124            modifiers: KeyModifiers::NONE,
125            component_areas: HashMap::new(),
126            is_modal_open: false,
127            active_modal: None,
128        }
129    }
130}
131
132impl<C: ComponentId> EventContext<C> {
133    /// Create a new event context
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    /// Check if a component is focused
139    pub fn is_focused(&self, component: C) -> bool {
140        self.focused_component == Some(component)
141    }
142
143    /// Check if a point is within a component's area
144    pub fn point_in_component(&self, component: C, x: u16, y: u16) -> bool {
145        self.component_areas
146            .get(&component)
147            .map(|area| {
148                x >= area.x
149                    && x < area.x.saturating_add(area.width)
150                    && y >= area.y
151                    && y < area.y.saturating_add(area.height)
152            })
153            .unwrap_or(false)
154    }
155
156    /// Get the component at a given point
157    pub fn component_at(&self, x: u16, y: u16) -> Option<C> {
158        if let Some(modal) = self.active_modal {
159            if self.point_in_component(modal, x, y) {
160                return Some(modal);
161            }
162        }
163
164        for (&id, area) in &self.component_areas {
165            if Some(id) != self.active_modal
166                && x >= area.x
167                && x < area.x.saturating_add(area.width)
168                && y >= area.y
169                && y < area.y.saturating_add(area.height)
170            {
171                return Some(id);
172            }
173        }
174        None
175    }
176
177    /// Update the area for a component
178    pub fn set_component_area(&mut self, component: C, area: Rect) {
179        self.component_areas.insert(component, area);
180    }
181
182    /// Set the focused component
183    pub fn set_focus(&mut self, component: Option<C>) {
184        self.focused_component = component;
185    }
186
187    /// Set modal state
188    pub fn set_modal(&mut self, modal: Option<C>) {
189        self.active_modal = modal;
190        self.is_modal_open = modal.is_some();
191        if let Some(m) = modal {
192            self.focused_component = Some(m);
193        }
194    }
195}
196
197/// An event with its context
198///
199/// Generic over the component ID type `C` which must implement `ComponentId`.
200#[derive(Debug, Clone)]
201pub struct Event<C: ComponentId> {
202    /// The event payload
203    pub kind: EventKind,
204    /// Context at the time of the event
205    pub context: EventContext<C>,
206}
207
208impl<C: ComponentId> Event<C> {
209    /// Create a new event
210    pub fn new(kind: EventKind, context: EventContext<C>) -> Self {
211        Self { kind, context }
212    }
213
214    /// Get the event type
215    pub fn event_type(&self) -> EventType {
216        self.kind.event_type()
217    }
218
219    /// Check if this is a global event
220    pub fn is_global(&self) -> bool {
221        self.kind.is_global()
222    }
223}