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}