Skip to main content

rosace_layout/widgets/
grid.rs

1//! [`Grid`] — a fixed-column grid layout.
2
3use rosace_core::child_container::ChildContainer;
4use rosace_core::element::{Element, NativeElement};
5#[cfg(debug_assertions)]
6use rosace_core::render_object::AxisBound;
7use rosace_core::types::{Point, Size};
8#[cfg(debug_assertions)]
9use rosace_trace::{
10    event::{ComponentId, RosaceTrace, TraceConstraints},
11    trace,
12};
13
14use crate::constraints::Constraints;
15use crate::layout_result::LayoutResult;
16
17/// A widget that arranges its children in a grid with a fixed number of columns.
18///
19/// Row heights are determined by the tallest child in each row; column widths
20/// by the widest child in each column.  Use [`spacing`](Self::spacing) to add
21/// a uniform gap between cells on both axes.
22#[derive(Debug, Clone)]
23pub struct Grid {
24    children: Vec<Element>,
25    columns: usize,
26    spacing: f32,
27}
28
29impl Grid {
30    /// Create a new `Grid` with the given number of `columns` and zero spacing.
31    pub fn new(columns: usize) -> Self {
32        Self {
33            children: Vec::new(),
34            columns: columns.max(1),
35            spacing: 0.0,
36        }
37    }
38
39    /// Set the gap in logical pixels between cells (applied horizontally and vertically).
40    pub fn spacing(mut self, s: f32) -> Self {
41        self.spacing = s;
42        self
43    }
44
45    /// Perform the Measure + Place passes and return a [`LayoutResult`].
46    ///
47    /// `child_sizes` must be in the same order as children appended via
48    /// [`ChildContainer::child`] / [`ChildContainer::children`].
49    ///
50    /// Emits [`RosaceTrace::LayoutStart`] and [`RosaceTrace::LayoutEnd`] events.
51    pub fn layout(&self, constraints: Constraints, child_sizes: &[Size]) -> LayoutResult {
52        #[cfg(debug_assertions)]
53        let start = std::time::Instant::now();
54
55        #[cfg(debug_assertions)]
56        trace!(RosaceTrace::LayoutStart {
57            component: ComponentId(0),
58            constraints: TraceConstraints {
59                min_width: constraints.min_width,
60                max_width: match &constraints.max_width {
61                    AxisBound::Bounded(v) => Some(*v),
62                    _ => None,
63                },
64                min_height: constraints.min_height,
65                max_height: match &constraints.max_height {
66                    AxisBound::Bounded(v) => Some(*v),
67                    _ => None,
68                },
69            },
70        });
71
72        let n = child_sizes.len();
73        let result = if n == 0 {
74            LayoutResult {
75                size: constraints.constrain(Size {
76                    width: 0.0,
77                    height: 0.0,
78                }),
79                child_positions: vec![],
80            }
81        } else {
82            let cols = self.columns;
83            let rows = n.div_ceil(cols);
84
85            // Maximum width per column.
86            let mut col_widths = vec![0.0_f32; cols];
87            for (i, size) in child_sizes.iter().enumerate() {
88                let col = i % cols;
89                col_widths[col] = col_widths[col].max(size.width);
90            }
91
92            // Maximum height per row.
93            let mut row_heights = vec![0.0_f32; rows];
94            for (i, size) in child_sizes.iter().enumerate() {
95                let row = i / cols;
96                row_heights[row] = row_heights[row].max(size.height);
97            }
98
99            // X offset for each column.
100            let mut col_offsets = vec![0.0_f32; cols];
101            for c in 1..cols {
102                col_offsets[c] = col_offsets[c - 1] + col_widths[c - 1] + self.spacing;
103            }
104
105            // Y offset for each row.
106            let mut row_offsets = vec![0.0_f32; rows];
107            for r in 1..rows {
108                row_offsets[r] = row_offsets[r - 1] + row_heights[r - 1] + self.spacing;
109            }
110
111            let mut positions = Vec::with_capacity(n);
112            for i in 0..n {
113                let col = i % cols;
114                let row = i / cols;
115                positions.push(Point {
116                    x: col_offsets[col],
117                    y: row_offsets[row],
118                });
119            }
120
121            let total_w: f32 =
122                col_widths.iter().sum::<f32>() + self.spacing * (cols - 1) as f32;
123            let total_h: f32 =
124                row_heights.iter().sum::<f32>() + self.spacing * (rows - 1) as f32;
125
126            LayoutResult {
127                size: constraints.constrain(Size {
128                    width: total_w,
129                    height: total_h,
130                }),
131                child_positions: positions,
132            }
133        };
134
135        #[cfg(debug_assertions)]
136        trace!(RosaceTrace::LayoutEnd {
137            component: ComponentId(0),
138            size: result.size,
139            duration: start.elapsed(),
140        });
141
142        result
143    }
144}
145
146impl ChildContainer for Grid {
147    fn child(mut self, element: impl Into<Element>) -> Self {
148        self.children.push(element.into());
149        self
150    }
151
152    fn children<E: Into<Element>>(mut self, elements: Vec<E>) -> Self {
153        self.children
154            .extend(elements.into_iter().map(Into::into));
155        self
156    }
157
158    fn prepend(mut self, element: impl Into<Element>) -> Self {
159        self.children.insert(0, element.into());
160        self
161    }
162}
163
164impl From<Grid> for Element {
165    fn from(g: Grid) -> Self {
166        Element::Native(NativeElement {
167            tag: "Grid",
168            payload: None,
169            children: g.children,
170            key: None,
171        })
172    }
173}