1use crate::box_model::BoxModel;
7use crate::fragment::{Fragment, FragmentKind};
8use crate::geometry::{Point, Size};
9use crate::layout::{self, LayoutContext};
10use crate::style::*;
11use crate::tree::NodeId;
12
13pub fn layout_grid(
15 ctx: &LayoutContext,
16 node: NodeId,
17 containing_block_width: f32,
18 containing_block_height: f32,
19) -> Fragment {
20 let style = ctx.tree.style(node);
21 let mut fragment = Fragment::new(node, FragmentKind::Box);
22
23 let border = BoxModel::resolve_border(style);
25 let padding = BoxModel::resolve_padding(style, containing_block_width);
26 let margin = BoxModel::resolve_margin(style, containing_block_width);
27
28 fragment.border = border;
29 fragment.padding = padding;
30 fragment.margin = margin;
31
32 let content_width = match style.width.resolve(containing_block_width) {
33 Some(mut w) => {
34 if style.box_sizing == BoxSizing::BorderBox {
35 w = (w - border.horizontal() - padding.horizontal()).max(0.0);
36 }
37 w
38 }
39 None => (containing_block_width
40 - border.horizontal()
41 - padding.horizontal()
42 - margin.horizontal())
43 .max(0.0),
44 };
45
46 let content_height_available = style
47 .height
48 .resolve(containing_block_height)
49 .unwrap_or(containing_block_height);
50
51 let explicit_cols = &style.grid_template_columns;
53 let explicit_rows = &style.grid_template_rows;
54 let col_gap = style.column_gap;
55 let row_gap = style.row_gap;
56
57 let children = ctx.tree.children(node);
59 let mut items: Vec<GridItem> = Vec::new();
60
61 for &child_id in children {
62 let child_style = ctx.tree.style(child_id);
63 if child_style.display.is_none() {
64 continue;
65 }
66 if child_style.position.is_absolutely_positioned() {
67 let child_frag =
68 layout::layout_node(ctx, child_id, content_width, content_height_available);
69 fragment.children.push(child_frag);
70 continue;
71 }
72 items.push(GridItem {
73 node: child_id,
74 row_start: resolve_grid_line(&child_style.grid_row_start),
75 row_end: resolve_grid_line(&child_style.grid_row_end),
76 col_start: resolve_grid_line(&child_style.grid_column_start),
77 col_end: resolve_grid_line(&child_style.grid_column_end),
78 });
79 }
80
81 let num_explicit_cols = explicit_cols.len().max(1);
83 let num_explicit_rows = explicit_rows.len().max(1);
84
85 let mut grid_col_count = num_explicit_cols;
87 let mut grid_row_count = num_explicit_rows;
88
89 for item in &mut items {
91 if item.col_start.is_none() && item.col_end.is_none() {
92 continue; }
94 let cs = item.col_start.unwrap_or(0);
96 let ce = item.col_end.unwrap_or(cs + 1);
97 let rs = item.row_start.unwrap_or(0);
98 let re = item.row_end.unwrap_or(rs + 1);
99
100 item.col_start = Some(cs);
101 item.col_end = Some(ce);
102 item.row_start = Some(rs);
103 item.row_end = Some(re);
104
105 grid_col_count = grid_col_count.max(ce as usize);
106 grid_row_count = grid_row_count.max(re as usize);
107 }
108
109 let mut auto_cursor_row = 0i32;
111 let mut auto_cursor_col = 0i32;
112 let is_row_flow = matches!(
113 style.grid_auto_flow,
114 GridAutoFlow::Row | GridAutoFlow::RowDense
115 );
116
117 for item in &mut items {
118 if item.col_start.is_some() && item.row_start.is_some() {
119 continue; }
121
122 if is_row_flow {
123 item.col_start = Some(auto_cursor_col);
124 item.col_end = Some(auto_cursor_col + 1);
125 item.row_start = Some(auto_cursor_row);
126 item.row_end = Some(auto_cursor_row + 1);
127
128 auto_cursor_col += 1;
129 if auto_cursor_col >= grid_col_count as i32 {
130 auto_cursor_col = 0;
131 auto_cursor_row += 1;
132 }
133 } else {
134 item.row_start = Some(auto_cursor_row);
135 item.row_end = Some(auto_cursor_row + 1);
136 item.col_start = Some(auto_cursor_col);
137 item.col_end = Some(auto_cursor_col + 1);
138
139 auto_cursor_row += 1;
140 if auto_cursor_row >= grid_row_count as i32 {
141 auto_cursor_row = 0;
142 auto_cursor_col += 1;
143 }
144 }
145
146 grid_col_count = grid_col_count.max(item.col_end.unwrap() as usize);
147 grid_row_count = grid_row_count.max(item.row_end.unwrap() as usize);
148 }
149
150 let col_sizes = size_tracks(
152 explicit_cols,
153 &style.grid_auto_columns,
154 grid_col_count,
155 content_width,
156 col_gap,
157 &items,
158 ctx,
159 true,
160 content_width,
161 content_height_available,
162 );
163
164 let row_sizes = size_tracks(
165 explicit_rows,
166 &style.grid_auto_rows,
167 grid_row_count,
168 content_height_available,
169 row_gap,
170 &items,
171 ctx,
172 false,
173 content_width,
174 content_height_available,
175 );
176
177 let col_positions = track_positions(&col_sizes, col_gap);
179 let row_positions = track_positions(&row_sizes, row_gap);
180
181 let _total_col_size = if col_sizes.is_empty() {
182 0.0
183 } else {
184 *col_positions.last().unwrap() + *col_sizes.last().unwrap()
185 };
186 let total_row_size = if row_sizes.is_empty() {
187 0.0
188 } else {
189 *row_positions.last().unwrap() + *row_sizes.last().unwrap()
190 };
191
192 for item in &items {
194 let cs = item.col_start.unwrap() as usize;
195 let ce = item.col_end.unwrap() as usize;
196 let rs = item.row_start.unwrap() as usize;
197 let re = item.row_end.unwrap() as usize;
198
199 let x = if cs < col_positions.len() {
200 col_positions[cs]
201 } else {
202 0.0
203 };
204 let y = if rs < row_positions.len() {
205 row_positions[rs]
206 } else {
207 0.0
208 };
209
210 let mut item_width = 0.0f32;
212 for c in cs..ce.min(col_sizes.len()) {
213 item_width += col_sizes[c];
214 if c > cs {
215 item_width += col_gap;
216 }
217 }
218
219 let mut item_height = 0.0f32;
220 for r in rs..re.min(row_sizes.len()) {
221 item_height += row_sizes[r];
222 if r > rs {
223 item_height += row_gap;
224 }
225 }
226
227 let mut child_frag = layout::layout_node(ctx, item.node, item_width, item_height);
228
229 let child_style = ctx.tree.style(item.node);
231 let align = effective_align_items(style.align_items, child_style.align_self);
232 let justify = style.justify_content;
233
234 let actual_w = child_frag.border_box().width;
235 let actual_h = child_frag.border_box().height;
236
237 let dx = match justify {
238 JustifyContent::Center => (item_width - actual_w) / 2.0,
239 JustifyContent::End | JustifyContent::FlexEnd => item_width - actual_w,
240 _ => 0.0,
241 };
242 let dy = match align {
243 AlignItems::Center => (item_height - actual_h) / 2.0,
244 AlignItems::End | AlignItems::FlexEnd => item_height - actual_h,
245 _ => 0.0,
246 };
247
248 child_frag.position = Point::new(
249 x + dx + child_frag.margin.left,
250 y + dy + child_frag.margin.top,
251 );
252 fragment.children.push(child_frag);
253 }
254
255 let final_height = style
256 .height
257 .resolve(containing_block_height)
258 .unwrap_or(total_row_size);
259 let min_h = style.min_height.resolve(containing_block_height);
260 let max_h = style
261 .max_height
262 .resolve(containing_block_height)
263 .unwrap_or(f32::INFINITY);
264
265 fragment.size = Size::new(content_width, final_height.max(min_h).min(max_h));
266 fragment
267}
268
269struct GridItem {
270 node: NodeId,
271 row_start: Option<i32>,
272 row_end: Option<i32>,
273 col_start: Option<i32>,
274 col_end: Option<i32>,
275}
276
277fn resolve_grid_line(placement: &GridPlacement) -> Option<i32> {
278 match placement {
279 GridPlacement::Line(n) => Some((*n - 1).max(0)), GridPlacement::Span(_) => None, GridPlacement::Auto => None,
282 GridPlacement::Named(_) => None,
283 }
284}
285
286fn size_tracks(
288 explicit: &[TrackDefinition],
289 auto_tracks: &[TrackSizingFunction],
290 track_count: usize,
291 available: f32,
292 gap: f32,
293 _items: &[GridItem],
294 _ctx: &LayoutContext,
295 _is_column: bool,
296 _containing_width: f32,
297 _containing_height: f32,
298) -> Vec<f32> {
299 let total_gaps = if track_count > 1 {
300 gap * (track_count - 1) as f32
301 } else {
302 0.0
303 };
304 let available_for_tracks = available - total_gaps;
305
306 let mut sizes = Vec::with_capacity(track_count);
307 let mut total_fixed = 0.0f32;
308 let mut total_fr = 0.0f32;
309 let mut auto_count = 0usize;
310
311 for i in 0..track_count {
313 let sizing = if i < explicit.len() {
314 &explicit[i].sizing
315 } else if !auto_tracks.is_empty() {
316 &auto_tracks[i % auto_tracks.len()]
317 } else {
318 &TrackSizingFunction::Auto
319 };
320
321 match sizing {
322 TrackSizingFunction::Length(px) => {
323 sizes.push(*px);
324 total_fixed += px;
325 }
326 TrackSizingFunction::Percentage(pct) => {
327 let s = available_for_tracks * pct;
328 sizes.push(s);
329 total_fixed += s;
330 }
331 TrackSizingFunction::Fr(fr) => {
332 sizes.push(0.0); total_fr += fr;
334 }
335 TrackSizingFunction::Auto => {
336 sizes.push(0.0); auto_count += 1;
338 }
339 TrackSizingFunction::MinContent | TrackSizingFunction::MaxContent => {
340 sizes.push(0.0);
342 auto_count += 1;
343 }
344 TrackSizingFunction::MinMax(min, _max) => {
345 let min_val = track_fn_to_px(min, available_for_tracks);
347 sizes.push(min_val);
348 total_fixed += min_val;
349 }
350 TrackSizingFunction::FitContent(_limit) => {
351 sizes.push(0.0);
352 auto_count += 1;
353 }
354 }
355 }
356
357 let remaining = (available_for_tracks - total_fixed).max(0.0);
359
360 if total_fr > 0.0 {
361 let per_fr = remaining / total_fr;
362 for i in 0..track_count {
363 let sizing = if i < explicit.len() {
364 &explicit[i].sizing
365 } else if !auto_tracks.is_empty() {
366 &auto_tracks[i % auto_tracks.len()]
367 } else {
368 &TrackSizingFunction::Auto
369 };
370 if let TrackSizingFunction::Fr(fr) = sizing {
371 sizes[i] = per_fr * fr;
372 }
373 }
374 let auto_min = 0.0;
376 for i in 0..track_count {
377 let sizing = if i < explicit.len() {
378 &explicit[i].sizing
379 } else if !auto_tracks.is_empty() {
380 &auto_tracks[i % auto_tracks.len()]
381 } else {
382 &TrackSizingFunction::Auto
383 };
384 if matches!(
385 sizing,
386 TrackSizingFunction::Auto
387 | TrackSizingFunction::MinContent
388 | TrackSizingFunction::MaxContent
389 | TrackSizingFunction::FitContent(_)
390 ) {
391 sizes[i] = auto_min;
392 }
393 }
394 } else if auto_count > 0 {
395 let per_auto = remaining / auto_count as f32;
396 for i in 0..track_count {
397 let sizing = if i < explicit.len() {
398 &explicit[i].sizing
399 } else if !auto_tracks.is_empty() {
400 &auto_tracks[i % auto_tracks.len()]
401 } else {
402 &TrackSizingFunction::Auto
403 };
404 if matches!(
405 sizing,
406 TrackSizingFunction::Auto
407 | TrackSizingFunction::MinContent
408 | TrackSizingFunction::MaxContent
409 | TrackSizingFunction::FitContent(_)
410 ) {
411 sizes[i] = per_auto;
412 }
413 }
414 }
415
416 sizes
417}
418
419fn track_fn_to_px(func: &TrackSizingFunction, available: f32) -> f32 {
420 match func {
421 TrackSizingFunction::Length(px) => *px,
422 TrackSizingFunction::Percentage(pct) => available * pct,
423 TrackSizingFunction::Auto => 0.0,
424 TrackSizingFunction::MinContent => 0.0,
425 TrackSizingFunction::MaxContent => available,
426 TrackSizingFunction::Fr(_) => 0.0,
427 TrackSizingFunction::MinMax(min, _) => track_fn_to_px(min, available),
428 TrackSizingFunction::FitContent(limit) => *limit,
429 }
430}
431
432fn track_positions(sizes: &[f32], gap: f32) -> Vec<f32> {
433 let mut positions = Vec::with_capacity(sizes.len());
434 let mut pos = 0.0f32;
435 for &size in sizes.iter() {
436 positions.push(pos);
437 pos += size + gap;
438 }
439 positions
440}
441
442fn effective_align_items(container: AlignItems, item: AlignSelf) -> AlignItems {
443 match item {
444 AlignSelf::Auto => container,
445 AlignSelf::Stretch => AlignItems::Stretch,
446 AlignSelf::FlexStart | AlignSelf::Start => AlignItems::Start,
447 AlignSelf::FlexEnd | AlignSelf::End => AlignItems::End,
448 AlignSelf::Center => AlignItems::Center,
449 AlignSelf::Baseline => AlignItems::Baseline,
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use crate::layout::{compute_layout, FixedWidthTextMeasure};
457 use crate::style::ComputedStyle;
458 use crate::tree::BoxTreeBuilder;
459
460 #[test]
461 fn test_grid_basic_2x2() {
462 let mut builder = BoxTreeBuilder::new();
463 let mut root_style = ComputedStyle {
464 display: Display::GRID,
465 ..ComputedStyle::block()
466 };
467 root_style.grid_template_columns = vec![
468 TrackDefinition::new(TrackSizingFunction::Fr(1.0)),
469 TrackDefinition::new(TrackSizingFunction::Fr(1.0)),
470 ];
471 root_style.grid_template_rows = vec![
472 TrackDefinition::new(TrackSizingFunction::Length(50.0)),
473 TrackDefinition::new(TrackSizingFunction::Length(50.0)),
474 ];
475 let root = builder.root(root_style);
476
477 for _ in 0..4 {
479 let child_style = ComputedStyle::block();
480 builder.element(root, child_style);
481 }
482
483 let tree = builder.build();
484 let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
485
486 let children = tree.children(tree.root());
487 let r0 = result.bounding_rect(children[0]).unwrap();
488 let r1 = result.bounding_rect(children[1]).unwrap();
489 let r2 = result.bounding_rect(children[2]).unwrap();
490 let r3 = result.bounding_rect(children[3]).unwrap();
491
492 assert!((r0.width - 400.0).abs() < 1.0);
494 assert!((r1.width - 400.0).abs() < 1.0);
495
496 assert!((r0.x - 0.0).abs() < 1.0);
498 assert!((r1.x - 400.0).abs() < 1.0);
499 assert!((r2.x - 0.0).abs() < 1.0);
500 assert!((r3.x - 400.0).abs() < 1.0);
501
502 assert!((r0.y - 0.0).abs() < 1.0);
503 assert!((r1.y - 0.0).abs() < 1.0);
504 assert!((r2.y - 50.0).abs() < 1.0);
505 assert!((r3.y - 50.0).abs() < 1.0);
506 }
507
508 #[test]
509 fn test_track_positions() {
510 let sizes = vec![100.0, 200.0, 150.0];
511 let positions = track_positions(&sizes, 10.0);
512 assert_eq!(positions, vec![0.0, 110.0, 320.0]);
513 }
514
515 #[test]
516 fn test_grid_with_gap() {
517 let mut builder = BoxTreeBuilder::new();
518 let mut root_style = ComputedStyle {
519 display: Display::GRID,
520 ..ComputedStyle::block()
521 };
522 root_style.grid_template_columns = vec![
523 TrackDefinition::new(TrackSizingFunction::Fr(1.0)),
524 TrackDefinition::new(TrackSizingFunction::Fr(1.0)),
525 ];
526 root_style.grid_template_rows =
527 vec![TrackDefinition::new(TrackSizingFunction::Length(50.0))];
528 root_style.column_gap = 20.0;
529 let root = builder.root(root_style);
530
531 builder.element(root, ComputedStyle::block());
532 builder.element(root, ComputedStyle::block());
533
534 let tree = builder.build();
535 let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(820.0, 600.0));
536
537 let children = tree.children(tree.root());
538 let r0 = result.bounding_rect(children[0]).unwrap();
539 let r1 = result.bounding_rect(children[1]).unwrap();
540
541 assert!((r0.width - 400.0).abs() < 1.0);
543 assert!((r1.x - 420.0).abs() < 1.0); }
545}