rab/tui/components/
box.rs1use crate::tui::Component;
2use crate::tui::util::{apply_background_to_line, visible_width};
3
4pub type BgFn = Box<dyn Fn(&str) -> String>;
6
7pub struct TuiBox {
11 children: Vec<Box<dyn Component>>,
12 padding_x: usize,
13 padding_y: usize,
14 bg_fn: Option<BgFn>,
15 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 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 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 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}