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() {
161 return;
162 }
163
164 for y in area.y..area.bottom() {
168 for x in area.x..area.right() {
169 if let Some(cell) = frame.buffer.get_mut(x, y) {
170 cell.content = ftui_render::cell::CellContent::EMPTY;
171 }
172 }
173 }
174
175 if self.children.is_empty() {
176 return;
177 }
178
179 let grid = Grid::new()
180 .rows(self.row_constraints.iter().copied())
181 .columns(self.col_constraints.iter().copied())
182 .row_gap(self.row_gap)
183 .col_gap(self.col_gap);
184
185 let grid_layout = grid.split(area);
186
187 for child in &self.children {
188 let rect = grid_layout.span(child.row, child.col, child.rowspan, child.colspan);
189 if !rect.is_empty() {
190 child.widget.render(rect, frame);
191 }
192 }
193 }
194
195 fn is_essential(&self) -> bool {
196 self.children.iter().any(|c| c.widget.is_essential())
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use ftui_render::cell::Cell;
204 use ftui_render::grapheme_pool::GraphemePool;
205 use std::cell::RefCell;
206 use std::rc::Rc;
207
208 fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
209 let mut lines = Vec::new();
210 for y in 0..buf.height() {
211 let mut row = String::with_capacity(buf.width() as usize);
212 for x in 0..buf.width() {
213 let ch = buf
214 .get(x, y)
215 .and_then(|c| c.content.as_char())
216 .unwrap_or(' ');
217 row.push(ch);
218 }
219 lines.push(row);
220 }
221 lines
222 }
223
224 #[derive(Debug, Clone, Copy)]
225 struct Fill(char);
226
227 impl Widget for Fill {
228 fn render(&self, area: Rect, frame: &mut Frame) {
229 for y in area.y..area.bottom() {
230 for x in area.x..area.right() {
231 frame.buffer.set(x, y, Cell::from_char(self.0));
232 }
233 }
234 }
235 }
236
237 #[derive(Clone, Debug)]
239 struct Recorder {
240 rects: Rc<RefCell<Vec<Rect>>>,
241 }
242
243 impl Recorder {
244 fn new() -> (Self, Rc<RefCell<Vec<Rect>>>) {
245 let rects = Rc::new(RefCell::new(Vec::new()));
246 (
247 Self {
248 rects: rects.clone(),
249 },
250 rects,
251 )
252 }
253 }
254
255 impl Widget for Recorder {
256 fn render(&self, area: Rect, _frame: &mut Frame) {
257 self.rects.borrow_mut().push(area);
258 }
259 }
260
261 #[test]
262 fn empty_layout_is_noop() {
263 let layout = Layout::new();
264 let mut pool = GraphemePool::new();
265 let mut frame = Frame::new(10, 10, &mut pool);
266 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
267
268 for y in 0..10 {
269 for x in 0..10u16 {
270 assert!(frame.buffer.get(x, y).unwrap().is_empty());
271 }
272 }
273 }
274
275 #[test]
276 fn single_cell_layout() {
277 let layout = Layout::new()
278 .rows([Constraint::Min(0)])
279 .columns([Constraint::Min(0)])
280 .cell(Fill('X'), 0, 0);
281
282 let mut pool = GraphemePool::new();
283 let mut frame = Frame::new(5, 3, &mut pool);
284 layout.render(Rect::new(0, 0, 5, 3), &mut frame);
285
286 assert_eq!(buf_to_lines(&frame.buffer), vec!["XXXXX", "XXXXX", "XXXXX"]);
287 }
288
289 #[test]
290 fn two_by_two_grid() {
291 let layout = Layout::new()
292 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
293 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
294 .cell(Fill('A'), 0, 0)
295 .cell(Fill('B'), 0, 1)
296 .cell(Fill('C'), 1, 0)
297 .cell(Fill('D'), 1, 1);
298
299 let mut pool = GraphemePool::new();
300 let mut frame = Frame::new(6, 2, &mut pool);
301 layout.render(Rect::new(0, 0, 6, 2), &mut frame);
302
303 assert_eq!(buf_to_lines(&frame.buffer), vec!["AAABBB", "CCCDDD"]);
304 }
305
306 #[test]
307 fn column_spanning() {
308 let layout = Layout::new()
309 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
310 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
311 .child(Fill('H'), 0, 0, 1, 2) .cell(Fill('L'), 1, 0)
313 .cell(Fill('R'), 1, 1);
314
315 let mut pool = GraphemePool::new();
316 let mut frame = Frame::new(6, 2, &mut pool);
317 layout.render(Rect::new(0, 0, 6, 2), &mut frame);
318
319 assert_eq!(buf_to_lines(&frame.buffer), vec!["HHHHHH", "LLLRRR"]);
320 }
321
322 #[test]
323 fn row_spanning() {
324 let layout = Layout::new()
325 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
326 .columns([Constraint::Fixed(2), Constraint::Fixed(2)])
327 .child(Fill('S'), 0, 0, 2, 1) .cell(Fill('A'), 0, 1)
329 .cell(Fill('B'), 1, 1);
330
331 let mut pool = GraphemePool::new();
332 let mut frame = Frame::new(4, 2, &mut pool);
333 layout.render(Rect::new(0, 0, 4, 2), &mut frame);
334
335 assert_eq!(buf_to_lines(&frame.buffer), vec!["SSAA", "SSBB"]);
336 }
337
338 #[test]
339 fn layout_with_gap() {
340 let (a, a_rects) = Recorder::new();
341 let (b, b_rects) = Recorder::new();
342
343 let layout = Layout::new()
344 .rows([Constraint::Fixed(1)])
345 .columns([Constraint::Fixed(3), Constraint::Fixed(3)])
346 .col_gap(2)
347 .cell(a, 0, 0)
348 .cell(b, 0, 1);
349
350 let mut pool = GraphemePool::new();
351 let mut frame = Frame::new(10, 1, &mut pool);
352 layout.render(Rect::new(0, 0, 10, 1), &mut frame);
353
354 let a_rect = a_rects.borrow()[0];
355 let b_rect = b_rects.borrow()[0];
356
357 assert_eq!(a_rect.width, 3);
358 assert_eq!(b_rect.width, 3);
359 assert!(b_rect.x >= a_rect.right());
361 }
362
363 #[test]
364 fn fixed_and_flexible_rows() {
365 let (header, header_rects) = Recorder::new();
366 let (content, content_rects) = Recorder::new();
367 let (footer, footer_rects) = Recorder::new();
368
369 let layout = Layout::new()
370 .rows([
371 Constraint::Fixed(1),
372 Constraint::Min(0),
373 Constraint::Fixed(1),
374 ])
375 .columns([Constraint::Min(0)])
376 .cell(header, 0, 0)
377 .cell(content, 1, 0)
378 .cell(footer, 2, 0);
379
380 let mut pool = GraphemePool::new();
381 let mut frame = Frame::new(20, 10, &mut pool);
382 layout.render(Rect::new(0, 0, 20, 10), &mut frame);
383
384 let h = header_rects.borrow()[0];
385 let c = content_rects.borrow()[0];
386 let f = footer_rects.borrow()[0];
387
388 assert_eq!(h.height, 1);
389 assert_eq!(f.height, 1);
390 assert_eq!(c.height, 8); assert_eq!(h.y, 0);
392 assert_eq!(f.y, 9);
393 }
394
395 #[test]
396 fn zero_area_is_noop() {
397 let (rec, rects) = Recorder::new();
398 let layout = Layout::new()
399 .rows([Constraint::Min(0)])
400 .columns([Constraint::Min(0)])
401 .cell(rec, 0, 0);
402
403 let mut pool = GraphemePool::new();
404 let mut frame = Frame::new(5, 5, &mut pool);
405 layout.render(Rect::new(0, 0, 0, 0), &mut frame);
406
407 assert!(rects.borrow().is_empty());
408 }
409
410 #[test]
411 fn len_and_is_empty() {
412 assert!(Layout::new().is_empty());
413 assert_eq!(Layout::new().len(), 0);
414
415 let layout = Layout::new()
416 .rows([Constraint::Min(0)])
417 .columns([Constraint::Min(0)])
418 .cell(Fill('X'), 0, 0);
419 assert!(!layout.is_empty());
420 assert_eq!(layout.len(), 1);
421 }
422
423 #[test]
424 fn is_essential_delegates() {
425 struct Essential;
426 impl Widget for Essential {
427 fn render(&self, _: Rect, _: &mut Frame) {}
428 fn is_essential(&self) -> bool {
429 true
430 }
431 }
432
433 let not_essential = Layout::new()
434 .rows([Constraint::Min(0)])
435 .columns([Constraint::Min(0)])
436 .cell(Fill('X'), 0, 0);
437 assert!(!not_essential.is_essential());
438
439 let essential = Layout::new()
440 .rows([Constraint::Min(0)])
441 .columns([Constraint::Min(0)])
442 .cell(Essential, 0, 0);
443 assert!(essential.is_essential());
444 }
445
446 #[test]
447 fn deterministic_render_order() {
448 let layout = Layout::new()
450 .rows([Constraint::Fixed(1)])
451 .columns([Constraint::Fixed(3)])
452 .cell(Fill('A'), 0, 0)
453 .cell(Fill('B'), 0, 0); let mut pool = GraphemePool::new();
456 let mut frame = Frame::new(3, 1, &mut pool);
457 layout.render(Rect::new(0, 0, 3, 1), &mut frame);
458
459 assert_eq!(buf_to_lines(&frame.buffer), vec!["BBB"]);
460 }
461
462 #[test]
463 fn layout_with_offset_area() {
464 let (rec, rects) = Recorder::new();
465 let layout = Layout::new()
466 .rows([Constraint::Fixed(2)])
467 .columns([Constraint::Fixed(3)])
468 .cell(rec, 0, 0);
469
470 let mut pool = GraphemePool::new();
471 let mut frame = Frame::new(10, 10, &mut pool);
472 layout.render(Rect::new(3, 4, 5, 5), &mut frame);
473
474 let r = rects.borrow()[0];
475 assert_eq!(r.x, 3);
476 assert_eq!(r.y, 4);
477 assert_eq!(r.width, 3);
478 assert_eq!(r.height, 2);
479 }
480
481 #[test]
482 fn three_by_three_grid() {
483 let layout = Layout::new()
484 .rows([
485 Constraint::Fixed(1),
486 Constraint::Fixed(1),
487 Constraint::Fixed(1),
488 ])
489 .columns([
490 Constraint::Fixed(2),
491 Constraint::Fixed(2),
492 Constraint::Fixed(2),
493 ])
494 .cell(Fill('1'), 0, 0)
495 .cell(Fill('2'), 0, 1)
496 .cell(Fill('3'), 0, 2)
497 .cell(Fill('4'), 1, 0)
498 .cell(Fill('5'), 1, 1)
499 .cell(Fill('6'), 1, 2)
500 .cell(Fill('7'), 2, 0)
501 .cell(Fill('8'), 2, 1)
502 .cell(Fill('9'), 2, 2);
503
504 let mut pool = GraphemePool::new();
505 let mut frame = Frame::new(6, 3, &mut pool);
506 layout.render(Rect::new(0, 0, 6, 3), &mut frame);
507
508 assert_eq!(
509 buf_to_lines(&frame.buffer),
510 vec!["112233", "445566", "778899"]
511 );
512 }
513
514 #[test]
515 fn layout_default_equals_new() {
516 let def: Layout<'_> = Layout::default();
517 assert!(def.is_empty());
518 assert_eq!(def.len(), 0);
519 }
520
521 #[test]
522 fn gap_sets_both_row_and_col() {
523 let (a, a_rects) = Recorder::new();
524 let (b, b_rects) = Recorder::new();
525
526 let layout = Layout::new()
527 .rows([Constraint::Fixed(2), Constraint::Fixed(2)])
528 .columns([Constraint::Fixed(3)])
529 .gap(1)
530 .cell(a, 0, 0)
531 .cell(b, 1, 0);
532
533 let mut pool = GraphemePool::new();
534 let mut frame = Frame::new(10, 10, &mut pool);
535 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
536
537 let a_rect = a_rects.borrow()[0];
538 let b_rect = b_rects.borrow()[0];
539 assert!(b_rect.y >= a_rect.bottom());
541 }
542
543 #[test]
544 fn child_clamps_zero_span_to_one() {
545 let (rec, rects) = Recorder::new();
546 let layout = Layout::new()
547 .rows([Constraint::Fixed(3)])
548 .columns([Constraint::Fixed(4)])
549 .child(rec, 0, 0, 0, 0); let mut pool = GraphemePool::new();
552 let mut frame = Frame::new(10, 10, &mut pool);
553 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
554
555 let r = rects.borrow()[0];
556 assert!(r.width > 0 && r.height > 0);
557 }
558
559 #[test]
562 fn render_in_1x1_area() {
563 let (rec, rects) = Recorder::new();
564 let layout = Layout::new()
565 .rows([Constraint::Min(0)])
566 .columns([Constraint::Min(0)])
567 .cell(rec, 0, 0);
568
569 let mut pool = GraphemePool::new();
570 let mut frame = Frame::new(10, 10, &mut pool);
571 layout.render(Rect::new(3, 3, 1, 1), &mut frame);
572
573 let r = rects.borrow()[0];
574 assert_eq!(r, Rect::new(3, 3, 1, 1));
575 }
576
577 #[test]
578 fn no_constraints_with_children() {
579 let (rec, rects) = Recorder::new();
580 let layout = Layout::new().cell(rec, 0, 0);
582
583 let mut pool = GraphemePool::new();
584 let mut frame = Frame::new(10, 10, &mut pool);
585 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
586
587 let _ = rects.borrow().len();
590 }
591
592 #[test]
593 fn fixed_constraints_exceed_area() {
594 let (a, a_rects) = Recorder::new();
595 let (b, b_rects) = Recorder::new();
596 let layout = Layout::new()
598 .rows([Constraint::Fixed(1)])
599 .columns([Constraint::Fixed(10), Constraint::Fixed(10)])
600 .cell(a, 0, 0)
601 .cell(b, 0, 1);
602
603 let mut pool = GraphemePool::new();
604 let mut frame = Frame::new(20, 5, &mut pool);
605 layout.render(Rect::new(0, 0, 8, 1), &mut frame);
606
607 let a_r = a_rects.borrow();
609 let b_r = b_rects.borrow();
610 assert!(!a_r.is_empty());
611 assert!(a_r[0].width > 0 || !b_r.is_empty());
613 }
614
615 #[test]
616 fn gap_larger_than_area() {
617 let (rec, rects) = Recorder::new();
618 let layout = Layout::new()
619 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
620 .columns([Constraint::Min(0)])
621 .row_gap(100) .cell(rec, 0, 0);
623
624 let mut pool = GraphemePool::new();
625 let mut frame = Frame::new(10, 5, &mut pool);
626 layout.render(Rect::new(0, 0, 10, 5), &mut frame);
627
628 let _ = rects.borrow().len();
630 }
631
632 #[test]
633 fn is_essential_mixed_children() {
634 struct Essential;
635 impl Widget for Essential {
636 fn render(&self, _: Rect, _: &mut Frame) {}
637 fn is_essential(&self) -> bool {
638 true
639 }
640 }
641
642 let layout = Layout::new()
644 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
645 .columns([Constraint::Min(0)])
646 .cell(Fill('X'), 0, 0)
647 .cell(Essential, 1, 0);
648 assert!(layout.is_essential());
649 }
650
651 #[test]
652 fn is_essential_all_non_essential() {
653 let layout = Layout::new()
654 .rows([Constraint::Fixed(1)])
655 .columns([Constraint::Min(0)])
656 .cell(Fill('X'), 0, 0)
657 .cell(Fill('Y'), 0, 0);
658 assert!(!layout.is_essential());
659 }
660
661 #[test]
662 fn multiple_flexible_rows_share_space() {
663 let (a, a_rects) = Recorder::new();
664 let (b, b_rects) = Recorder::new();
665
666 let layout = Layout::new()
667 .rows([Constraint::Min(0), Constraint::Min(0)])
668 .columns([Constraint::Min(0)])
669 .cell(a, 0, 0)
670 .cell(b, 1, 0);
671
672 let mut pool = GraphemePool::new();
673 let mut frame = Frame::new(10, 10, &mut pool);
674 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
675
676 let a_h = a_rects.borrow()[0].height;
677 let b_h = b_rects.borrow()[0].height;
678 assert_eq!(a_h + b_h, 10);
679 assert!(a_h > 0 && b_h > 0);
680 }
681
682 #[test]
683 fn col_gap_with_single_column() {
684 let (rec, rects) = Recorder::new();
685 let layout = Layout::new()
687 .rows([Constraint::Min(0)])
688 .columns([Constraint::Min(0)])
689 .col_gap(5)
690 .cell(rec, 0, 0);
691
692 let mut pool = GraphemePool::new();
693 let mut frame = Frame::new(10, 5, &mut pool);
694 layout.render(Rect::new(0, 0, 10, 5), &mut frame);
695
696 let r = rects.borrow()[0];
697 assert_eq!(r.width, 10, "single column should get full width");
698 }
699
700 #[test]
701 fn row_gap_with_single_row() {
702 let (rec, rects) = Recorder::new();
703 let layout = Layout::new()
704 .rows([Constraint::Min(0)])
705 .columns([Constraint::Min(0)])
706 .row_gap(5)
707 .cell(rec, 0, 0);
708
709 let mut pool = GraphemePool::new();
710 let mut frame = Frame::new(10, 5, &mut pool);
711 layout.render(Rect::new(0, 0, 10, 5), &mut frame);
712
713 let r = rects.borrow()[0];
714 assert_eq!(r.height, 5, "single row should get full height");
715 }
716
717 #[test]
718 fn layout_debug_no_children() {
719 let layout = Layout::new()
720 .rows([Constraint::Fixed(1)])
721 .columns([Constraint::Fixed(2)]);
722 let dbg = format!("{layout:?}");
723 assert!(dbg.contains("Layout"));
724 assert!(dbg.contains("children"));
725 }
726
727 #[test]
728 fn child_beyond_grid_bounds() {
729 let (rec, rects) = Recorder::new();
730 let layout = Layout::new()
732 .rows([Constraint::Fixed(3)])
733 .columns([Constraint::Fixed(3)])
734 .cell(rec, 5, 5);
735
736 let mut pool = GraphemePool::new();
737 let mut frame = Frame::new(10, 10, &mut pool);
738 layout.render(Rect::new(0, 0, 10, 10), &mut frame);
739
740 let borrowed = rects.borrow();
743 if !borrowed.is_empty() {
744 let r = borrowed[0];
746 let _ = r;
748 }
749 }
750
751 #[test]
752 fn many_children_same_cell_last_wins() {
753 let layout = Layout::new()
754 .rows([Constraint::Fixed(1)])
755 .columns([Constraint::Fixed(3)])
756 .cell(Fill('A'), 0, 0)
757 .cell(Fill('B'), 0, 0)
758 .cell(Fill('C'), 0, 0);
759
760 let mut pool = GraphemePool::new();
761 let mut frame = Frame::new(3, 1, &mut pool);
762 layout.render(Rect::new(0, 0, 3, 1), &mut frame);
763
764 assert_eq!(buf_to_lines(&frame.buffer), vec!["CCC"]);
765 }
766
767 #[test]
768 fn render_fewer_children_clears_removed_region() {
769 let full = Layout::new()
770 .rows([Constraint::Fixed(1)])
771 .columns([Constraint::Fixed(4), Constraint::Fixed(4)])
772 .cell(Fill('A'), 0, 0)
773 .cell(Fill('B'), 0, 1);
774 let partial = Layout::new()
775 .rows([Constraint::Fixed(1)])
776 .columns([Constraint::Fixed(4), Constraint::Fixed(4)])
777 .cell(Fill('A'), 0, 0);
778
779 let area = Rect::new(0, 0, 8, 1);
780 let mut pool = GraphemePool::new();
781 let mut frame = Frame::new(8, 1, &mut pool);
782
783 full.render(area, &mut frame);
784 partial.render(area, &mut frame);
785
786 assert_eq!(buf_to_lines(&frame.buffer), vec!["AAAA "]);
787 }
788
789 #[test]
790 fn empty_layout_clears_previous_content() {
791 let filled = Layout::new()
792 .rows([Constraint::Fixed(1)])
793 .columns([Constraint::Fixed(4)])
794 .cell(Fill('X'), 0, 0);
795 let empty = Layout::new();
796
797 let area = Rect::new(0, 0, 4, 1);
798 let mut pool = GraphemePool::new();
799 let mut frame = Frame::new(4, 1, &mut pool);
800
801 filled.render(area, &mut frame);
802 empty.render(area, &mut frame);
803
804 assert_eq!(buf_to_lines(&frame.buffer), vec![" "]);
805 }
806
807 #[test]
810 fn layout_child_debug() {
811 let layout = Layout::new()
812 .rows([Constraint::Fixed(1)])
813 .columns([Constraint::Fixed(1)])
814 .child(Fill('X'), 2, 3, 4, 5);
815
816 let dbg = format!("{:?}", layout);
817 assert!(dbg.contains("Layout"));
818 assert!(dbg.contains("LayoutChild"));
819 }
820}