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() || 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    /// Records the rect it receives during render.
223    #[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) // span both columns
297            .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) // span both rows
313            .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        // Gap of 2 between columns
345        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); // 10 - 1 (header) - 1 (footer)
376        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        // Later children overwrite earlier ones when placed in the same cell
434        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); // same cell, overwrites A
439
440        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        // Gap of 1 between rows
525        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); // both spans 0 -> clamped to 1
535
536        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    // ─── Edge-case tests (bd-x93m1) ────────────────────────────────────
545
546    #[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        // No rows() or columns() called — empty constraint vecs
566        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        // Grid with empty constraints → no cells → child skipped (empty rect)
573        // Just verify it doesn't panic
574        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        // Two columns of Fixed(10) in a width=8 area
582        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        // Both should get some allocation even if constraints can't all be satisfied
593        let a_r = a_rects.borrow();
594        let b_r = b_rects.borrow();
595        assert!(!a_r.is_empty());
596        // At least one child should have been rendered
597        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) // gap >> area height
607            .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        // Should not panic; child may or may not get space depending on solver
614        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        // One non-essential + one essential = essential
628        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        // col_gap shouldn't matter with only 1 column
671        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        // 1x1 grid but child at row=5, col=5
716        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        // Child beyond grid bounds — grid.span() returns empty or default rect
726        // Either not rendered or rendered with zero/minimal area
727        let borrowed = rects.borrow();
728        if !borrowed.is_empty() {
729            // If rendered, it should have gotten an area (possibly empty)
730            let r = borrowed[0];
731            // Just verify no panic occurred
732            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    // ─── End edge-case tests (bd-x93m1) ──────────────────────────────
753
754    #[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}