1#![forbid(unsafe_code)]
2
3use crate::Widget;
15use ftui_core::geometry::Rect;
16use ftui_render::frame::Frame;
17
18pub struct Group<'a> {
34 children: Vec<Box<dyn Widget + 'a>>,
35}
36
37impl<'a> Group<'a> {
38 pub fn new() -> Self {
40 Self {
41 children: Vec::new(),
42 }
43 }
44
45 #[must_use]
47 pub fn push<W: Widget + 'a>(mut self, widget: W) -> Self {
48 self.children.push(Box::new(widget));
49 self
50 }
51
52 #[must_use]
54 pub fn push_boxed(mut self, widget: Box<dyn Widget + 'a>) -> Self {
55 self.children.push(widget);
56 self
57 }
58
59 #[inline]
61 pub fn len(&self) -> usize {
62 self.children.len()
63 }
64
65 #[inline]
67 pub fn is_empty(&self) -> bool {
68 self.children.is_empty()
69 }
70}
71
72impl Default for Group<'_> {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl Widget for Group<'_> {
79 fn render(&self, area: Rect, frame: &mut Frame) {
80 if area.is_empty() {
81 return;
82 }
83
84 for child in &self.children {
85 child.render(area, frame);
86 }
87 }
88
89 fn is_essential(&self) -> bool {
90 self.children.iter().any(|c| c.is_essential())
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use ftui_render::cell::Cell;
98 use ftui_render::grapheme_pool::GraphemePool;
99
100 fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
101 let mut lines = Vec::new();
102 for y in 0..buf.height() {
103 let mut row = String::with_capacity(buf.width() as usize);
104 for x in 0..buf.width() {
105 let ch = buf
106 .get(x, y)
107 .and_then(|c| c.content.as_char())
108 .unwrap_or(' ');
109 row.push(ch);
110 }
111 lines.push(row);
112 }
113 lines
114 }
115
116 #[derive(Debug, Clone, Copy)]
117 struct Fill(char);
118
119 impl Widget for Fill {
120 fn render(&self, area: Rect, frame: &mut Frame) {
121 for y in area.y..area.bottom() {
122 for x in area.x..area.right() {
123 frame.buffer.set(x, y, Cell::from_char(self.0));
124 }
125 }
126 }
127 }
128
129 #[derive(Debug, Clone, Copy)]
131 struct Dot {
132 ch: char,
133 dx: u16,
134 dy: u16,
135 }
136
137 impl Widget for Dot {
138 fn render(&self, area: Rect, frame: &mut Frame) {
139 let x = area.x.saturating_add(self.dx);
140 let y = area.y.saturating_add(self.dy);
141 if x < area.right() && y < area.bottom() {
142 frame.buffer.set(x, y, Cell::from_char(self.ch));
143 }
144 }
145 }
146
147 #[test]
148 fn empty_group_is_noop() {
149 let group = Group::new();
150 let area = Rect::new(0, 0, 5, 3);
151 let mut pool = GraphemePool::new();
152 let mut frame = Frame::new(5, 3, &mut pool);
153 group.render(area, &mut frame);
154
155 for y in 0..3 {
156 for x in 0..5u16 {
157 assert!(frame.buffer.get(x, y).unwrap().is_empty());
158 }
159 }
160 }
161
162 #[test]
163 fn single_child_renders() {
164 let group = Group::new().push(Fill('A'));
165 let area = Rect::new(0, 0, 3, 2);
166 let mut pool = GraphemePool::new();
167 let mut frame = Frame::new(3, 2, &mut pool);
168 group.render(area, &mut frame);
169
170 assert_eq!(buf_to_lines(&frame.buffer), vec!["AAA", "AAA"]);
171 }
172
173 #[test]
174 fn later_children_overwrite_earlier() {
175 let group = Group::new().push(Fill('A')).push(Dot {
176 ch: 'X',
177 dx: 1,
178 dy: 0,
179 });
180 let area = Rect::new(0, 0, 3, 1);
181 let mut pool = GraphemePool::new();
182 let mut frame = Frame::new(3, 1, &mut pool);
183 group.render(area, &mut frame);
184
185 assert_eq!(buf_to_lines(&frame.buffer), vec!["AXA"]);
186 }
187
188 #[test]
189 fn deterministic_render_order() {
190 let group = Group::new().push(Fill('A')).push(Fill('B'));
192 let area = Rect::new(0, 0, 3, 1);
193 let mut pool = GraphemePool::new();
194 let mut frame = Frame::new(3, 1, &mut pool);
195 group.render(area, &mut frame);
196
197 assert_eq!(buf_to_lines(&frame.buffer), vec!["BBB"]);
199 }
200
201 #[test]
202 fn multiple_dots_compose() {
203 let group = Group::new()
204 .push(Dot {
205 ch: '1',
206 dx: 0,
207 dy: 0,
208 })
209 .push(Dot {
210 ch: '2',
211 dx: 2,
212 dy: 0,
213 })
214 .push(Dot {
215 ch: '3',
216 dx: 1,
217 dy: 1,
218 });
219 let area = Rect::new(0, 0, 3, 2);
220 let mut pool = GraphemePool::new();
221 let mut frame = Frame::new(3, 2, &mut pool);
222 group.render(area, &mut frame);
223
224 assert_eq!(buf_to_lines(&frame.buffer), vec!["1 2", " 3 "]);
225 }
226
227 #[test]
228 fn zero_area_is_noop() {
229 let group = Group::new().push(Fill('X'));
230 let area = Rect::new(0, 0, 0, 0);
231 let mut pool = GraphemePool::new();
232 let mut frame = Frame::new(5, 5, &mut pool);
233 group.render(area, &mut frame);
234
235 for y in 0..5 {
236 for x in 0..5u16 {
237 assert!(frame.buffer.get(x, y).unwrap().is_empty());
238 }
239 }
240 }
241
242 #[test]
243 fn len_and_is_empty() {
244 let g0 = Group::new();
245 assert!(g0.is_empty());
246 assert_eq!(g0.len(), 0);
247
248 let g1 = Group::new().push(Fill('A'));
249 assert!(!g1.is_empty());
250 assert_eq!(g1.len(), 1);
251
252 let g3 = Group::new().push(Fill('A')).push(Fill('B')).push(Fill('C'));
253 assert_eq!(g3.len(), 3);
254 }
255
256 #[test]
257 fn is_essential_any_child() {
258 struct Essential;
259 impl Widget for Essential {
260 fn render(&self, _: Rect, _: &mut Frame) {}
261 fn is_essential(&self) -> bool {
262 true
263 }
264 }
265
266 assert!(!Group::new().push(Fill('A')).is_essential());
267 assert!(Group::new().push(Essential).is_essential());
268 assert!(Group::new().push(Fill('A')).push(Essential).is_essential());
269 }
270
271 #[test]
272 fn push_boxed_works() {
273 let boxed: Box<dyn Widget> = Box::new(Fill('Z'));
274 let group = Group::new().push_boxed(boxed);
275 assert_eq!(group.len(), 1);
276
277 let area = Rect::new(0, 0, 2, 1);
278 let mut pool = GraphemePool::new();
279 let mut frame = Frame::new(2, 1, &mut pool);
280 group.render(area, &mut frame);
281
282 assert_eq!(buf_to_lines(&frame.buffer), vec!["ZZ"]);
283 }
284
285 #[test]
286 fn nested_groups_compose() {
287 let inner = Group::new().push(Fill('I'));
288 let outer = Group::new().push(Fill('O')).push(inner);
289
290 let area = Rect::new(0, 0, 3, 1);
291 let mut pool = GraphemePool::new();
292 let mut frame = Frame::new(3, 1, &mut pool);
293 outer.render(area, &mut frame);
294
295 assert_eq!(buf_to_lines(&frame.buffer), vec!["III"]);
297 }
298
299 #[test]
300 fn default_group_is_empty() {
301 let g = Group::default();
302 assert!(g.is_empty());
303 assert_eq!(g.len(), 0);
304 }
305
306 #[test]
307 fn large_group_all_render() {
308 let n = 20;
309 let group = (0..n).fold(Group::new(), |g, _| g.push(Fill('X')));
310 assert_eq!(group.len(), n);
311
312 let area = Rect::new(0, 0, 3, 1);
313 let mut pool = GraphemePool::new();
314 let mut frame = Frame::new(3, 1, &mut pool);
315 group.render(area, &mut frame);
316 assert_eq!(buf_to_lines(&frame.buffer), vec!["XXX"]);
317 }
318
319 #[test]
320 fn group_with_offset_area() {
321 let group = Group::new().push(Fill('X'));
322 let area = Rect::new(2, 1, 3, 2);
323 let mut pool = GraphemePool::new();
324 let mut frame = Frame::new(6, 4, &mut pool);
325 group.render(area, &mut frame);
326
327 assert_eq!(
329 buf_to_lines(&frame.buffer),
330 vec![" ", " XXX ", " XXX ", " "]
331 );
332 }
333}