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