Skip to main content

ftui_widgets/
layout.rs

1#![forbid(unsafe_code)]
2
3//! Layout composition widget.
4//!
5//! A 2D grid-based layout container that places child widgets using
6//! [`Grid`] constraints. Each child is assigned to a grid cell or span,
7//! and the grid solver computes the final placement rects.
8//!
9//! This widget is glue over `ftui_layout::Grid` — it does not implement
10//! a parallel constraint solver.
11//!
12//! # Example
13//!
14//! ```ignore
15//! use ftui_widgets::layout::Layout;
16//! use ftui_layout::Constraint;
17//!
18//! let layout = Layout::new()
19//!     .rows([Constraint::Fixed(1), Constraint::Min(0), Constraint::Fixed(1)])
20//!     .columns([Constraint::Fixed(20), Constraint::Min(0)])
21//!     .child(header_widget, 0, 0, 1, 2)  // row 0, col 0, span 1x2
22//!     .child(sidebar_widget, 1, 0, 1, 1)
23//!     .child(content_widget, 1, 1, 1, 1)
24//!     .child(footer_widget, 2, 0, 1, 2);
25//! ```
26
27use crate::Widget;
28use ftui_core::geometry::Rect;
29use ftui_layout::{Constraint, Grid};
30use ftui_render::frame::Frame;
31
32/// A child entry in the layout grid.
33pub 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/// A 2D grid-based layout container.
53///
54/// Children are placed at grid coordinates with optional spanning.
55/// The grid solver distributes space according to row/column constraints.
56#[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    /// Create a new empty layout.
73    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    /// Set the row constraints.
84    #[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    /// Set the column constraints.
91    #[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    /// Set the gap between rows.
98    #[must_use]
99    pub fn row_gap(mut self, gap: u16) -> Self {
100        self.row_gap = gap;
101        self
102    }
103
104    /// Set the gap between columns.
105    #[must_use]
106    pub fn col_gap(mut self, gap: u16) -> Self {
107        self.col_gap = gap;
108        self
109    }
110
111    /// Set uniform gap for both rows and columns.
112    #[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    /// Add a child widget at a specific grid position with spanning.
120    #[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    /// Add a child widget at a single grid cell (1x1).
140    #[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    /// Number of children.
146    #[inline]
147    pub fn len(&self) -> usize {
148        self.children.len()
149    }
150
151    /// Whether the layout has no children.
152    #[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        // Layout owns the full grid rect. Clear stale child glyphs before
165        // rendering the current child set while preserving parent-applied
166        // styling already present in the buffer.
167        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    /// Records the rect it receives during render.
238    #[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) // span both columns
312            .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) // span both rows
328            .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        // Gap of 2 between columns
360        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); // 10 - 1 (header) - 1 (footer)
391        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        // Later children overwrite earlier ones when placed in the same cell
449        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); // same cell, overwrites A
454
455        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        // Gap of 1 between rows
540        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); // both spans 0 -> clamped to 1
550
551        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    // ─── Edge-case tests (bd-x93m1) ────────────────────────────────────
560
561    #[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        // No rows() or columns() called — empty constraint vecs
581        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        // Grid with empty constraints → no cells → child skipped (empty rect)
588        // Just verify it doesn't panic
589        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        // Two columns of Fixed(10) in a width=8 area
597        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        // Both should get some allocation even if constraints can't all be satisfied
608        let a_r = a_rects.borrow();
609        let b_r = b_rects.borrow();
610        assert!(!a_r.is_empty());
611        // At least one child should have been rendered
612        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) // gap >> area height
622            .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        // Should not panic; child may or may not get space depending on solver
629        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        // One non-essential + one essential = essential
643        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        // col_gap shouldn't matter with only 1 column
686        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        // 1x1 grid but child at row=5, col=5
731        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        // Child beyond grid bounds — grid.span() returns empty or default rect
741        // Either not rendered or rendered with zero/minimal area
742        let borrowed = rects.borrow();
743        if !borrowed.is_empty() {
744            // If rendered, it should have gotten an area (possibly empty)
745            let r = borrowed[0];
746            // Just verify no panic occurred
747            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    // ─── End edge-case tests (bd-x93m1) ──────────────────────────────
808
809    #[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}