rab/tui/components/
box.rs1use crate::tui::Component;
2use crate::tui::Style;
3use crate::tui::util::visible_width;
4
5pub struct TuiBox {
9 children: Vec<Box<dyn Component>>,
10 padding_x: usize,
11 padding_y: usize,
12 bg_style: Option<Style>,
13 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 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 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 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}