Skip to main content

rpi_tui/
box_component.rs

1//! Box component - a container that applies padding and background to children.
2//!
3//! Similar to a div with padding and background in CSS.
4
5use std::any::Any;
6use std::sync::{Arc, Mutex};
7
8use super::component::Component;
9use crate::utils::{apply_background_to_line, visible_width};
10
11/// Render cache for Box component.
12#[derive(Debug, Clone)]
13struct RenderCache {
14    child_lines: Vec<String>,
15    width: usize,
16    bg_sample: Option<String>,
17    lines: Vec<String>,
18}
19
20/// Box component - a container with padding and optional background.
21pub struct Box {
22    children: Mutex<Vec<Arc<dyn Component>>>,
23    padding_x: usize,
24    padding_y: usize,
25    bg_fn: Mutex<Option<Arc<dyn Fn(&str) -> String + Send + Sync>>>,
26    cache: Mutex<Option<RenderCache>>,
27}
28
29impl Box {
30    /// Create a new box with specified padding.
31    pub fn new(padding_x: usize, padding_y: usize) -> Self {
32        Self {
33            children: Mutex::new(Vec::new()),
34            padding_x,
35            padding_y,
36            bg_fn: Mutex::new(None),
37            cache: Mutex::new(None),
38        }
39    }
40
41    /// Create a box with default padding (1, 1).
42    pub fn simple() -> Self {
43        Self::new(1, 1)
44    }
45
46    /// Create a box with no padding.
47    pub fn no_padding() -> Self {
48        Self::new(0, 0)
49    }
50
51    /// Add a child component.
52    pub fn add_child(&self, component: Arc<dyn Component>) {
53        if let Ok(mut children) = self.children.lock() {
54            children.push(component);
55        }
56        self.invalidate_cache();
57    }
58
59    /// Remove a child component.
60    pub fn remove_child(&self, component: &Arc<dyn Component>) {
61        if let Ok(mut children) = self.children.lock() {
62            children.retain(|c| !Arc::ptr_eq(c, component));
63        }
64        self.invalidate_cache();
65    }
66
67    /// Clear all children.
68    pub fn clear(&self) {
69        if let Ok(mut children) = self.children.lock() {
70            children.clear();
71        }
72        self.invalidate_cache();
73    }
74
75    /// Set the background function.
76    pub fn set_bg_fn(&self, f: Option<Arc<dyn Fn(&str) -> String + Send + Sync>>) {
77        if let Ok(mut bg_fn) = self.bg_fn.lock() {
78            *bg_fn = f;
79        }
80        // Don't invalidate - we'll detect bg_fn changes by sampling output
81    }
82
83    /// Invalidate the render cache.
84    fn invalidate_cache(&self) {
85        if let Ok(mut cache) = self.cache.lock() {
86            *cache = None;
87        }
88    }
89
90    /// Check if the cache is valid.
91    fn is_cache_valid(
92        &self,
93        width: usize,
94        child_lines: &[String],
95        bg_sample: Option<&str>,
96    ) -> bool {
97        if let Ok(cache) = self.cache.lock() {
98            if let Some(ref cache) = *cache {
99                return cache.width == width
100                    && cache.bg_sample.as_deref() == bg_sample
101                    && cache.child_lines.len() == child_lines.len()
102                    && cache
103                        .child_lines
104                        .iter()
105                        .zip(child_lines.iter())
106                        .all(|(a, b)| a == b);
107            }
108        }
109        false
110    }
111
112    /// Apply background to a line.
113    fn apply_bg(&self, line: &str, width: usize) -> String {
114        if let Ok(bg_fn) = self.bg_fn.lock() {
115            if let Some(ref f) = *bg_fn {
116                return apply_background_to_line(line, width, |s| f(s));
117            }
118        }
119        // Pad with spaces
120        let vis_len = visible_width(line);
121        let pad = " ".repeat(width.saturating_sub(vis_len));
122        format!("{}{}", line, pad)
123    }
124}
125
126impl Component for Box {
127    fn render(&self, width: usize) -> Vec<String> {
128        let children = if let Ok(children) = self.children.lock() {
129            children.clone()
130        } else {
131            return Vec::new();
132        };
133
134        if children.is_empty() {
135            return Vec::new();
136        }
137
138        let content_width = width.saturating_sub(self.padding_x * 2);
139        let left_pad = " ".repeat(self.padding_x);
140
141        // Render all children
142        let mut child_lines: Vec<String> = Vec::new();
143        for child in &children {
144            let lines = child.render(content_width);
145            for line in lines {
146                child_lines.push(format!("{}{}", left_pad, line));
147            }
148        }
149
150        if child_lines.is_empty() {
151            return Vec::new();
152        }
153
154        // Sample bg_fn output for cache validation
155        let bg_sample = if let Ok(bg_fn) = self.bg_fn.lock() {
156            bg_fn.as_ref().map(|f| f("test"))
157        } else {
158            None
159        };
160
161        // Check cache validity
162        if self.is_cache_valid(width, &child_lines, bg_sample.as_deref()) {
163            if let Ok(cache) = self.cache.lock() {
164                if let Some(ref cache) = *cache {
165                    return cache.lines.clone();
166                }
167            }
168        }
169
170        // Apply background and padding
171        let mut result: Vec<String> = Vec::new();
172
173        // Top padding
174        for _ in 0..self.padding_y {
175            result.push(self.apply_bg("", width));
176        }
177
178        // Content
179        for line in &child_lines {
180            result.push(self.apply_bg(line, width));
181        }
182
183        // Bottom padding
184        for _ in 0..self.padding_y {
185            result.push(self.apply_bg("", width));
186        }
187
188        // Update cache
189        if let Ok(mut cache) = self.cache.lock() {
190            *cache = Some(RenderCache {
191                child_lines,
192                width,
193                bg_sample,
194                lines: result.clone(),
195            });
196        }
197
198        result
199    }
200
201    fn invalidate(&self) {
202        self.invalidate_cache();
203        if let Ok(children) = self.children.lock() {
204            for child in &*children {
205                child.invalidate();
206            }
207        }
208    }
209
210    fn as_any(&self) -> &dyn Any {
211        self
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::text::Text;
219
220    #[test]
221    fn test_box_padding() {
222        let box_component = Box::new(2, 1);
223        let text = Arc::new(Text::new("Hello", 1, 0));
224        box_component.add_child(text);
225
226        let lines = box_component.render(10);
227        assert!(!lines.is_empty());
228
229        // Should have top padding, content, and bottom padding
230        assert!(lines.len() >= 3);
231    }
232
233    #[test]
234    fn test_box_empty() {
235        let box_component = Box::new(1, 1);
236        let lines = box_component.render(10);
237        assert!(lines.is_empty());
238    }
239
240    #[test]
241    fn test_box_clear() {
242        let box_component = Box::new(1, 1);
243        let text = Arc::new(Text::new("Hello", 1, 0));
244        box_component.add_child(text);
245        box_component.clear();
246
247        let lines = box_component.render(10);
248        assert!(lines.is_empty());
249    }
250}