Skip to main content

rab/tui/
component.rs

1use crossterm::event::KeyEvent;
2
3use crate::tui::util::visible_width;
4
5/// Key for render caching — components return this to indicate when cache is valid.
6/// Two renders with the same cache key produce identical output.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct RenderCacheKey {
9    /// Viewport width.
10    pub width: usize,
11    /// Whether expanded (for collapsible components).
12    pub expanded: bool,
13    /// Additional state hash (tool name, args hash, etc.).
14    pub state_hash: u64,
15}
16
17/// Cached render output.
18#[derive(Debug, Clone)]
19pub struct RenderCache {
20    /// The cache key used to generate this output.
21    pub key: RenderCacheKey,
22    /// Rendered lines.
23    pub lines: Vec<String>,
24}
25
26/// Every renderable UI element.
27pub trait Component {
28    /// Render to lines for the given viewport width.
29    /// Each returned string MUST NOT exceed `width` in visible width.
30    fn render(&mut self, width: usize) -> Vec<String>;
31
32    /// Render and pad each line to exactly `width` visible columns.
33    /// Default implementation calls `render(width)` and pads each line
34    /// with trailing spaces if its visible width is less than `width`.
35    fn render_padded(&mut self, width: usize) -> Vec<String> {
36        let lines = self.render(width);
37        lines
38            .into_iter()
39            .map(|line| {
40                let vw = visible_width(&line);
41                if vw < width {
42                    format!("{}{}", line, " ".repeat(width - vw))
43                } else {
44                    line
45                }
46            })
47            .collect()
48    }
49
50    /// Handle keyboard input. Return `true` if consumed.
51    fn handle_input(&mut self, _key: &KeyEvent) -> bool {
52        false
53    }
54
55    /// Handle a paste event (text from bracketed paste mode).
56    /// Default no-op; override to process pasted content.
57    fn handle_paste(&mut self, _text: &str) {}
58
59    /// Mark this component as needing re-render.
60    /// Called when internal state changes (output received, expanded toggled, etc.).
61    fn invalidate(&mut self) {}
62
63    /// Check if this component needs re-render.
64    /// Default: false — the Container's per-child cache tracking determines
65    /// whether to re-render. Override to return true for components whose
66    /// state can change without explicit invalidation (e.g. ToolExecComponent
67    /// receiving streaming output).
68    fn is_dirty(&self) -> bool {
69        false
70    }
71
72    /// Clear dirty flag after successful render.
73    fn clear_dirty(&mut self) {}
74
75    /// Get the cache key for this component's current state.
76    /// Return None to disable caching (always re-render).
77    fn cache_key(&self, _width: usize) -> Option<RenderCacheKey> {
78        None
79    }
80
81    /// Get cached render output, if available and valid.
82    fn get_cached_render(&self) -> Option<&RenderCache> {
83        None
84    }
85
86    /// Store render output in cache.
87    fn set_cached_render(&mut self, _cache: RenderCache) {}
88
89    /// Whether this component wants focus (for IME cursor positioning).
90    fn is_focusable(&self) -> bool {
91        false
92    }
93
94    /// Toggle expanded/collapsed state. No-op by default.
95    /// Override for components that support expand/collapse (tool results, messages, etc.).
96    fn set_expanded(&mut self, _expanded: bool) {}
97
98    /// Toggle thinking block visibility. No-op by default.
99    /// Override for components that display thinking content (AssistantMessageComponent).
100    fn set_hide_thinking(&mut self, _hide: bool) {}
101}