1use std::any::Any;
6use std::sync::Arc;
7
8pub trait Component: Send + Sync {
13 fn render(&self, width: usize) -> Vec<String>;
21
22 fn render_document(&self, width: usize) -> RenderedDocument {
27 RenderedDocument::from_lines(self.render(width))
28 }
29
30 fn handle_input(&self, _data: &str) -> bool {
38 false
39 }
40
41 fn wants_key_release(&self) -> bool {
44 false
45 }
46
47 fn invalidate(&self);
50
51 fn as_any(&self) -> &dyn Any;
54}
55
56#[derive(Clone, Default)]
61pub struct RenderedDocument {
62 blocks: Vec<Arc<Vec<String>>>,
63 ends: Vec<usize>,
64}
65
66impl RenderedDocument {
67 pub fn from_lines(lines: Vec<String>) -> Self {
69 Self::from_shared(Arc::new(lines))
70 }
71
72 pub fn from_shared(lines: Arc<Vec<String>>) -> Self {
74 if lines.is_empty() {
75 return Self::default();
76 }
77 Self {
78 ends: vec![lines.len()],
79 blocks: vec![lines],
80 }
81 }
82
83 pub fn append(&mut self, other: Self) {
85 let offset = self.len();
86 self.ends
87 .extend(other.ends.into_iter().map(|end| offset + end));
88 self.blocks.extend(other.blocks);
89 }
90
91 pub fn len(&self) -> usize {
93 self.ends.last().copied().unwrap_or(0)
94 }
95
96 pub fn is_empty(&self) -> bool {
98 self.blocks.is_empty()
99 }
100
101 pub fn lines(&self, start: usize, height: usize) -> Vec<String> {
103 let mut index = self.ends.partition_point(|end| *end <= start);
104 let mut offset = start.saturating_sub(if index == 0 { 0 } else { self.ends[index - 1] });
105 let mut visible = Vec::with_capacity(height.min(self.len().saturating_sub(start)));
106 while index < self.blocks.len() && visible.len() < height {
107 let block = &self.blocks[index];
108 visible.extend(
109 block
110 .iter()
111 .skip(offset)
112 .take(height - visible.len())
113 .cloned(),
114 );
115 index += 1;
116 offset = 0;
117 }
118 visible
119 }
120}
121
122pub trait Focusable: Component {
128 fn set_focused(&self, focused: bool);
130
131 fn is_focused(&self) -> bool;
133}
134
135pub type ComponentRef = Arc<dyn Component>;
137
138pub type FocusableRef = Arc<dyn Focusable>;
140
141pub const CURSOR_MARKER: &str = "\x1b_pi:c\x07";
146
147pub fn is_focusable(component: &ComponentRef) -> bool {
149 component
150 .as_any()
151 .downcast_ref::<Arc<dyn Focusable>>()
152 .is_some()
153 || component.as_any().type_id() == std::any::TypeId::of::<Arc<dyn Focusable>>()
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[allow(dead_code)]
161 struct TestComponent {
162 content: String,
163 }
164
165 impl Component for TestComponent {
166 fn render(&self, _width: usize) -> Vec<String> {
167 vec![self.content.clone()]
168 }
169
170 fn invalidate(&self) {}
171
172 fn as_any(&self) -> &dyn Any {
173 self
174 }
175 }
176
177 #[test]
178 fn document_composition_retains_shared_rows_and_slices_across_blocks() {
179 let first = Arc::new(vec!["one".into(), "two".into()]);
180 let second = Arc::new(vec!["three".into(), "four".into()]);
181 let mut document = RenderedDocument::from_shared(first.clone());
182 document.append(RenderedDocument::default());
183 document.append(RenderedDocument::from_shared(second.clone()));
184 assert!(Arc::ptr_eq(&document.blocks[0], &first));
185 assert!(Arc::ptr_eq(&document.blocks[1], &second));
186 assert_eq!(document.len(), 4);
187 assert_eq!(document.lines(1, 2), ["two", "three"]);
188 assert_eq!(document.lines(2, usize::MAX), ["three", "four"]);
189 assert!(document.lines(4, 1).is_empty());
190 assert!(document.lines(usize::MAX, 1).is_empty());
191 assert!(document.lines(0, 0).is_empty());
192 }
193
194 #[test]
195 fn document_viewport_does_not_copy_large_history() {
196 for size in [1, 1_000, 10_000] {
197 let rows = Arc::new(vec!["historical row".repeat(64); size]);
198 let document = RenderedDocument::from_shared(rows.clone());
199 let copy = document.clone();
200 assert!(Arc::ptr_eq(©.blocks[0], &rows));
201 assert_eq!(copy.lines(size - 1, 1), [rows[size - 1].clone()]);
202 assert_eq!(Arc::strong_count(&rows), 3);
203 }
204 }
205
206 #[test]
207 fn test_cursor_marker() {
208 assert!(CURSOR_MARKER.starts_with("\x1b"));
209 assert!(CURSOR_MARKER.ends_with("\x07"));
210 }
211}