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 #[must_use]
85 pub fn rows(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
86 self.row_constraints = constraints.into_iter().collect();
87 self
88 }
89
90 #[must_use]
92 pub fn columns(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
93 self.col_constraints = constraints.into_iter().collect();
94 self
95 }
96
97 #[must_use]
99 pub fn row_gap(mut self, gap: u16) -> Self {
100 self.row_gap = gap;
101 self
102 }
103
104 #[must_use]
106 pub fn col_gap(mut self, gap: u16) -> Self {
107 self.col_gap = gap;
108 self
109 }
110
111 #[must_use]
113 pub fn gap(mut self, gap: u16) -> Self {
114 self.row_gap = gap;
115 self.col_gap = gap;
116 self
117 }
118
119 #[must_use]
121 pub fn child(
122 mut self,
123 widget: impl Widget + 'a,
124 row: usize,
125 col: usize,
126 rowspan: usize,
127 colspan: usize,
128 ) -> Self {
129 self.children.push(LayoutChild {
130 widget: Box::new(widget),
131 row,
132 col,
133 rowspan: rowspan.max(1),
134 colspan: colspan.max(1),
135 });
136 self
137 }
138
139 #[must_use]
141 pub fn cell(self, widget: impl Widget + 'a, row: usize, col: usize) -> Self {
142 self.child(widget, row, col, 1, 1)
143 }
144
145 #[inline]
147 pub fn len(&self) -> usize {
148 self.children.len()
149 }
150
151 #[inline]
153 pub fn is_empty(&self) -> bool {
154 self.children.is_empty()
155 }
156}
157
158impl Widget for Layout<'_> {
159 fn render(&self, area: Rect, frame: &mut Frame) {
160 if area.is_empty() || self.children.is_empty() {
161 return;
162 }
163
164 let grid = Grid::new()
165 .rows(self.row_constraints.iter().copied())
166 .columns(self.col_constraints.iter().copied())
167 .row_gap(self.row_gap)
168 .col_gap(self.col_gap);
169
170 let grid_layout = grid.split(area);
171
172 for child in &self.children {
173 let rect = grid_layout.span(child.row, child.col, child.rowspan, child.colspan);
174 if !rect.is_empty() {
175 child.widget.render(rect, frame);
176 }
177 }
178 }
179
180 fn is_essential(&self) -> bool {
181 self.children.iter().any(|c| c.widget.is_essential())
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use ftui_render::cell::Cell;
189 use ftui_render::grapheme_pool::GraphemePool;
190 use std::cell::RefCell;
191 use std::rc::Rc;
192
193 fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
194 let mut lines = Vec::new();
195 for y in 0..buf.height() {
196 let mut row = String::with_capacity(buf.width() as usize);
197 for x in 0..buf.width() {
198 let ch = buf
199 .get(x, y)
200 .and_then(|c| c.content.as_char())
201 .unwrap_or(' ');
202 row.push(ch);
203 }
204 lines.push(row);
205 }
206 lines
207 }
208
209 #[derive(Debug, Clone, Copy)]
210 struct Fill(char);
211
212 impl Widget for Fill {
213 fn render(&self, area: Rect, frame: &mut Frame) {
214 for y in area.y..area.bottom() {
215 for x in area.x..area.right() {
216 frame.buffer.set(x, y, Cell::from_char(self.0));
217 }
218 }
219 }
220 }
221
222 #[derive(Clone, Debug)]
224 struct Recorder {
225 rects: Rc<RefCell<Vec<Rect>>>,
226 }
227
228 impl Recorder {
229 fn new() -> (Self, Rc<RefCell<Vec<Rect>>>) {
230 let rects = Rc::new(RefCell::new(Vec::new()));
231 (
232 Self {
233 rects: rects.clone(),
234 },
235 rects,
236 )
237 }
238 }
239
240 impl Widget for Recorder {
241 fn render(&self, area: Rect, _frame: &mut Frame) {
242 self.rects.borrow_mut().push(area);
243 }
244 }
245
246 #[test]
247 fn empty_layout_is_noop() {
248 let layout = Layout::new();
249 let mut pool = GraphemePool::new();
250 let mut frame = Frame::new(10, 10, &mut pool);
251 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
252
253 for y in 0..10 {
254 for x in 0..10u16 {
255 assert!(frame.buffer.get(x, y).unwrap().is_empty());
256 }
257 }
258 }
259
260 #[test]
261 fn single_cell_layout() {
262 let layout = Layout::new()
263 .rows([Constraint::Min(0)])
264 .columns([Constraint::Min(0)])
265 .cell(Fill('X'), 0, 0);
266
267 let mut pool = GraphemePool::new();
268 let mut frame = Frame::new(5, 3, &mut pool);
269 layout.render(Rect::new(0, 0, 5, 3), &mut frame);
270
271 assert_eq!(buf_to_lines(&frame.buffer), vec!["XXXXX", "XXXXX", "XXXXX"]);
272 }
273
274 #[test]
275 fn two_by_two_grid() {
276 let layout = Layout::new()
277 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
278 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
279 .cell(Fill('A'), 0, 0)
280 .cell(Fill('B'), 0, 1)
281 .cell(Fill('C'), 1, 0)
282 .cell(Fill('D'), 1, 1);
283
284 let mut pool = GraphemePool::new();
285 let mut frame = Frame::new(6, 2, &mut pool);
286 layout.render(Rect::new(0, 0, 6, 2), &mut frame);
287
288 assert_eq!(buf_to_lines(&frame.buffer), vec!["AAABBB", "CCCDDD"]);
289 }
290
291 #[test]
292 fn column_spanning() {
293 let layout = Layout::new()
294 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
295 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
296 .child(Fill('H'), 0, 0, 1, 2) .cell(Fill('L'), 1, 0)
298 .cell(Fill('R'), 1, 1);
299
300 let mut pool = GraphemePool::new();
301 let mut frame = Frame::new(6, 2, &mut pool);
302 layout.render(Rect::new(0, 0, 6, 2), &mut frame);
303
304 assert_eq!(buf_to_lines(&frame.buffer), vec!["HHHHHH", "LLLRRR"]);
305 }
306
307 #[test]
308 fn row_spanning() {
309 let layout = Layout::new()
310 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
311 .columns([Constraint::Fixed(2), Constraint::Fixed(2)])
312 .child(Fill('S'), 0, 0, 2, 1) .cell(Fill('A'), 0, 1)
314 .cell(Fill('B'), 1, 1);
315
316 let mut pool = GraphemePool::new();
317 let mut frame = Frame::new(4, 2, &mut pool);
318 layout.render(Rect::new(0, 0, 4, 2), &mut frame);
319
320 assert_eq!(buf_to_lines(&frame.buffer), vec!["SSAA", "SSBB"]);
321 }
322
323 #[test]
324 fn layout_with_gap() {
325 let (a, a_rects) = Recorder::new();
326 let (b, b_rects) = Recorder::new();
327
328 let layout = Layout::new()
329 .rows([Constraint::Fixed(1)])
330 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
331 .col_gap(2)
332 .cell(a, 0, 0)
333 .cell(b, 0, 1);
334
335 let mut pool = GraphemePool::new();
336 let mut frame = Frame::new(10, 1, &mut pool);
337 layout.render(Rect::new(0, 0, 10, 1), &mut frame);
338
339 let a_rect = a_rects.borrow()[0];
340 let b_rect = b_rects.borrow()[0];
341
342 assert_eq!(a_rect.width, 3);
343 assert_eq!(b_rect.width, 3);
344 assert!(b_rect.x >= a_rect.right());
346 }
347
348 #[test]
349 fn fixed_and_flexible_rows() {
350 let (header, header_rects) = Recorder::new();
351 let (content, content_rects) = Recorder::new();
352 let (footer, footer_rects) = Recorder::new();
353
354 let layout = Layout::new()
355 .rows([
356 Constraint::Fixed(1),
357 Constraint::Min(0),
358 Constraint::Fixed(1),
359 ])
360 .columns([Constraint::Min(0)])
361 .cell(header, 0, 0)
362 .cell(content, 1, 0)
363 .cell(footer, 2, 0);
364
365 let mut pool = GraphemePool::new();
366 let mut frame = Frame::new(20, 10, &mut pool);
367 layout.render(Rect::new(0, 0, 20, 10), &mut frame);
368
369 let h = header_rects.borrow()[0];
370 let c = content_rects.borrow()[0];
371 let f = footer_rects.borrow()[0];
372
373 assert_eq!(h.height, 1);
374 assert_eq!(f.height, 1);
375 assert_eq!(c.height, 8); assert_eq!(h.y, 0);
377 assert_eq!(f.y, 9);
378 }
379
380 #[test]
381 fn zero_area_is_noop() {
382 let (rec, rects) = Recorder::new();
383 let layout = Layout::new()
384 .rows([Constraint::Min(0)])
385 .columns([Constraint::Min(0)])
386 .cell(rec, 0, 0);
387
388 let mut pool = GraphemePool::new();
389 let mut frame = Frame::new(5, 5, &mut pool);
390 layout.render(Rect::new(0, 0, 0, 0), &mut frame);
391
392 assert!(rects.borrow().is_empty());
393 }
394
395 #[test]
396 fn len_and_is_empty() {
397 assert!(Layout::new().is_empty());
398 assert_eq!(Layout::new().len(), 0);
399
400 let layout = Layout::new()
401 .rows([Constraint::Min(0)])
402 .columns([Constraint::Min(0)])
403 .cell(Fill('X'), 0, 0);
404 assert!(!layout.is_empty());
405 assert_eq!(layout.len(), 1);
406 }
407
408 #[test]
409 fn is_essential_delegates() {
410 struct Essential;
411 impl Widget for Essential {
412 fn render(&self, _: Rect, _: &mut Frame) {}
413 fn is_essential(&self) -> bool {
414 true
415 }
416 }
417
418 let not_essential = Layout::new()
419 .rows([Constraint::Min(0)])
420 .columns([Constraint::Min(0)])
421 .cell(Fill('X'), 0, 0);
422 assert!(!not_essential.is_essential());
423
424 let essential = Layout::new()
425 .rows([Constraint::Min(0)])
426 .columns([Constraint::Min(0)])
427 .cell(Essential, 0, 0);
428 assert!(essential.is_essential());
429 }
430
431 #[test]
432 fn deterministic_render_order() {
433 let layout = Layout::new()
435 .rows([Constraint::Fixed(1)])
436 .columns([Constraint::Fixed(3)])
437 .cell(Fill('A'), 0, 0)
438 .cell(Fill('B'), 0, 0); let mut pool = GraphemePool::new();
441 let mut frame = Frame::new(3, 1, &mut pool);
442 layout.render(Rect::new(0, 0, 3, 1), &mut frame);
443
444 assert_eq!(buf_to_lines(&frame.buffer), vec!["BBB"]);
445 }
446
447 #[test]
448 fn layout_with_offset_area() {
449 let (rec, rects) = Recorder::new();
450 let layout = Layout::new()
451 .rows([Constraint::Fixed(2)])
452 .columns([Constraint::Fixed(3)])
453 .cell(rec, 0, 0);
454
455 let mut pool = GraphemePool::new();
456 let mut frame = Frame::new(10, 10, &mut pool);
457 layout.render(Rect::new(3, 4, 5, 5), &mut frame);
458
459 let r = rects.borrow()[0];
460 assert_eq!(r.x, 3);
461 assert_eq!(r.y, 4);
462 assert_eq!(r.width, 3);
463 assert_eq!(r.height, 2);
464 }
465
466 #[test]
467 fn three_by_three_grid() {
468 let layout = Layout::new()
469 .rows([
470 Constraint::Fixed(1),
471 Constraint::Fixed(1),
472 Constraint::Fixed(1),
473 ])
474 .columns([
475 Constraint::Fixed(2),
476 Constraint::Fixed(2),
477 Constraint::Fixed(2),
478 ])
479 .cell(Fill('1'), 0, 0)
480 .cell(Fill('2'), 0, 1)
481 .cell(Fill('3'), 0, 2)
482 .cell(Fill('4'), 1, 0)
483 .cell(Fill('5'), 1, 1)
484 .cell(Fill('6'), 1, 2)
485 .cell(Fill('7'), 2, 0)
486 .cell(Fill('8'), 2, 1)
487 .cell(Fill('9'), 2, 2);
488
489 let mut pool = GraphemePool::new();
490 let mut frame = Frame::new(6, 3, &mut pool);
491 layout.render(Rect::new(0, 0, 6, 3), &mut frame);
492
493 assert_eq!(
494 buf_to_lines(&frame.buffer),
495 vec!["112233", "445566", "778899"]
496 );
497 }
498
499 #[test]
500 fn layout_default_equals_new() {
501 let def: Layout<'_> = Layout::default();
502 assert!(def.is_empty());
503 assert_eq!(def.len(), 0);
504 }
505
506 #[test]
507 fn gap_sets_both_row_and_col() {
508 let (a, a_rects) = Recorder::new();
509 let (b, b_rects) = Recorder::new();
510
511 let layout = Layout::new()
512 .rows([Constraint::Fixed(2), Constraint::Fixed(2)])
513 .columns([Constraint::Fixed(3)])
514 .gap(1)
515 .cell(a, 0, 0)
516 .cell(b, 1, 0);
517
518 let mut pool = GraphemePool::new();
519 let mut frame = Frame::new(10, 10, &mut pool);
520 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
521
522 let a_rect = a_rects.borrow()[0];
523 let b_rect = b_rects.borrow()[0];
524 assert!(b_rect.y >= a_rect.bottom());
526 }
527
528 #[test]
529 fn child_clamps_zero_span_to_one() {
530 let (rec, rects) = Recorder::new();
531 let layout = Layout::new()
532 .rows([Constraint::Fixed(3)])
533 .columns([Constraint::Fixed(4)])
534 .child(rec, 0, 0, 0, 0); let mut pool = GraphemePool::new();
537 let mut frame = Frame::new(10, 10, &mut pool);
538 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
539
540 let r = rects.borrow()[0];
541 assert!(r.width > 0 && r.height > 0);
542 }
543
544 #[test]
547 fn render_in_1x1_area() {
548 let (rec, rects) = Recorder::new();
549 let layout = Layout::new()
550 .rows([Constraint::Min(0)])
551 .columns([Constraint::Min(0)])
552 .cell(rec, 0, 0);
553
554 let mut pool = GraphemePool::new();
555 let mut frame = Frame::new(10, 10, &mut pool);
556 layout.render(Rect::new(3, 3, 1, 1), &mut frame);
557
558 let r = rects.borrow()[0];
559 assert_eq!(r, Rect::new(3, 3, 1, 1));
560 }
561
562 #[test]
563 fn no_constraints_with_children() {
564 let (rec, rects) = Recorder::new();
565 let layout = Layout::new().cell(rec, 0, 0);
567
568 let mut pool = GraphemePool::new();
569 let mut frame = Frame::new(10, 10, &mut pool);
570 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
571
572 let _ = rects.borrow().len();
575 }
576
577 #[test]
578 fn fixed_constraints_exceed_area() {
579 let (a, a_rects) = Recorder::new();
580 let (b, b_rects) = Recorder::new();
581 let layout = Layout::new()
583 .rows([Constraint::Fixed(1)])
584 .columns([Constraint::Fixed(10), Constraint::Fixed(10)])
585 .cell(a, 0, 0)
586 .cell(b, 0, 1);
587
588 let mut pool = GraphemePool::new();
589 let mut frame = Frame::new(20, 5, &mut pool);
590 layout.render(Rect::new(0, 0, 8, 1), &mut frame);
591
592 let a_r = a_rects.borrow();
594 let b_r = b_rects.borrow();
595 assert!(!a_r.is_empty());
596 assert!(a_r[0].width > 0 || !b_r.is_empty());
598 }
599
600 #[test]
601 fn gap_larger_than_area() {
602 let (rec, rects) = Recorder::new();
603 let layout = Layout::new()
604 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
605 .columns([Constraint::Min(0)])
606 .row_gap(100) .cell(rec, 0, 0);
608
609 let mut pool = GraphemePool::new();
610 let mut frame = Frame::new(10, 5, &mut pool);
611 layout.render(Rect::new(0, 0, 10, 5), &mut frame);
612
613 let _ = rects.borrow().len();
615 }
616
617 #[test]
618 fn is_essential_mixed_children() {
619 struct Essential;
620 impl Widget for Essential {
621 fn render(&self, _: Rect, _: &mut Frame) {}
622 fn is_essential(&self) -> bool {
623 true
624 }
625 }
626
627 let layout = Layout::new()
629 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
630 .columns([Constraint::Min(0)])
631 .cell(Fill('X'), 0, 0)
632 .cell(Essential, 1, 0);
633 assert!(layout.is_essential());
634 }
635
636 #[test]
637 fn is_essential_all_non_essential() {
638 let layout = Layout::new()
639 .rows([Constraint::Fixed(1)])
640 .columns([Constraint::Min(0)])
641 .cell(Fill('X'), 0, 0)
642 .cell(Fill('Y'), 0, 0);
643 assert!(!layout.is_essential());
644 }
645
646 #[test]
647 fn multiple_flexible_rows_share_space() {
648 let (a, a_rects) = Recorder::new();
649 let (b, b_rects) = Recorder::new();
650
651 let layout = Layout::new()
652 .rows([Constraint::Min(0), Constraint::Min(0)])
653 .columns([Constraint::Min(0)])
654 .cell(a, 0, 0)
655 .cell(b, 1, 0);
656
657 let mut pool = GraphemePool::new();
658 let mut frame = Frame::new(10, 10, &mut pool);
659 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
660
661 let a_h = a_rects.borrow()[0].height;
662 let b_h = b_rects.borrow()[0].height;
663 assert_eq!(a_h + b_h, 10);
664 assert!(a_h > 0 && b_h > 0);
665 }
666
667 #[test]
668 fn col_gap_with_single_column() {
669 let (rec, rects) = Recorder::new();
670 let layout = Layout::new()
672 .rows([Constraint::Min(0)])
673 .columns([Constraint::Min(0)])
674 .col_gap(5)
675 .cell(rec, 0, 0);
676
677 let mut pool = GraphemePool::new();
678 let mut frame = Frame::new(10, 5, &mut pool);
679 layout.render(Rect::new(0, 0, 10, 5), &mut frame);
680
681 let r = rects.borrow()[0];
682 assert_eq!(r.width, 10, "single column should get full width");
683 }
684
685 #[test]
686 fn row_gap_with_single_row() {
687 let (rec, rects) = Recorder::new();
688 let layout = Layout::new()
689 .rows([Constraint::Min(0)])
690 .columns([Constraint::Min(0)])
691 .row_gap(5)
692 .cell(rec, 0, 0);
693
694 let mut pool = GraphemePool::new();
695 let mut frame = Frame::new(10, 5, &mut pool);
696 layout.render(Rect::new(0, 0, 10, 5), &mut frame);
697
698 let r = rects.borrow()[0];
699 assert_eq!(r.height, 5, "single row should get full height");
700 }
701
702 #[test]
703 fn layout_debug_no_children() {
704 let layout = Layout::new()
705 .rows([Constraint::Fixed(1)])
706 .columns([Constraint::Fixed(2)]);
707 let dbg = format!("{layout:?}");
708 assert!(dbg.contains("Layout"));
709 assert!(dbg.contains("children"));
710 }
711
712 #[test]
713 fn child_beyond_grid_bounds() {
714 let (rec, rects) = Recorder::new();
715 let layout = Layout::new()
717 .rows([Constraint::Fixed(3)])
718 .columns([Constraint::Fixed(3)])
719 .cell(rec, 5, 5);
720
721 let mut pool = GraphemePool::new();
722 let mut frame = Frame::new(10, 10, &mut pool);
723 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
724
725 let borrowed = rects.borrow();
728 if !borrowed.is_empty() {
729 let r = borrowed[0];
731 let _ = r;
733 }
734 }
735
736 #[test]
737 fn many_children_same_cell_last_wins() {
738 let layout = Layout::new()
739 .rows([Constraint::Fixed(1)])
740 .columns([Constraint::Fixed(3)])
741 .cell(Fill('A'), 0, 0)
742 .cell(Fill('B'), 0, 0)
743 .cell(Fill('C'), 0, 0);
744
745 let mut pool = GraphemePool::new();
746 let mut frame = Frame::new(3, 1, &mut pool);
747 layout.render(Rect::new(0, 0, 3, 1), &mut frame);
748
749 assert_eq!(buf_to_lines(&frame.buffer), vec!["CCC"]);
750 }
751
752 #[test]
755 fn layout_child_debug() {
756 let layout = Layout::new()
757 .rows([Constraint::Fixed(1)])
758 .columns([Constraint::Fixed(1)])
759 .child(Fill('X'), 2, 3, 4, 5);
760
761 let dbg = format!("{:?}", layout);
762 assert!(dbg.contains("Layout"));
763 assert!(dbg.contains("LayoutChild"));
764 }
765}