Skip to main content

tui_lipan/widgets/grid/
mod.rs

1//! Explicit row/column grid layout (`Grid`).
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub(crate) use layout::measure_grid;
8pub(crate) use node::GridNode;
9pub(crate) use reconcile::{GridReconcile, reconcile_grid};
10
11use std::hash::Hash;
12
13use crate::core::element::{Element, ElementKind};
14use crate::layout::hash::LayoutHash;
15use crate::style::{Align, BorderStyle, Justify, LayoutConstraints, Length, Padding, Style};
16
17/// Track sizing and chrome configuration for a [`Grid`].
18#[derive(Clone, Debug)]
19pub struct GridProps {
20    /// Column track sizes. An empty list lets the grid infer columns from item placement.
21    pub columns: Vec<Length>,
22    /// Row track sizes. An empty list lets the grid infer rows from item placement.
23    pub rows: Vec<Length>,
24    /// Horizontal gap between columns, in cells.
25    pub gap_x: u16,
26    /// Vertical gap between rows, in cells.
27    pub gap_y: u16,
28    /// Inner padding applied inside the grid (and inside the border, if any).
29    pub padding: Padding,
30    /// Base style for the grid container.
31    pub style: Style,
32    /// Cross-axis alignment of each item within its cell.
33    pub align: Align,
34    /// Single-child cell alignment on the main axis. `SpaceBetween`, `SpaceAround`, and
35    /// `SpaceEvenly` match `Start` (no siblings to distribute between).
36    pub justify: Justify,
37    /// Width of the grid container.
38    pub width: Length,
39    /// Height of the grid container.
40    pub height: Length,
41    /// Whether to draw a border around the grid.
42    pub border: bool,
43    /// Border line style used when [`border`](Self::border) is enabled.
44    pub border_style: BorderStyle,
45}
46
47impl Default for GridProps {
48    fn default() -> Self {
49        Self {
50            columns: Vec::new(),
51            rows: Vec::new(),
52            gap_x: 0,
53            gap_y: 0,
54            padding: Padding::default(),
55            style: Style::default(),
56            align: Align::Start,
57            justify: Justify::Start,
58            width: Length::Flex(1),
59            height: Length::Flex(1),
60            border: false,
61            border_style: BorderStyle::Plain,
62        }
63    }
64}
65
66/// A single child placed in a [`Grid`], with optional explicit cell placement and span.
67#[derive(Clone)]
68pub struct GridItem {
69    pub(crate) element: Element,
70    pub(crate) placement: Option<(u16, u16)>,
71    pub(crate) span: (u16, u16),
72}
73
74/// Explicit row/column grid container.
75///
76/// Children are placed either in flow order ([`child`](Self::child)) or at explicit
77/// cells ([`cell`](Self::cell)), and may span multiple tracks
78/// ([`span`](Self::span) / [`cell_span`](Self::cell_span)).
79#[derive(Clone)]
80pub struct Grid {
81    pub(crate) props: GridProps,
82    pub(crate) items: Vec<GridItem>,
83}
84
85impl Grid {
86    /// Creates an empty grid with default props (flex width/height, no tracks).
87    pub fn new() -> Self {
88        Self {
89            props: GridProps::default(),
90            items: Vec::new(),
91        }
92    }
93
94    /// Sets explicit column track sizes.
95    pub fn columns<I: IntoIterator<Item = Length>>(mut self, columns: I) -> Self {
96        self.props.columns = columns.into_iter().collect();
97        self
98    }
99
100    /// Sets explicit row track sizes.
101    pub fn rows<I: IntoIterator<Item = Length>>(mut self, rows: I) -> Self {
102        self.props.rows = rows.into_iter().collect();
103        self
104    }
105
106    /// Sets `n` equally sized [`Auto`](Length::Auto) columns (at least one).
107    pub fn uniform_columns(mut self, n: usize) -> Self {
108        self.props.columns = vec![Length::Auto; n.max(1)];
109        self
110    }
111
112    /// Sets both the horizontal and vertical gap between tracks.
113    pub fn gap(mut self, gap: u16) -> Self {
114        self.props.gap_x = gap;
115        self.props.gap_y = gap;
116        self
117    }
118
119    /// Sets the horizontal gap between columns.
120    pub fn gap_x(mut self, gap: u16) -> Self {
121        self.props.gap_x = gap;
122        self
123    }
124
125    /// Sets the vertical gap between rows.
126    pub fn gap_y(mut self, gap: u16) -> Self {
127        self.props.gap_y = gap;
128        self
129    }
130
131    /// Alias for [`gap_x`](Self::gap_x): the horizontal gap between columns.
132    pub fn column_gap(mut self, gap: u16) -> Self {
133        self.props.gap_x = gap;
134        self
135    }
136
137    /// Alias for [`gap_y`](Self::gap_y): the vertical gap between rows.
138    pub fn row_gap(mut self, gap: u16) -> Self {
139        self.props.gap_y = gap;
140        self
141    }
142
143    /// Appends a child in flow order (auto-placed into the next free cell).
144    pub fn child(mut self, element: impl Into<Element>) -> Self {
145        self.items.push(GridItem {
146            element: element.into(),
147            placement: None,
148            span: (1, 1),
149        });
150        self
151    }
152
153    /// Sets the row/column span of the most recently added child (minimum 1 each).
154    pub fn span(mut self, row_span: u16, col_span: u16) -> Self {
155        if let Some(last) = self.items.last_mut() {
156            last.span = (row_span.max(1), col_span.max(1));
157        }
158        self
159    }
160
161    /// Places a child at an explicit `(row, col)` cell.
162    pub fn cell(mut self, row: u16, col: u16, element: impl Into<Element>) -> Self {
163        self.items.push(GridItem {
164            element: element.into(),
165            placement: Some((row, col)),
166            span: (1, 1),
167        });
168        self
169    }
170
171    /// Places a child at an explicit `(row, col)` cell spanning `row_span`×`col_span` tracks.
172    pub fn cell_span(
173        mut self,
174        row: u16,
175        col: u16,
176        row_span: u16,
177        col_span: u16,
178        element: impl Into<Element>,
179    ) -> Self {
180        self.items.push(GridItem {
181            element: element.into(),
182            placement: Some((row, col)),
183            span: (row_span.max(1), col_span.max(1)),
184        });
185        self
186    }
187
188    /// Sets the inner padding of the grid.
189    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
190        self.props.padding = padding.into();
191        self
192    }
193
194    /// Sets the base style of the grid container.
195    pub fn style(mut self, style: Style) -> Self {
196        self.props.style = style;
197        self
198    }
199
200    /// Sets the cross-axis alignment of items within their cells.
201    pub fn align(mut self, align: Align) -> Self {
202        self.props.align = align;
203        self
204    }
205
206    /// Sets the main-axis alignment of a single child within its cell.
207    pub fn justify(mut self, justify: Justify) -> Self {
208        self.props.justify = justify;
209        self
210    }
211
212    /// Sets the width of the grid container.
213    pub fn width(mut self, width: Length) -> Self {
214        self.props.width = width;
215        self
216    }
217
218    /// Sets the height of the grid container.
219    pub fn height(mut self, height: Length) -> Self {
220        self.props.height = height;
221        self
222    }
223
224    /// Toggles drawing a border around the grid.
225    pub fn border(mut self, border: bool) -> Self {
226        self.props.border = border;
227        self
228    }
229
230    /// Sets the border line style (takes effect when [`border`](Self::border) is enabled).
231    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
232        self.props.border_style = border_style;
233        self
234    }
235}
236
237impl Default for Grid {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243impl From<Grid> for Element {
244    fn from(value: Grid) -> Self {
245        let has_children = !value.items.is_empty();
246        let is_flex_h = matches!(value.props.height, Length::Flex(_));
247        let is_flex_w = matches!(value.props.width, Length::Flex(_));
248        let chrome_h = value.props.padding.vertical() + if value.props.border { 2 } else { 0 };
249
250        let (min_w, min_h) = match (is_flex_w, is_flex_h) {
251            (true, true) => {
252                let mut w = value.props.padding.horizontal();
253                let mut h = chrome_h;
254                if value.props.border {
255                    w += 2;
256                }
257                if has_children {
258                    w = w.max(1);
259                    h = h.max(1);
260                }
261                (w, h)
262            }
263            (true, false) => {
264                let (_, h) = measure_grid(&value.props, &value.items, None, None);
265                let mut min_w = value.props.padding.horizontal();
266                if value.props.border {
267                    min_w += 2;
268                }
269                (min_w, h)
270            }
271            (false, true) => {
272                let (w, _) = measure_grid(&value.props, &value.items, None, None);
273                let mut chrome_h = value.props.padding.vertical();
274                if value.props.border {
275                    chrome_h += 2;
276                }
277                (w, chrome_h.max(1))
278            }
279            (false, false) => measure_grid(&value.props, &value.items, None, None),
280        };
281
282        let children_need_width = value
283            .items
284            .iter()
285            .any(|i| crate::widgets::scroll_child_height_depends_on_width(&i.element));
286
287        let min_h = if matches!(value.props.height, Length::Auto) && children_need_width {
288            chrome_h
289        } else {
290            min_h
291        };
292
293        Element::new(ElementKind::Grid(value)).with_layout(
294            LayoutConstraints::default()
295                .min_width(Length::Px(min_w))
296                .min_height(Length::Px(min_h)),
297        )
298    }
299}
300
301impl LayoutHash for Grid {
302    fn layout_hash(
303        &self,
304        hasher: &mut impl std::hash::Hasher,
305        recurse: &dyn Fn(&Element) -> Option<u64>,
306    ) -> Option<()> {
307        crate::layout::hash::hash_grid_props(&self.props, hasher);
308        for item in &self.items {
309            recurse(&item.element)?.hash(hasher);
310            item.placement.hash(hasher);
311            item.span.hash(hasher);
312        }
313        Some(())
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use crate::core::element::{Element, IntoElement, Key};
320    use crate::core::node::{NodeId, NodeTree};
321    use crate::layout::LayoutEngine;
322    use crate::layout::measure::min_size_constrained;
323    use crate::style::{Align, Length, Rect};
324    use crate::widgets::{Frame, Grid, Text};
325
326    fn find_by_key(tree: &NodeTree, key: &str) -> NodeId {
327        let key = Key::from(key.to_string());
328        tree.iter()
329            .find(|n| n.key.as_ref() == Some(&key))
330            .map(|n| n.id)
331            .unwrap_or(NodeId::INVALID)
332    }
333
334    #[test]
335    fn grid_rows_measure_to_content_height() {
336        let grid = Grid::new()
337            .uniform_columns(2)
338            .gap(1)
339            .child(Text::new("left"))
340            .child(Text::new("right"));
341
342        let (_, h) = min_size_constrained(&grid.into(), Some(40), None);
343
344        assert!(h >= 1, "grid height should include row content, got {h}");
345    }
346
347    #[test]
348    fn grid_tracks_position_2x2_children() {
349        let grid: Element = Grid::new()
350            .columns([Length::Auto, Length::Auto])
351            .rows([Length::Px(6), Length::Px(6)])
352            .width(Length::Flex(1))
353            .height(Length::Flex(1))
354            .child(Frame::new().border(true).key("a"))
355            .child(Frame::new().border(true).key("b"))
356            .child(Frame::new().border(true).key("c"))
357            .child(Frame::new().border(true).key("d"))
358            .into();
359
360        let mut tree = NodeTree::new();
361        LayoutEngine::reconcile_with_focus(
362            &mut tree,
363            &grid,
364            Rect {
365                x: 0,
366                y: 0,
367                w: 40,
368                h: 12,
369            },
370            None,
371        );
372
373        let a = tree.node(find_by_key(&tree, "a")).rect;
374        let c = tree.node(find_by_key(&tree, "c")).rect;
375
376        assert_eq!(a.y, 0);
377        assert_eq!(c.y, 6);
378        assert!(a.h <= 6 && c.h <= 6);
379    }
380
381    #[test]
382    fn grid_spanning_cell_width_is_two_tracks_plus_gap_x() {
383        let grid: Element = Grid::new()
384            .columns([Length::Px(5), Length::Px(7)])
385            .rows([Length::Px(10)])
386            .gap_x(2)
387            .align(Align::Stretch)
388            .cell_span(0, 0, 1, 2, Frame::new().border(false).key("span"))
389            .into();
390
391        let mut tree = NodeTree::new();
392        LayoutEngine::reconcile_with_focus(
393            &mut tree,
394            &grid,
395            Rect {
396                x: 0,
397                y: 0,
398                w: 40,
399                h: 12,
400            },
401            None,
402        );
403
404        let span = tree.node(find_by_key(&tree, "span")).rect;
405        assert_eq!(span.w, 5 + 2 + 7);
406    }
407
408    #[test]
409    fn grid_auto_tracks_do_not_absorb_parent_slack() {
410        let grid: Element = Grid::new()
411            .uniform_columns(3)
412            .rows([Length::Auto, Length::Auto, Length::Auto])
413            .gap_x(2)
414            .gap_y(0)
415            .cell(
416                0,
417                0,
418                Frame::new()
419                    .border(true)
420                    .width(Length::Px(5))
421                    .height(Length::Px(3))
422                    .key("a"),
423            )
424            .cell(
425                0,
426                1,
427                Frame::new()
428                    .border(true)
429                    .width(Length::Px(5))
430                    .height(Length::Px(3))
431                    .key("b"),
432            )
433            .cell(
434                1,
435                0,
436                Frame::new()
437                    .border(true)
438                    .width(Length::Px(5))
439                    .height(Length::Px(3))
440                    .key("c"),
441            )
442            .into();
443
444        let mut tree = NodeTree::new();
445        LayoutEngine::reconcile_with_focus(
446            &mut tree,
447            &grid,
448            Rect {
449                x: 0,
450                y: 0,
451                w: 40,
452                h: 12,
453            },
454            None,
455        );
456
457        let a = tree.node(find_by_key(&tree, "a")).rect;
458        let b = tree.node(find_by_key(&tree, "b")).rect;
459        let c = tree.node(find_by_key(&tree, "c")).rect;
460
461        assert_eq!(a.w, 5);
462        assert_eq!(a.h, 3);
463        assert_eq!(b.x, a.x + 5 + 2);
464        assert_eq!(c.y, a.y + 3);
465    }
466}