Skip to main content

cssbox_core/
grid.rs

1//! CSS Grid Layout algorithm.
2//!
3//! Implements CSS Grid Layout Module Level 2.
4//! Reference: https://www.w3.org/TR/css-grid-2/
5
6use 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
13/// Layout a grid container and its items.
14pub 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    // Resolve container box model
24    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    // §7: Define the explicit grid
52    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    // Collect grid items
58    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    // §8: Place items onto the grid
82    let num_explicit_cols = explicit_cols.len().max(1);
83    let num_explicit_rows = explicit_rows.len().max(1);
84
85    // Auto-placement
86    let mut grid_col_count = num_explicit_cols;
87    let mut grid_row_count = num_explicit_rows;
88
89    // First pass: place explicitly positioned items
90    for item in &mut items {
91        if item.col_start.is_none() && item.col_end.is_none() {
92            continue; // auto-placed
93        }
94        // Resolve explicit positions
95        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    // Second pass: auto-place remaining items
110    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; // already placed
120        }
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    // §12: Track sizing algorithm
151    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    // Compute track positions
178    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    // Layout and position each item
193    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        // Calculate item area size
211        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        // Apply alignment
230        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)), // CSS lines are 1-based
280        GridPlacement::Span(_) => None,                  // simplified
281        GridPlacement::Auto => None,
282        GridPlacement::Named(_) => None,
283    }
284}
285
286/// §12: Track sizing algorithm (simplified).
287fn 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    // Initialize track sizes from definitions
312    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); // placeholder
333                total_fr += fr;
334            }
335            TrackSizingFunction::Auto => {
336                sizes.push(0.0); // placeholder
337                auto_count += 1;
338            }
339            TrackSizingFunction::MinContent | TrackSizingFunction::MaxContent => {
340                // Simplified: treat as auto
341                sizes.push(0.0);
342                auto_count += 1;
343            }
344            TrackSizingFunction::MinMax(min, _max) => {
345                // Simplified: use min as base
346                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    // Distribute remaining space to fr tracks and auto tracks
358    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        // Auto tracks get minimum
375        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        // 4 items for 2x2 grid
478        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        // Each column should be 400px wide (800 / 2)
493        assert!((r0.width - 400.0).abs() < 1.0);
494        assert!((r1.width - 400.0).abs() < 1.0);
495
496        // Positions
497        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        // (820 - 20 gap) / 2 = 400 each
542        assert!((r0.width - 400.0).abs() < 1.0);
543        assert!((r1.x - 420.0).abs() < 1.0); // 400 + 20 gap
544    }
545}