Skip to main content

rab/tui/components/
box.rs

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