Skip to main content

rosace_widgets/tree/
grid.rs

1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use super::{Widget, Children, LayoutCtx, PaintCtx, BoxedWidget, avail_w};
4
5/// Placement algorithm for a [`Grid`] (D115/Phase 32 Step 1).
6///
7/// Internal — selected through the [`Grid::staggered`] / [`Grid::bento`]
8/// builders; the default (`Uniform`) is the original Grid behavior,
9/// unchanged (Phase 32 Migration Rule: all additive).
10#[derive(Clone, Copy, PartialEq, Eq, Default)]
11enum GridMode {
12    /// Equal-width cells, row height = tallest child of that row (the
13    /// original, default behavior).
14    #[default]
15    Uniform,
16    /// Masonry: each child keeps its OWN measured height at the column
17    /// width and drops into the currently-shortest column.
18    Staggered,
19    /// Fixed lattice: items span whole columns/rows (see
20    /// [`Grid::child_span`]); every lattice row is [`Grid::row_height`]
21    /// tall.
22    Bento,
23}
24
25/// Default lattice row height for [`Grid::bento`] mode, in logical px.
26const DEFAULT_BENTO_ROW_HEIGHT: f32 = 96.0;
27
28/// A fixed-column grid. Children flow left→right, top→bottom into `columns`
29/// equal-width cells; each row's height is its tallest child. Lays out
30/// something new (not a Column/Row) — see D095.
31///
32/// Two additional placement modes (D115/Phase 32 Step 1):
33/// - [`Grid::staggered`] — masonry packing (Pinterest-style): children keep
34///   their own measured heights and fill the shortest column first.
35/// - [`Grid::bento`] — a fixed lattice where children added via
36///   [`Grid::child_span`] cover multiple columns/rows (dashboard tiles).
37pub struct Grid {
38    columns: usize,
39    spacing: f32,
40    run_spacing: f32,
41    children: Vec<BoxedWidget>,
42    /// Per-child `(col_span, row_span)`, parallel to `children`. Only
43    /// consulted in [`GridMode::Bento`]; `(1, 1)` everywhere else.
44    spans: Vec<(u16, u16)>,
45    mode: GridMode,
46    /// Lattice row height for bento mode (logical px).
47    row_height: f32,
48}
49
50impl Grid {
51    /// A uniform grid with `columns` equal-width columns.
52    pub fn new(columns: usize) -> Self {
53        Self {
54            columns: columns.max(1),
55            spacing: 8.0,
56            run_spacing: 8.0,
57            children: Vec::new(),
58            spans: Vec::new(),
59            mode: GridMode::default(),
60            row_height: DEFAULT_BENTO_ROW_HEIGHT,
61        }
62    }
63    /// Horizontal gap between columns (logical px).
64    pub fn spacing(mut self, s: f32) -> Self { self.spacing = s; self }
65    /// Vertical gap between rows (logical px).
66    pub fn run_spacing(mut self, s: f32) -> Self { self.run_spacing = s; self }
67    /// Append a child (span `1×1` in bento mode).
68    pub fn child(mut self, w: impl Widget + 'static) -> Self {
69        self.children.push(Box::new(w));
70        self.spans.push((1, 1));
71        self
72    }
73    /// Append several children (each span `1×1` in bento mode).
74    pub fn children(mut self, ws: Vec<BoxedWidget>) -> Self {
75        self.spans.extend(std::iter::repeat_n((1, 1), ws.len()));
76        self.children.extend(ws);
77        self
78    }
79    /// A uniform `columns`-wide grid of `count` items, each built by calling
80    /// `builder(i)` — a convenience constructor for the common "N items from
81    /// a data source" case, so callers don't have to hand-build a `Vec`
82    /// first. Eager, not virtualized (all `count` children build up front;
83    /// for large counts where that matters, `ListView::builder` is the
84    /// virtualized one).
85    pub fn builder(columns: usize, count: usize, builder: impl Fn(usize) -> BoxedWidget) -> Self {
86        Self::new(columns).children((0..count).map(builder).collect())
87    }
88
89    /// Switch to masonry placement: each child keeps its own measured
90    /// height at the column width and is placed into the currently-shortest
91    /// column (leftmost wins ties). Layout height = the tallest column.
92    pub fn staggered(mut self) -> Self { self.mode = GridMode::Staggered; self }
93
94    /// Switch to bento placement: children occupy whole cells of a fixed
95    /// lattice (`columns` wide, rows of [`Grid::row_height`]), spanning
96    /// multiple columns/rows per [`Grid::child_span`]. Items are placed
97    /// first-fit: top-to-bottom, left-to-right, into the first free block
98    /// that fits their span.
99    pub fn bento(mut self) -> Self { self.mode = GridMode::Bento; self }
100
101    /// Append a child spanning `col_span × row_span` lattice cells and
102    /// switch to bento mode.
103    ///
104    /// A per-child span (rather than a parallel `.bento(Vec<(u16, u16)>)`
105    /// span list) was chosen deliberately: the span lives at the same call
106    /// site as the child it describes, so conditionally-added children can
107    /// never silently desynchronize an index-aligned spans vector.
108    pub fn child_span(mut self, w: impl Widget + 'static, col_span: u16, row_span: u16) -> Self {
109        self.mode = GridMode::Bento;
110        self.children.push(Box::new(w));
111        self.spans.push((col_span.max(1), row_span.max(1)));
112        self
113    }
114
115    /// Lattice row height for bento mode (logical px, default `96.0`).
116    pub fn row_height(mut self, h: f32) -> Self { self.row_height = h.max(1.0); self }
117
118    fn cell_width(&self, total: f32) -> f32 {
119        let gaps = self.spacing * (self.columns.saturating_sub(1)) as f32;
120        ((total - gaps) / self.columns as f32).max(0.0)
121    }
122
123    /// Measured cell sizes + total height for a given available width
124    /// (uniform mode).
125    fn measure(&self, ctx: &LayoutCtx, width: f32) -> (Vec<Size>, f32) {
126        let cw = self.cell_width(width);
127        let sizes: Vec<Size> = self.children.iter()
128            .map(|c| c.layout(&ctx.with_constraints(Constraints::loose(cw, f32::INFINITY))))
129            .collect();
130        let mut y = 0.0;
131        let mut i = 0;
132        while i < sizes.len() {
133            let row_h = sizes[i..(i + self.columns).min(sizes.len())]
134                .iter().map(|s| s.height).fold(0.0_f32, f32::max);
135            y += row_h;
136            if i + self.columns < sizes.len() { y += self.run_spacing; }
137            i += self.columns;
138        }
139        (sizes, y)
140    }
141
142    /// Masonry placement: per-child rects (relative to the grid origin) +
143    /// total content height. Each child is measured at the column width,
144    /// keeps its own height, and goes into the currently-shortest column.
145    fn arrange_staggered(&self, ctx: &LayoutCtx, width: f32) -> (Vec<Rect>, f32) {
146        let cw = self.cell_width(width);
147        let mut col_h = vec![0.0f32; self.columns];
148        let mut rects = Vec::with_capacity(self.children.len());
149        for c in &self.children {
150            let s = c.layout(&ctx.with_constraints(Constraints::loose(cw, f32::INFINITY)));
151            // Shortest column; leftmost wins ties (the masonry convention).
152            let mut col = 0;
153            for (i, h) in col_h.iter().enumerate().skip(1) {
154                if *h < col_h[col] { col = i; }
155            }
156            let x = col as f32 * (cw + self.spacing);
157            rects.push(Rect {
158                origin: Point { x, y: col_h[col] },
159                size: Size { width: cw, height: s.height },
160            });
161            col_h[col] += s.height + self.run_spacing;
162        }
163        let tallest = col_h.iter().fold(0.0_f32, |a, &h| a.max(h));
164        (rects, (tallest - self.run_spacing).max(0.0))
165    }
166
167    /// Bento placement: per-child rects (relative) + total content height.
168    /// First-fit on a `columns`-wide lattice of `row_height`-tall rows.
169    fn arrange_bento(&self, width: f32) -> (Vec<Rect>, f32) {
170        let cw = self.cell_width(width);
171        // Occupancy lattice — grown row-by-row as placements demand.
172        let mut occ: Vec<Vec<bool>> = Vec::new();
173        let mut rects = Vec::with_capacity(self.children.len());
174        let mut rows_used = 0usize;
175
176        for i in 0..self.children.len() {
177            let (cs, rs) = self.spans.get(i).copied().unwrap_or((1, 1));
178            let cs = (cs as usize).clamp(1, self.columns);
179            let rs = (rs as usize).max(1);
180
181            let (row, col) = Self::first_fit(&occ, self.columns, cs, rs);
182            // Grow the lattice and mark the block occupied.
183            while occ.len() < row + rs { occ.push(vec![false; self.columns]); }
184            for cells in occ.iter_mut().take(row + rs).skip(row) {
185                for cell in cells.iter_mut().take(col + cs).skip(col) { *cell = true; }
186            }
187            rows_used = rows_used.max(row + rs);
188
189            rects.push(Rect {
190                origin: Point {
191                    x: col as f32 * (cw + self.spacing),
192                    y: row as f32 * (self.row_height + self.run_spacing),
193                },
194                size: Size {
195                    width: cs as f32 * cw + (cs - 1) as f32 * self.spacing,
196                    height: rs as f32 * self.row_height + (rs - 1) as f32 * self.run_spacing,
197                },
198            });
199        }
200
201        let total = if rows_used == 0 {
202            0.0
203        } else {
204            rows_used as f32 * self.row_height + (rows_used - 1) as f32 * self.run_spacing
205        };
206        (rects, total)
207    }
208
209    /// First lattice position `(row, col)` where a `cs × rs` block fits.
210    /// Always terminates: every row at/after `occ.len()` is empty.
211    fn first_fit(occ: &[Vec<bool>], columns: usize, cs: usize, rs: usize) -> (usize, usize) {
212        for row in 0..=occ.len() {
213            for col in 0..=(columns - cs) {
214                let fits = (row..row + rs).all(|r| {
215                    occ.get(r).is_none_or(|cells| !cells[col..col + cs].iter().any(|&o| o))
216                });
217                if fits { return (row, col); }
218            }
219        }
220        (occ.len(), 0) // unreachable — the all-empty `occ.len()` row always fits
221    }
222
223    /// Relative child rects + content height for the non-uniform modes.
224    fn arrange(&self, ctx: &LayoutCtx, width: f32) -> (Vec<Rect>, f32) {
225        match self.mode {
226            GridMode::Staggered => self.arrange_staggered(ctx, width),
227            // Uniform never routes here (kept on its original row-based
228            // path in layout/paint); bento is the only other arm.
229            _ => self.arrange_bento(width),
230        }
231    }
232}
233
234impl Widget for Grid {
235    fn children(&self) -> Children<'_> { Children::Many(&self.children) }
236
237    fn layout(&self, ctx: &LayoutCtx) -> Size {
238        let w = avail_w(ctx.constraints);
239        let h = match self.mode {
240            GridMode::Uniform => self.measure(ctx, w).1,
241            _ => self.arrange(ctx, w).1,
242        };
243        ctx.constraints.constrain(Size { width: w, height: h })
244    }
245
246    fn paint(&self, ctx: &mut PaintCtx) {
247        let r = ctx.rect;
248        if self.mode != GridMode::Uniform {
249            let (rects, _) = self.arrange(
250                &ctx.layout_ctx(Constraints::loose(r.size.width, f32::INFINITY)),
251                r.size.width,
252            );
253            for (child, rel) in self.children.iter().zip(rects) {
254                let rect = Rect {
255                    origin: Point { x: r.origin.x + rel.origin.x, y: r.origin.y + rel.origin.y },
256                    size: rel.size,
257                };
258                child.paint(&mut ctx.child(rect));
259            }
260            return;
261        }
262
263        let cw = self.cell_width(r.size.width);
264        let (sizes, _) = self.measure(&ctx.layout_ctx(Constraints::loose(r.size.width, r.size.height)), r.size.width);
265        let mut y = r.origin.y;
266        let mut i = 0;
267        while i < self.children.len() {
268            let end = (i + self.columns).min(self.children.len());
269            let row_h = sizes[i..end].iter().map(|s| s.height).fold(0.0_f32, f32::max);
270            for (col, idx) in (i..end).enumerate() {
271                let x = r.origin.x + col as f32 * (cw + self.spacing);
272                let rect = Rect { origin: Point { x, y }, size: Size { width: cw, height: row_h } };
273                self.children[idx].paint(&mut ctx.child(rect));
274            }
275            y += row_h + self.run_spacing;
276            i += self.columns;
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    /// A leaf reporting a fixed size regardless of constraints.
286    struct Fixed(f32, f32);
287    impl Widget for Fixed {
288        fn layout(&self, _ctx: &LayoutCtx) -> Size {
289            Size { width: self.0, height: self.1 }
290        }
291        fn paint(&self, _ctx: &mut PaintCtx) {}
292    }
293
294    fn test_env() -> (rosace_render::FontCache, rosace_theme::ThemeData) {
295        (rosace_render::FontCache::embedded(), rosace_theme::built_in::dark_theme())
296    }
297
298    #[test]
299    fn staggered_packs_items_into_the_shortest_column() {
300        // 2 columns, no gaps, 300px wide → 150px cells. Heights 40/100/20/30:
301        // item0 → col0 (y 0), item1 → col1 (y 0), item2 → col0 (y 40,
302        // shortest), item3 → col0 again (y 60; col0 = 60 < col1 = 100).
303        let grid = Grid::new(2)
304            .spacing(0.0)
305            .run_spacing(0.0)
306            .staggered()
307            .child(Fixed(150.0, 40.0))
308            .child(Fixed(150.0, 100.0))
309            .child(Fixed(150.0, 20.0))
310            .child(Fixed(150.0, 30.0));
311        let (font, theme) = test_env();
312        let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
313        let (rects, height) = grid.arrange_staggered(&ctx, 300.0);
314
315        assert_eq!((rects[0].origin.x, rects[0].origin.y), (0.0, 0.0));
316        assert_eq!((rects[1].origin.x, rects[1].origin.y), (150.0, 0.0));
317        assert_eq!((rects[2].origin.x, rects[2].origin.y), (0.0, 40.0));
318        assert_eq!((rects[3].origin.x, rects[3].origin.y), (0.0, 60.0));
319        // Tallest column: col1 at 100 (col0 ends at 90).
320        assert_eq!(height, 100.0);
321        assert_eq!(grid.layout(&ctx).height, 100.0);
322    }
323
324    #[test]
325    fn staggered_children_keep_their_own_heights() {
326        let grid = Grid::new(2)
327            .spacing(0.0)
328            .run_spacing(0.0)
329            .staggered()
330            .child(Fixed(150.0, 40.0))
331            .child(Fixed(150.0, 100.0));
332        let (font, theme) = test_env();
333        let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
334        let (rects, _) = grid.arrange_staggered(&ctx, 300.0);
335        assert_eq!(rects[0].size.height, 40.0);
336        assert_eq!(rects[1].size.height, 100.0);
337    }
338
339    #[test]
340    fn bento_honors_column_and_row_spans() {
341        // 2 columns, no gaps, 200px wide → 100px cells, 50px lattice rows.
342        // item0 spans 2×1 (full first row), item1/item2 fill row 1,
343        // item3 spans 1×2 (rows 2-3, col 0).
344        let grid = Grid::new(2)
345            .spacing(0.0)
346            .run_spacing(0.0)
347            .row_height(50.0)
348            .child_span(Fixed(1.0, 1.0), 2, 1)
349            .child_span(Fixed(1.0, 1.0), 1, 1)
350            .child_span(Fixed(1.0, 1.0), 1, 1)
351            .child_span(Fixed(1.0, 1.0), 1, 2);
352        let (rects, height) = grid.arrange_bento(200.0);
353
354        assert_eq!((rects[0].origin.x, rects[0].origin.y), (0.0, 0.0));
355        assert_eq!((rects[0].size.width, rects[0].size.height), (200.0, 50.0));
356        assert_eq!((rects[1].origin.x, rects[1].origin.y), (0.0, 50.0));
357        assert_eq!((rects[2].origin.x, rects[2].origin.y), (100.0, 50.0));
358        assert_eq!((rects[3].origin.x, rects[3].origin.y), (0.0, 100.0));
359        assert_eq!((rects[3].size.width, rects[3].size.height), (100.0, 100.0));
360        // 4 lattice rows × 50px.
361        assert_eq!(height, 200.0);
362    }
363
364    #[test]
365    fn bento_first_fit_backfills_gaps_beside_tall_items() {
366        // 2 columns: item0 is 1×2 (col 0, rows 0-1); item1 (1×1) must land
367        // beside it at (row 0, col 1), not below it.
368        let grid = Grid::new(2)
369            .spacing(0.0)
370            .run_spacing(0.0)
371            .row_height(50.0)
372            .child_span(Fixed(1.0, 1.0), 1, 2)
373            .child_span(Fixed(1.0, 1.0), 1, 1);
374        let (rects, height) = grid.arrange_bento(200.0);
375        assert_eq!((rects[1].origin.x, rects[1].origin.y), (100.0, 0.0));
376        assert_eq!(height, 100.0);
377    }
378
379    #[test]
380    fn uniform_default_behavior_is_unchanged() {
381        // Regression guard for the Migration Rule: a plain Grid::new still
382        // lays out row-by-row with row height = tallest child.
383        let grid = Grid::new(2)
384            .spacing(0.0)
385            .run_spacing(0.0)
386            .child(Fixed(150.0, 40.0))
387            .child(Fixed(150.0, 100.0))
388            .child(Fixed(150.0, 20.0));
389        let (font, theme) = test_env();
390        let ctx = LayoutCtx::new(Constraints::loose(300.0, 1000.0), &font, &theme);
391        // Row 0 = max(40, 100) = 100; row 1 = 20 → 120 total.
392        assert_eq!(grid.layout(&ctx).height, 120.0);
393    }
394}