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    pub fn rows(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
85        self.row_constraints = constraints.into_iter().collect();
86        self
87    }
88
89    /// Set the column constraints.
90    pub fn columns(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
91        self.col_constraints = constraints.into_iter().collect();
92        self
93    }
94
95    /// Set the gap between rows.
96    pub fn row_gap(mut self, gap: u16) -> Self {
97        self.row_gap = gap;
98        self
99    }
100
101    /// Set the gap between columns.
102    pub fn col_gap(mut self, gap: u16) -> Self {
103        self.col_gap = gap;
104        self
105    }
106
107    /// Set uniform gap for both rows and columns.
108    pub fn gap(mut self, gap: u16) -> Self {
109        self.row_gap = gap;
110        self.col_gap = gap;
111        self
112    }
113
114    /// Add a child widget at a specific grid position with spanning.
115    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    /// Add a child widget at a single grid cell (1x1).
134    pub fn cell(self, widget: impl Widget + 'a, row: usize, col: usize) -> Self {
135        self.child(widget, row, col, 1, 1)
136    }
137
138    /// Number of children.
139    pub fn len(&self) -> usize {
140        self.children.len()
141    }
142
143    /// Whether the layout has no children.
144    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    /// Records the rect it receives during render.
214    #[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) // span both columns
288            .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) // span both rows
304            .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        // Gap of 2 between columns
336        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); // 10 - 1 (header) - 1 (footer)
367        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        // Later children overwrite earlier ones when placed in the same cell
425        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); // same cell, overwrites A
430
431        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}