1use rosace_core::types::{Point, Rect, Size};
2use rosace_layout::Constraints;
3use super::{Widget, Children, LayoutCtx, PaintCtx, BoxedWidget, avail_w};
4
5#[derive(Clone, Copy, PartialEq, Eq, Default)]
11enum GridMode {
12 #[default]
15 Uniform,
16 Staggered,
19 Bento,
23}
24
25const DEFAULT_BENTO_ROW_HEIGHT: f32 = 96.0;
27
28pub struct Grid {
38 columns: usize,
39 spacing: f32,
40 run_spacing: f32,
41 children: Vec<BoxedWidget>,
42 spans: Vec<(u16, u16)>,
45 mode: GridMode,
46 row_height: f32,
48}
49
50impl Grid {
51 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 pub fn spacing(mut self, s: f32) -> Self { self.spacing = s; self }
65 pub fn run_spacing(mut self, s: f32) -> Self { self.run_spacing = s; self }
67 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 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 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 pub fn staggered(mut self) -> Self { self.mode = GridMode::Staggered; self }
93
94 pub fn bento(mut self) -> Self { self.mode = GridMode::Bento; self }
100
101 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 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 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 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 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 fn arrange_bento(&self, width: f32) -> (Vec<Rect>, f32) {
170 let cw = self.cell_width(width);
171 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 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 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) }
222
223 fn arrange(&self, ctx: &LayoutCtx, width: f32) -> (Vec<Rect>, f32) {
225 match self.mode {
226 GridMode::Staggered => self.arrange_staggered(ctx, width),
227 _ => 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 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 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 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 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 assert_eq!(height, 200.0);
362 }
363
364 #[test]
365 fn bento_first_fit_backfills_gaps_beside_tall_items() {
366 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 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 assert_eq!(grid.layout(&ctx).height, 120.0);
393 }
394}