Skip to main content

rab/tui/components/
box.rs

1use crate::tui::Component;
2use crate::tui::Style;
3use crate::tui::util::visible_width;
4
5/// A container with padding and background color function.
6/// Children are rendered inside the padded area.
7/// Port of pi's `packages/tui/src/components/box.ts`.
8pub struct TuiBox {
9    children: Vec<Box<dyn Component>>,
10    padding_x: usize,
11    padding_y: usize,
12    bg_style: Option<Style>,
13    // Render cache
14    cached_child_lines: Vec<String>,
15    cached_width: usize,
16    cached_bg_sample: Option<String>,
17    cached_lines: Vec<String>,
18}
19
20impl TuiBox {
21    pub fn new(padding_x: usize, padding_y: usize, bg_style: Option<Style>) -> Self {
22        Self {
23            children: Vec::new(),
24            padding_x,
25            padding_y,
26            bg_style,
27            cached_child_lines: Vec::new(),
28            cached_width: 0,
29            cached_bg_sample: None,
30            cached_lines: Vec::new(),
31        }
32    }
33
34    pub fn add_child(&mut self, component: Box<dyn Component>) {
35        self.children.push(component);
36        self.invalidate_cache();
37    }
38
39    pub fn set_bg_style(&mut self, bg_style: Option<Style>) {
40        self.bg_style = bg_style;
41        self.invalidate_cache();
42    }
43
44    fn invalidate_cache(&mut self) {
45        self.cached_child_lines.clear();
46        self.cached_lines.clear();
47    }
48
49    fn apply_bg(&self, line: &str, width: usize) -> String {
50        if let Some(ref style) = self.bg_style {
51            let vis = visible_width(line);
52            let padded = if vis < width {
53                format!("{}{}", line, " ".repeat(width - vis))
54            } else {
55                line.to_string()
56            };
57            style.apply(&padded)
58        } else {
59            let vis = visible_width(line);
60            if vis < width {
61                format!("{}{}", line, " ".repeat(width - vis))
62            } else {
63                line.to_string()
64            }
65        }
66    }
67}
68
69impl Component for TuiBox {
70    fn render(&mut self, width: usize) -> Vec<String> {
71        if self.children.is_empty() {
72            return vec![];
73        }
74
75        let content_width = width.saturating_sub(2 * self.padding_x).max(1);
76        let left_pad = " ".repeat(self.padding_x);
77
78        // Render all children at content width
79        let mut child_lines: Vec<String> = Vec::new();
80        for child in self.children.iter_mut() {
81            for line in child.render(content_width) {
82                child_lines.push(format!("{}{}", left_pad, line));
83            }
84        }
85
86        if child_lines.is_empty() {
87            return vec![];
88        }
89
90        // Check cache: compare child lines, width, and bg sample
91        let bg_sample = self.bg_style.as_ref().map(|s| s.apply("test"));
92        if self.cached_child_lines == child_lines
93            && self.cached_width == width
94            && self.cached_bg_sample == bg_sample
95            && !self.cached_lines.is_empty()
96        {
97            return self.cached_lines.clone();
98        }
99
100        let mut result: Vec<String> = Vec::new();
101        for _ in 0..self.padding_y {
102            result.push(self.apply_bg("", width));
103        }
104        for line in &child_lines {
105            result.push(self.apply_bg(line, width));
106        }
107        for _ in 0..self.padding_y {
108            result.push(self.apply_bg("", width));
109        }
110
111        // Update cache
112        self.cached_child_lines = child_lines;
113        self.cached_width = width;
114        self.cached_bg_sample = bg_sample;
115        self.cached_lines = result.clone();
116
117        result
118    }
119
120    fn invalidate(&mut self) {
121        self.invalidate_cache();
122        for child in &mut self.children {
123            child.invalidate();
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::tui::components::Text;
132
133    #[test]
134    fn test_box_render() {
135        let mut b = TuiBox::new(1, 1, None);
136        b.add_child(Box::new(Text::new("hello", 0, 0, None)));
137        let lines = b.render(20);
138        assert!(lines.len() >= 3);
139    }
140}