tui_dispatch_core/event.rs
1//! Event types for the pub/sub system
2
3use crossterm::event::{KeyCode, 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/// Policy for determining which events are treated as global.
111///
112/// Global events bypass modal blocking and are delivered to global subscribers
113/// even when a modal is active. Resize events are always treated as global
114/// regardless of this policy.
115///
116/// By default, Esc, Ctrl+C, and Ctrl+Q are global keys. Use this to customize
117/// that behavior — for example, if your app uses Esc for "close modal" and
118/// doesn't want it treated as global.
119///
120/// # Example
121///
122/// ```ignore
123/// use tui_dispatch::{EventBus, GlobalKeyPolicy};
124///
125/// // Remove Esc from global keys (keep only Ctrl+C, Ctrl+Q)
126/// let bus = EventBus::new()
127/// .with_global_key_policy(GlobalKeyPolicy::without_esc());
128///
129/// // Custom set of global keys
130/// let bus = EventBus::new()
131/// .with_global_key_policy(GlobalKeyPolicy::keys(vec![
132/// (KeyCode::Char('c'), KeyModifiers::CONTROL),
133/// ]));
134///
135/// // No key events are global (only Resize remains global)
136/// let bus = EventBus::new()
137/// .with_global_key_policy(GlobalKeyPolicy::none());
138/// ```
139#[derive(Default)]
140pub enum GlobalKeyPolicy {
141 /// Default: Esc, Ctrl+C, Ctrl+Q are global. Same as `EventKind::is_global()`.
142 #[default]
143 Default,
144 /// Only these specific key combinations are global.
145 /// Each entry is `(KeyCode, required_modifiers)`.
146 Keys(Vec<(KeyCode, KeyModifiers)>),
147 /// Custom predicate function.
148 Custom(Box<dyn Fn(&EventKind) -> bool + Send + Sync>),
149}
150
151impl GlobalKeyPolicy {
152 /// No key events are global (only Resize remains global).
153 pub fn none() -> Self {
154 Self::Keys(vec![])
155 }
156
157 /// Default without Esc — only Ctrl+C and Ctrl+Q are global.
158 ///
159 /// Useful for apps that use Esc to close modals/dialogs.
160 pub fn without_esc() -> Self {
161 Self::Keys(vec![
162 (KeyCode::Char('c'), KeyModifiers::CONTROL),
163 (KeyCode::Char('q'), KeyModifiers::CONTROL),
164 ])
165 }
166
167 /// Only these specific key combinations are global.
168 ///
169 /// Each entry is `(KeyCode, required_modifiers)` — the key matches if
170 /// its code equals the given code and its modifiers contain the
171 /// required modifiers.
172 pub fn keys(keys: Vec<(KeyCode, KeyModifiers)>) -> Self {
173 Self::Keys(keys)
174 }
175
176 /// Custom predicate for full control over global classification.
177 ///
178 /// Resize events are still always global regardless of the predicate.
179 pub fn custom(f: impl Fn(&EventKind) -> bool + Send + Sync + 'static) -> Self {
180 Self::Custom(Box::new(f))
181 }
182
183 /// Check whether an event is global under this policy.
184 ///
185 /// Resize events always return true regardless of the policy.
186 pub fn is_global(&self, event: &EventKind) -> bool {
187 // Resize is always global
188 if matches!(event, EventKind::Resize(..)) {
189 return true;
190 }
191 match self {
192 Self::Default => event.is_global(),
193 Self::Keys(keys) => {
194 if let EventKind::Key(key) = event {
195 keys.iter()
196 .any(|(code, mods)| key.code == *code && key.modifiers.contains(*mods))
197 } else {
198 false
199 }
200 }
201 Self::Custom(f) => f(event),
202 }
203 }
204}
205
206/// Context passed with every event
207///
208/// Generic over the component ID type `C` which must implement `ComponentId`.
209#[derive(Debug, Clone)]
210pub struct EventContext<C: ComponentId> {
211 /// Current mouse position (if known)
212 pub mouse_position: Option<(u16, u16)>,
213 /// Active key modifiers
214 pub modifiers: KeyModifiers,
215 /// Component areas for hit-testing
216 pub component_areas: HashMap<C, Rect>,
217}
218
219impl<C: ComponentId> Default for EventContext<C> {
220 fn default() -> Self {
221 Self {
222 mouse_position: None,
223 modifiers: KeyModifiers::NONE,
224 component_areas: HashMap::new(),
225 }
226 }
227}
228
229impl<C: ComponentId> EventContext<C> {
230 /// Create a new event context
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 /// Check if a point is within a component's area
236 pub fn point_in_component(&self, component: C, x: u16, y: u16) -> bool {
237 self.component_areas
238 .get(&component)
239 .map(|area| {
240 x >= area.x
241 && x < area.x.saturating_add(area.width)
242 && y >= area.y
243 && y < area.y.saturating_add(area.height)
244 })
245 .unwrap_or(false)
246 }
247
248 /// Get the first component containing a point
249 ///
250 /// The ordering is based on the internal map iteration order and should be
251 /// treated as undefined. Prefer routing through `EventBus` when ordering
252 /// matters.
253 pub fn component_at(&self, x: u16, y: u16) -> Option<C> {
254 self.component_areas
255 .iter()
256 .find(|(_, area)| {
257 x >= area.x
258 && x < area.x.saturating_add(area.width)
259 && y >= area.y
260 && y < area.y.saturating_add(area.height)
261 })
262 .map(|(id, _)| *id)
263 }
264
265 /// Update the area for a component
266 pub fn set_component_area(&mut self, component: C, area: Rect) {
267 self.component_areas.insert(component, area);
268 }
269}
270
271/// An event with its context
272///
273/// Generic over the component ID type `C` which must implement `ComponentId`.
274#[derive(Debug, Clone)]
275pub struct Event<C: ComponentId> {
276 /// The event payload
277 pub kind: EventKind,
278 /// Context at the time of the event
279 pub context: EventContext<C>,
280}
281
282impl<C: ComponentId> Event<C> {
283 /// Create a new event
284 pub fn new(kind: EventKind, context: EventContext<C>) -> Self {
285 Self { kind, context }
286 }
287
288 /// Get the event type
289 pub fn event_type(&self) -> EventType {
290 self.kind.event_type()
291 }
292
293 /// Check if this is a global event
294 pub fn is_global(&self) -> bool {
295 self.kind.is_global()
296 }
297}