1#![forbid(unsafe_code)]
2
3use crate::Widget;
28use ftui_core::geometry::Rect;
29use ftui_layout::{Constraint, Grid};
30use ftui_render::frame::Frame;
31
32pub struct LayoutChild<'a> {
34 widget: Box<dyn Widget + 'a>,
35 row: usize,
36 col: usize,
37 rowspan: usize,
38 colspan: usize,
39}
40
41impl std::fmt::Debug for LayoutChild<'_> {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("LayoutChild")
44 .field("row", &self.row)
45 .field("col", &self.col)
46 .field("rowspan", &self.rowspan)
47 .field("colspan", &self.colspan)
48 .finish()
49 }
50}
51
52#[derive(Debug)]
57pub struct Layout<'a> {
58 children: Vec<LayoutChild<'a>>,
59 row_constraints: Vec<Constraint>,
60 col_constraints: Vec<Constraint>,
61 row_gap: u16,
62 col_gap: u16,
63}
64
65impl Default for Layout<'_> {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl<'a> Layout<'a> {
72 pub fn new() -> Self {
74 Self {
75 children: Vec::new(),
76 row_constraints: Vec::new(),
77 col_constraints: Vec::new(),
78 row_gap: 0,
79 col_gap: 0,
80 }
81 }
82
83 pub fn rows(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
85 self.row_constraints = constraints.into_iter().collect();
86 self
87 }
88
89 pub fn columns(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
91 self.col_constraints = constraints.into_iter().collect();
92 self
93 }
94
95 pub fn row_gap(mut self, gap: u16) -> Self {
97 self.row_gap = gap;
98 self
99 }
100
101 pub fn col_gap(mut self, gap: u16) -> Self {
103 self.col_gap = gap;
104 self
105 }
106
107 pub fn gap(mut self, gap: u16) -> Self {
109 self.row_gap = gap;
110 self.col_gap = gap;
111 self
112 }
113
114 pub fn child(
116 mut self,
117 widget: impl Widget + 'a,
118 row: usize,
119 col: usize,
120 rowspan: usize,
121 colspan: usize,
122 ) -> Self {
123 self.children.push(LayoutChild {
124 widget: Box::new(widget),
125 row,
126 col,
127 rowspan: rowspan.max(1),
128 colspan: colspan.max(1),
129 });
130 self
131 }
132
133 pub fn cell(self, widget: impl Widget + 'a, row: usize, col: usize) -> Self {
135 self.child(widget, row, col, 1, 1)
136 }
137
138 pub fn len(&self) -> usize {
140 self.children.len()
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.children.is_empty()
146 }
147}
148
149impl Widget for Layout<'_> {
150 fn render(&self, area: Rect, frame: &mut Frame) {
151 if area.is_empty() || self.children.is_empty() {
152 return;
153 }
154
155 let grid = Grid::new()
156 .rows(self.row_constraints.iter().copied())
157 .columns(self.col_constraints.iter().copied())
158 .row_gap(self.row_gap)
159 .col_gap(self.col_gap);
160
161 let grid_layout = grid.split(area);
162
163 for child in &self.children {
164 let rect = grid_layout.span(child.row, child.col, child.rowspan, child.colspan);
165 if !rect.is_empty() {
166 child.widget.render(rect, frame);
167 }
168 }
169 }
170
171 fn is_essential(&self) -> bool {
172 self.children.iter().any(|c| c.widget.is_essential())
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use ftui_render::cell::Cell;
180 use ftui_render::grapheme_pool::GraphemePool;
181 use std::cell::RefCell;
182 use std::rc::Rc;
183
184 fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
185 let mut lines = Vec::new();
186 for y in 0..buf.height() {
187 let mut row = String::with_capacity(buf.width() as usize);
188 for x in 0..buf.width() {
189 let ch = buf
190 .get(x, y)
191 .and_then(|c| c.content.as_char())
192 .unwrap_or(' ');
193 row.push(ch);
194 }
195 lines.push(row);
196 }
197 lines
198 }
199
200 #[derive(Debug, Clone, Copy)]
201 struct Fill(char);
202
203 impl Widget for Fill {
204 fn render(&self, area: Rect, frame: &mut Frame) {
205 for y in area.y..area.bottom() {
206 for x in area.x..area.right() {
207 frame.buffer.set(x, y, Cell::from_char(self.0));
208 }
209 }
210 }
211 }
212
213 #[derive(Clone, Debug)]
215 struct Recorder {
216 rects: Rc<RefCell<Vec<Rect>>>,
217 }
218
219 impl Recorder {
220 fn new() -> (Self, Rc<RefCell<Vec<Rect>>>) {
221 let rects = Rc::new(RefCell::new(Vec::new()));
222 (
223 Self {
224 rects: rects.clone(),
225 },
226 rects,
227 )
228 }
229 }
230
231 impl Widget for Recorder {
232 fn render(&self, area: Rect, _frame: &mut Frame) {
233 self.rects.borrow_mut().push(area);
234 }
235 }
236
237 #[test]
238 fn empty_layout_is_noop() {
239 let layout = Layout::new();
240 let mut pool = GraphemePool::new();
241 let mut frame = Frame::new(10, 10, &mut pool);
242 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
243
244 for y in 0..10 {
245 for x in 0..10u16 {
246 assert!(frame.buffer.get(x, y).unwrap().is_empty());
247 }
248 }
249 }
250
251 #[test]
252 fn single_cell_layout() {
253 let layout = Layout::new()
254 .rows([Constraint::Min(0)])
255 .columns([Constraint::Min(0)])
256 .cell(Fill('X'), 0, 0);
257
258 let mut pool = GraphemePool::new();
259 let mut frame = Frame::new(5, 3, &mut pool);
260 layout.render(Rect::new(0, 0, 5, 3), &mut frame);
261
262 assert_eq!(buf_to_lines(&frame.buffer), vec!["XXXXX", "XXXXX", "XXXXX"]);
263 }
264
265 #[test]
266 fn two_by_two_grid() {
267 let layout = Layout::new()
268 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
269 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
270 .cell(Fill('A'), 0, 0)
271 .cell(Fill('B'), 0, 1)
272 .cell(Fill('C'), 1, 0)
273 .cell(Fill('D'), 1, 1);
274
275 let mut pool = GraphemePool::new();
276 let mut frame = Frame::new(6, 2, &mut pool);
277 layout.render(Rect::new(0, 0, 6, 2), &mut frame);
278
279 assert_eq!(buf_to_lines(&frame.buffer), vec!["AAABBB", "CCCDDD"]);
280 }
281
282 #[test]
283 fn column_spanning() {
284 let layout = Layout::new()
285 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
286 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
287 .child(Fill('H'), 0, 0, 1, 2) .cell(Fill('L'), 1, 0)
289 .cell(Fill('R'), 1, 1);
290
291 let mut pool = GraphemePool::new();
292 let mut frame = Frame::new(6, 2, &mut pool);
293 layout.render(Rect::new(0, 0, 6, 2), &mut frame);
294
295 assert_eq!(buf_to_lines(&frame.buffer), vec!["HHHHHH", "LLLRRR"]);
296 }
297
298 #[test]
299 fn row_spanning() {
300 let layout = Layout::new()
301 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
302 .columns([Constraint::Fixed(2), Constraint::Fixed(2)])
303 .child(Fill('S'), 0, 0, 2, 1) .cell(Fill('A'), 0, 1)
305 .cell(Fill('B'), 1, 1);
306
307 let mut pool = GraphemePool::new();
308 let mut frame = Frame::new(4, 2, &mut pool);
309 layout.render(Rect::new(0, 0, 4, 2), &mut frame);
310
311 assert_eq!(buf_to_lines(&frame.buffer), vec!["SSAA", "SSBB"]);
312 }
313
314 #[test]
315 fn layout_with_gap() {
316 let (a, a_rects) = Recorder::new();
317 let (b, b_rects) = Recorder::new();
318
319 let layout = Layout::new()
320 .rows([Constraint::Fixed(1)])
321 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
322 .col_gap(2)
323 .cell(a, 0, 0)
324 .cell(b, 0, 1);
325
326 let mut pool = GraphemePool::new();
327 let mut frame = Frame::new(10, 1, &mut pool);
328 layout.render(Rect::new(0, 0, 10, 1), &mut frame);
329
330 let a_rect = a_rects.borrow()[0];
331 let b_rect = b_rects.borrow()[0];
332
333 assert_eq!(a_rect.width, 3);
334 assert_eq!(b_rect.width, 3);
335 assert!(b_rect.x >= a_rect.right());
337 }
338
339 #[test]
340 fn fixed_and_flexible_rows() {
341 let (header, header_rects) = Recorder::new();
342 let (content, content_rects) = Recorder::new();
343 let (footer, footer_rects) = Recorder::new();
344
345 let layout = Layout::new()
346 .rows([
347 Constraint::Fixed(1),
348 Constraint::Min(0),
349 Constraint::Fixed(1),
350 ])
351 .columns([Constraint::Min(0)])
352 .cell(header, 0, 0)
353 .cell(content, 1, 0)
354 .cell(footer, 2, 0);
355
356 let mut pool = GraphemePool::new();
357 let mut frame = Frame::new(20, 10, &mut pool);
358 layout.render(Rect::new(0, 0, 20, 10), &mut frame);
359
360 let h = header_rects.borrow()[0];
361 let c = content_rects.borrow()[0];
362 let f = footer_rects.borrow()[0];
363
364 assert_eq!(h.height, 1);
365 assert_eq!(f.height, 1);
366 assert_eq!(c.height, 8); assert_eq!(h.y, 0);
368 assert_eq!(f.y, 9);
369 }
370
371 #[test]
372 fn zero_area_is_noop() {
373 let (rec, rects) = Recorder::new();
374 let layout = Layout::new()
375 .rows([Constraint::Min(0)])
376 .columns([Constraint::Min(0)])
377 .cell(rec, 0, 0);
378
379 let mut pool = GraphemePool::new();
380 let mut frame = Frame::new(5, 5, &mut pool);
381 layout.render(Rect::new(0, 0, 0, 0), &mut frame);
382
383 assert!(rects.borrow().is_empty());
384 }
385
386 #[test]
387 fn len_and_is_empty() {
388 assert!(Layout::new().is_empty());
389 assert_eq!(Layout::new().len(), 0);
390
391 let layout = Layout::new()
392 .rows([Constraint::Min(0)])
393 .columns([Constraint::Min(0)])
394 .cell(Fill('X'), 0, 0);
395 assert!(!layout.is_empty());
396 assert_eq!(layout.len(), 1);
397 }
398
399 #[test]
400 fn is_essential_delegates() {
401 struct Essential;
402 impl Widget for Essential {
403 fn render(&self, _: Rect, _: &mut Frame) {}
404 fn is_essential(&self) -> bool {
405 true
406 }
407 }
408
409 let not_essential = Layout::new()
410 .rows([Constraint::Min(0)])
411 .columns([Constraint::Min(0)])
412 .cell(Fill('X'), 0, 0);
413 assert!(!not_essential.is_essential());
414
415 let essential = Layout::new()
416 .rows([Constraint::Min(0)])
417 .columns([Constraint::Min(0)])
418 .cell(Essential, 0, 0);
419 assert!(essential.is_essential());
420 }
421
422 #[test]
423 fn deterministic_render_order() {
424 let layout = Layout::new()
426 .rows([Constraint::Fixed(1)])
427 .columns([Constraint::Fixed(3)])
428 .cell(Fill('A'), 0, 0)
429 .cell(Fill('B'), 0, 0); let mut pool = GraphemePool::new();
432 let mut frame = Frame::new(3, 1, &mut pool);
433 layout.render(Rect::new(0, 0, 3, 1), &mut frame);
434
435 assert_eq!(buf_to_lines(&frame.buffer), vec!["BBB"]);
436 }
437
438 #[test]
439 fn layout_with_offset_area() {
440 let (rec, rects) = Recorder::new();
441 let layout = Layout::new()
442 .rows([Constraint::Fixed(2)])
443 .columns([Constraint::Fixed(3)])
444 .cell(rec, 0, 0);
445
446 let mut pool = GraphemePool::new();
447 let mut frame = Frame::new(10, 10, &mut pool);
448 layout.render(Rect::new(3, 4, 5, 5), &mut frame);
449
450 let r = rects.borrow()[0];
451 assert_eq!(r.x, 3);
452 assert_eq!(r.y, 4);
453 assert_eq!(r.width, 3);
454 assert_eq!(r.height, 2);
455 }
456
457 #[test]
458 fn three_by_three_grid() {
459 let layout = Layout::new()
460 .rows([
461 Constraint::Fixed(1),
462 Constraint::Fixed(1),
463 Constraint::Fixed(1),
464 ])
465 .columns([
466 Constraint::Fixed(2),
467 Constraint::Fixed(2),
468 Constraint::Fixed(2),
469 ])
470 .cell(Fill('1'), 0, 0)
471 .cell(Fill('2'), 0, 1)
472 .cell(Fill('3'), 0, 2)
473 .cell(Fill('4'), 1, 0)
474 .cell(Fill('5'), 1, 1)
475 .cell(Fill('6'), 1, 2)
476 .cell(Fill('7'), 2, 0)
477 .cell(Fill('8'), 2, 1)
478 .cell(Fill('9'), 2, 2);
479
480 let mut pool = GraphemePool::new();
481 let mut frame = Frame::new(6, 3, &mut pool);
482 layout.render(Rect::new(0, 0, 6, 3), &mut frame);
483
484 assert_eq!(
485 buf_to_lines(&frame.buffer),
486 vec!["112233", "445566", "778899"]
487 );
488 }
489}