rpi_tui/component.rs
1//! Component trait and base types for TUI.
2//!
3//! Mirrors the TypeScript `Component` interface from `packages/tui/src/tui.ts`.
4
5use std::any::Any;
6use std::sync::Arc;
7
8/// Component interface - all components must implement this.
9///
10/// Components are the building blocks of the TUI. Each component can render
11/// itself to a list of strings (one per line) for a given viewport width.
12pub trait Component: Send + Sync {
13 /// Render the component to lines for the given viewport width.
14 ///
15 /// # Arguments
16 /// * `width` - Current viewport width in columns
17 ///
18 /// # Returns
19 /// Array of strings, each representing a line
20 fn render(&self, width: usize) -> Vec<String>;
21
22 /// Optional handler for keyboard input when component has focus.
23 ///
24 /// # Arguments
25 /// * `data` - Raw input data (key sequence)
26 ///
27 /// # Returns
28 /// true if the input was consumed, false otherwise
29 fn handle_input(&self, _data: &str) -> bool {
30 false
31 }
32
33 /// If true, component receives key release events (Kitty protocol).
34 /// Default is false - release events are filtered out.
35 fn wants_key_release(&self) -> bool {
36 false
37 }
38
39 /// Invalidate any cached rendering state.
40 /// Called when theme changes or when component needs to re-render from scratch.
41 fn invalidate(&self);
42
43 /// Downcast to Any for runtime type checking.
44 /// This enables the layout system to check for LayoutNodeProvider.
45 fn as_any(&self) -> &dyn Any;
46}
47
48/// Interface for components that can receive focus and display a hardware cursor.
49///
50/// When focused, the component should emit `CURSOR_MARKER` at the cursor position
51/// in its render output. TUI will find this marker and position the hardware
52/// cursor there for proper IME candidate window positioning.
53pub trait Focusable: Component {
54 /// Set by TUI when focus changes. Component should emit CURSOR_MARKER when true.
55 fn set_focused(&self, focused: bool);
56
57 /// Check if the component is currently focused.
58 fn is_focused(&self) -> bool;
59}
60
61/// Type alias for a reference-counted component.
62pub type ComponentRef = Arc<dyn Component>;
63
64/// Type alias for a reference-counted focusable component.
65pub type FocusableRef = Arc<dyn Focusable>;
66
67/// Cursor position marker - APC (Application Program Command) sequence.
68/// This is a zero-width escape sequence that terminals ignore.
69/// Components emit this at the cursor position when focused.
70/// TUI finds and strips this marker, then positions the hardware cursor there.
71pub const CURSOR_MARKER: &str = "\x1b_pi:c\x07";
72
73/// Helper to check if a component implements Focusable.
74pub fn is_focusable(component: &ComponentRef) -> bool {
75 component
76 .as_any()
77 .downcast_ref::<Arc<dyn Focusable>>()
78 .is_some()
79 || component.as_any().type_id() == std::any::TypeId::of::<Arc<dyn Focusable>>()
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[allow(dead_code)]
87 struct TestComponent {
88 content: String,
89 }
90
91 impl Component for TestComponent {
92 fn render(&self, _width: usize) -> Vec<String> {
93 vec![self.content.clone()]
94 }
95
96 fn invalidate(&self) {}
97
98 fn as_any(&self) -> &dyn Any {
99 self
100 }
101 }
102
103 #[test]
104 fn test_cursor_marker() {
105 assert!(CURSOR_MARKER.starts_with("\x1b"));
106 assert!(CURSOR_MARKER.ends_with("\x07"));
107 }
108}