1mod 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#[derive(Clone, Debug)]
19pub struct GridProps {
20 pub columns: Vec<Length>,
22 pub rows: Vec<Length>,
24 pub gap_x: u16,
26 pub gap_y: u16,
28 pub padding: Padding,
30 pub style: Style,
32 pub align: Align,
34 pub justify: Justify,
37 pub width: Length,
39 pub height: Length,
41 pub border: bool,
43 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#[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#[derive(Clone)]
80pub struct Grid {
81 pub(crate) props: GridProps,
82 pub(crate) items: Vec<GridItem>,
83}
84
85impl Grid {
86 pub fn new() -> Self {
88 Self {
89 props: GridProps::default(),
90 items: Vec::new(),
91 }
92 }
93
94 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 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 pub fn uniform_columns(mut self, n: usize) -> Self {
108 self.props.columns = vec![Length::Auto; n.max(1)];
109 self
110 }
111
112 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 pub fn gap_x(mut self, gap: u16) -> Self {
121 self.props.gap_x = gap;
122 self
123 }
124
125 pub fn gap_y(mut self, gap: u16) -> Self {
127 self.props.gap_y = gap;
128 self
129 }
130
131 pub fn column_gap(mut self, gap: u16) -> Self {
133 self.props.gap_x = gap;
134 self
135 }
136
137 pub fn row_gap(mut self, gap: u16) -> Self {
139 self.props.gap_y = gap;
140 self
141 }
142
143 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 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 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 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 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
190 self.props.padding = padding.into();
191 self
192 }
193
194 pub fn style(mut self, style: Style) -> Self {
196 self.props.style = style;
197 self
198 }
199
200 pub fn align(mut self, align: Align) -> Self {
202 self.props.align = align;
203 self
204 }
205
206 pub fn justify(mut self, justify: Justify) -> Self {
208 self.props.justify = justify;
209 self
210 }
211
212 pub fn width(mut self, width: Length) -> Self {
214 self.props.width = width;
215 self
216 }
217
218 pub fn height(mut self, height: Length) -> Self {
220 self.props.height = height;
221 self
222 }
223
224 pub fn border(mut self, border: bool) -> Self {
226 self.props.border = border;
227 self
228 }
229
230 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}