Skip to main content

tui_dispatch_core/
component.rs

1//! Component trait for pure UI elements
2
3use ratatui::{layout::Rect, Frame};
4
5use crate::event::EventKind;
6
7/// A pure UI component that renders based on props and emits actions
8///
9/// Components follow these rules:
10/// 1. Props contain ALL read-only data needed for rendering
11/// 2. `handle_event` returns actions, never mutates external state
12/// 3. `render` is a pure function of props (plus internal UI state like scroll position)
13///
14/// Internal UI state (scroll position, selection highlight) can be stored in `&mut self`,
15/// but data mutations must go through actions.
16///
17/// # Focus and Context
18///
19/// Components receive `EventKind` (the raw event) rather than the full `Event` with context.
20/// Focus information and other context should be passed through `Props`. This keeps components
21/// decoupled from the specific ComponentId type used by the application.
22///
23/// # Example
24///
25/// ```
26/// use tui_dispatch_core::{Component, EventKind};
27/// use ratatui::{Frame, layout::Rect, widgets::Paragraph};
28/// use crossterm::event::KeyCode;
29///
30/// #[derive(Clone)]
31/// enum Action { Increment, Decrement }
32///
33/// struct Counter;
34///
35/// struct CounterProps { count: i32, is_focused: bool }
36///
37/// impl Component<Action> for Counter {
38///     type Props<'a> = CounterProps;
39///
40///     fn handle_event(
41///         &mut self,
42///         event: &EventKind,
43///         props: Self::Props<'_>,
44///     ) -> impl IntoIterator<Item = Action> {
45///         if !props.is_focused { return None; }
46///         if let EventKind::Key(key) = event {
47///             match key.code {
48///                 KeyCode::Up => return Some(Action::Increment),
49///                 KeyCode::Down => return Some(Action::Decrement),
50///                 _ => {}
51///             }
52///         }
53///         None
54///     }
55///
56///     fn render(&mut self, frame: &mut Frame, area: Rect, props: Self::Props<'_>) {
57///         let text = format!("Count: {}", props.count);
58///         frame.render_widget(Paragraph::new(text), area);
59///     }
60/// }
61/// ```
62pub trait Component<A> {
63    /// Data required to render the component (read-only)
64    type Props<'a>;
65
66    /// Handle an event and return actions to dispatch
67    ///
68    /// Components receive the raw `EventKind` (key press, mouse event, etc.).
69    /// Focus state and other context should be passed through `Props`.
70    ///
71    /// Returns any type implementing `IntoIterator<Item = A>`:
72    /// - `None` - no actions (most common)
73    /// - `Some(action)` - single action
74    /// - `[a, b]` or `vec![...]` - multiple actions
75    ///
76    /// Default implementation returns no actions (render-only components).
77    #[allow(unused_variables)]
78    fn handle_event(
79        &mut self,
80        event: &EventKind,
81        props: Self::Props<'_>,
82    ) -> impl IntoIterator<Item = A> {
83        None::<A>
84    }
85
86    /// Render the component to the frame
87    fn render(&mut self, frame: &mut Frame, area: Rect, props: Self::Props<'_>);
88}