Skip to main content

forme/layout/
grid.rs

1//! # CSS Grid Layout
2//!
3//! Implements a subset of CSS Grid for 2D layouts within the page-native
4//! layout engine. Supports:
5//! - Fixed (pt), fractional (fr), and auto track sizing
6//! - MinMax track sizes
7//! - Explicit grid placement (column/row start/end/span)
8//! - Auto-placement (row-major sparse)
9//! - Row/column gap
10//! - Page breaks at row boundaries
11
12use crate::style::{GridPlacement, GridTrackSize};
13
14/// Resolved grid item placement in the grid.
15#[derive(Debug, Clone)]
16pub struct GridItemPlacement {
17    /// Index of the child node.
18    pub child_index: usize,
19    /// Column start (0-based).
20    pub col_start: usize,
21    /// Column end (exclusive, 0-based).
22    pub col_end: usize,
23    /// Row start (0-based).
24    pub row_start: usize,
25    /// Row end (exclusive, 0-based).
26    pub row_end: usize,
27}
28
29/// Resolve track sizes to concrete widths/heights in points.
30///
31/// Algorithm:
32/// 1. Fixed tracks → exact size
33/// 2. Auto tracks → `content_sizes[i]` (intrinsic content size)
34/// 3. Fr tracks → distribute remaining space proportionally
35/// 4. MinMax → clamp between resolved min and max
36pub fn resolve_tracks(
37    template: &[GridTrackSize],
38    available_space: f64,
39    gap: f64,
40    content_sizes: &[f64],
41) -> Vec<f64> {
42    if template.is_empty() {
43        return vec![];
44    }
45
46    let total_gap = if template.len() > 1 {
47        gap * (template.len() - 1) as f64
48    } else {
49        0.0
50    };
51    let space_after_gaps = (available_space - total_gap).max(0.0);
52
53    let mut sizes = vec![0.0_f64; template.len()];
54    let mut remaining = space_after_gaps;
55    let mut total_fr = 0.0_f64;
56
57    // First pass: resolve fixed and auto tracks
58    for (i, track) in template.iter().enumerate() {
59        match track {
60            GridTrackSize::Pt(pts) => {
61                sizes[i] = *pts;
62                remaining -= pts;
63            }
64            GridTrackSize::Auto => {
65                let content = content_sizes.get(i).copied().unwrap_or(0.0);
66                sizes[i] = content;
67                remaining -= content;
68            }
69            GridTrackSize::Fr(fr) => {
70                total_fr += fr;
71            }
72            GridTrackSize::MinMax(min, max) => {
73                let min_val = resolve_single_track(min, 0.0);
74                let max_val = resolve_single_track(max, space_after_gaps);
75                let content = content_sizes.get(i).copied().unwrap_or(0.0);
76                let val = content.max(min_val).min(max_val);
77                sizes[i] = val;
78                remaining -= val;
79            }
80        }
81    }
82
83    // Second pass: distribute remaining space to fr tracks
84    remaining = remaining.max(0.0);
85    if total_fr > 0.0 {
86        let fr_unit = remaining / total_fr;
87        for (i, track) in template.iter().enumerate() {
88            if let GridTrackSize::Fr(fr) = track {
89                sizes[i] = fr * fr_unit;
90            }
91        }
92    }
93
94    sizes
95}
96
97/// Resolve a single track size to a point value (for MinMax bounds).
98fn resolve_single_track(track: &GridTrackSize, available: f64) -> f64 {
99    match track {
100        GridTrackSize::Pt(pts) => *pts,
101        GridTrackSize::Fr(fr) => fr * available, // approximation
102        GridTrackSize::Auto => 0.0,
103        GridTrackSize::MinMax(min, _) => resolve_single_track(min, available),
104    }
105}
106
107/// Place items in the grid using explicit placement + auto-placement.
108///
109/// Items with explicit `grid_placement` are placed first. Remaining items
110/// fill the grid left-to-right, top-to-bottom (row-major sparse).
111pub fn place_items(
112    placements: &[Option<&GridPlacement>],
113    num_columns: usize,
114) -> Vec<GridItemPlacement> {
115    if num_columns == 0 {
116        return vec![];
117    }
118
119    let num_items = placements.len();
120    let max_rows = num_items.div_ceil(num_columns) + num_items; // generous upper bound
121
122    // Occupancy grid: true = occupied
123    let mut occupied = vec![vec![false; num_columns]; max_rows];
124    let mut result = Vec::with_capacity(num_items);
125
126    // Phase 1: Place explicitly positioned items
127    for (i, placement) in placements.iter().enumerate() {
128        if let Some(gp) = placement {
129            if gp.column_start.is_some() || gp.row_start.is_some() {
130                let col_start = gp
131                    .column_start
132                    .map(|c| (c - 1).max(0) as usize)
133                    .unwrap_or(0);
134                let row_start = gp.row_start.map(|r| (r - 1).max(0) as usize).unwrap_or(0);
135
136                let col_span = if let (Some(cs), Some(ce)) = (gp.column_start, gp.column_end) {
137                    ((ce - cs).max(1)) as usize
138                } else {
139                    gp.column_span.unwrap_or(1) as usize
140                };
141
142                let row_span = if let (Some(rs), Some(re)) = (gp.row_start, gp.row_end) {
143                    ((re - rs).max(1)) as usize
144                } else {
145                    gp.row_span.unwrap_or(1) as usize
146                };
147
148                let col_end = (col_start + col_span).min(num_columns);
149                let row_end = row_start + row_span;
150
151                // Mark cells as occupied
152                for r in row_start..row_end {
153                    for c in col_start..col_end {
154                        if r < occupied.len() && c < num_columns {
155                            occupied[r][c] = true;
156                        }
157                    }
158                }
159
160                result.push(GridItemPlacement {
161                    child_index: i,
162                    col_start,
163                    col_end,
164                    row_start,
165                    row_end,
166                });
167            }
168        }
169    }
170
171    // Phase 2: Auto-place remaining items (row-major sparse)
172    let mut auto_row = 0;
173    let mut auto_col = 0;
174
175    for (i, placement) in placements.iter().enumerate() {
176        let is_explicit = if let Some(gp) = placement {
177            gp.column_start.is_some() || gp.row_start.is_some()
178        } else {
179            false
180        };
181
182        if is_explicit {
183            continue;
184        }
185
186        let col_span = placement.and_then(|gp| gp.column_span).unwrap_or(1) as usize;
187        let row_span = placement.and_then(|gp| gp.row_span).unwrap_or(1) as usize;
188
189        // Find next available slot
190        loop {
191            if auto_col + col_span > num_columns {
192                auto_col = 0;
193                auto_row += 1;
194            }
195
196            // Grow occupancy grid if needed
197            while auto_row + row_span > occupied.len() {
198                occupied.push(vec![false; num_columns]);
199            }
200
201            // Check if slot is free
202            let mut fits = auto_col + col_span <= num_columns;
203            if fits {
204                'check: for row in occupied.iter().skip(auto_row).take(row_span) {
205                    for &cell in row.iter().skip(auto_col).take(col_span) {
206                        if cell {
207                            fits = false;
208                            break 'check;
209                        }
210                    }
211                }
212            }
213
214            if fits {
215                break;
216            }
217
218            auto_col += 1;
219        }
220
221        // Place the item
222        let col_end = auto_col + col_span;
223        let row_end = auto_row + row_span;
224
225        for r in auto_row..row_end {
226            for c in auto_col..col_end {
227                if r < occupied.len() && c < num_columns {
228                    occupied[r][c] = true;
229                }
230            }
231        }
232
233        result.push(GridItemPlacement {
234            child_index: i,
235            col_start: auto_col,
236            col_end,
237            row_start: auto_row,
238            row_end,
239        });
240
241        auto_col = col_end;
242    }
243
244    result
245}
246
247/// Compute the number of rows needed based on item placements.
248pub fn compute_num_rows(placements: &[GridItemPlacement]) -> usize {
249    placements.iter().map(|p| p.row_end).max().unwrap_or(0)
250}
251
252/// Compute the x-offset for a column, accounting for gaps.
253pub fn column_x_offset(col: usize, col_widths: &[f64], gap: f64) -> f64 {
254    let mut x = 0.0;
255    for c in 0..col {
256        x += col_widths.get(c).copied().unwrap_or(0.0);
257        x += gap;
258    }
259    x
260}
261
262/// Compute the width of a multi-column span.
263pub fn span_width(col_start: usize, col_end: usize, col_widths: &[f64], gap: f64) -> f64 {
264    let mut w = 0.0;
265    for c in col_start..col_end {
266        w += col_widths.get(c).copied().unwrap_or(0.0);
267        if c > col_start {
268            w += gap;
269        }
270    }
271    w
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_resolve_tracks_fixed() {
280        let tracks = vec![GridTrackSize::Pt(100.0), GridTrackSize::Pt(200.0)];
281        let sizes = resolve_tracks(&tracks, 400.0, 0.0, &[]);
282        assert_eq!(sizes.len(), 2);
283        assert!((sizes[0] - 100.0).abs() < 0.001);
284        assert!((sizes[1] - 200.0).abs() < 0.001);
285    }
286
287    #[test]
288    fn test_resolve_tracks_fr() {
289        let tracks = vec![
290            GridTrackSize::Pt(100.0),
291            GridTrackSize::Fr(1.0),
292            GridTrackSize::Fr(2.0),
293        ];
294        let sizes = resolve_tracks(&tracks, 400.0, 0.0, &[]);
295        assert_eq!(sizes.len(), 3);
296        assert!((sizes[0] - 100.0).abs() < 0.001);
297        assert!((sizes[1] - 100.0).abs() < 0.001); // 1fr = 300/3 = 100
298        assert!((sizes[2] - 200.0).abs() < 0.001); // 2fr = 300*2/3 = 200
299    }
300
301    #[test]
302    fn test_resolve_tracks_with_gap() {
303        let tracks = vec![GridTrackSize::Fr(1.0), GridTrackSize::Fr(1.0)];
304        let sizes = resolve_tracks(&tracks, 210.0, 10.0, &[]);
305        assert_eq!(sizes.len(), 2);
306        // 210 - 10 (gap) = 200, split equally = 100 each
307        assert!((sizes[0] - 100.0).abs() < 0.001);
308        assert!((sizes[1] - 100.0).abs() < 0.001);
309    }
310
311    #[test]
312    fn test_resolve_tracks_auto() {
313        let tracks = vec![GridTrackSize::Auto, GridTrackSize::Fr(1.0)];
314        let content_sizes = vec![80.0, 0.0];
315        let sizes = resolve_tracks(&tracks, 400.0, 0.0, &content_sizes);
316        assert!((sizes[0] - 80.0).abs() < 0.001);
317        assert!((sizes[1] - 320.0).abs() < 0.001);
318    }
319
320    #[test]
321    fn test_place_items_auto() {
322        // 6 items, 3 columns → 2 rows
323        let placements: Vec<Option<&GridPlacement>> = vec![None; 6];
324        let result = place_items(&placements, 3);
325        assert_eq!(result.len(), 6);
326
327        // First row: items 0, 1, 2
328        assert_eq!(result[0].col_start, 0);
329        assert_eq!(result[0].row_start, 0);
330        assert_eq!(result[1].col_start, 1);
331        assert_eq!(result[1].row_start, 0);
332        assert_eq!(result[2].col_start, 2);
333        assert_eq!(result[2].row_start, 0);
334
335        // Second row: items 3, 4, 5
336        assert_eq!(result[3].col_start, 0);
337        assert_eq!(result[3].row_start, 1);
338        assert_eq!(result[4].col_start, 1);
339        assert_eq!(result[4].row_start, 1);
340        assert_eq!(result[5].col_start, 2);
341        assert_eq!(result[5].row_start, 1);
342    }
343
344    #[test]
345    fn test_place_items_explicit() {
346        let gp = GridPlacement {
347            column_start: Some(2),
348            column_end: None,
349            row_start: Some(1),
350            row_end: None,
351            column_span: None,
352            row_span: None,
353        };
354        let placements: Vec<Option<&GridPlacement>> = vec![Some(&gp), None, None];
355        let result = place_items(&placements, 3);
356        assert_eq!(result.len(), 3);
357
358        // Find the explicitly placed item (child_index 0)
359        let explicit = result.iter().find(|p| p.child_index == 0).unwrap();
360        assert_eq!(explicit.col_start, 1); // column 2 → 0-based index 1
361        assert_eq!(explicit.row_start, 0); // row 1 → 0-based index 0
362    }
363
364    #[test]
365    fn test_place_items_spanning() {
366        let gp = GridPlacement {
367            column_start: None,
368            column_end: None,
369            row_start: None,
370            row_end: None,
371            column_span: Some(2),
372            row_span: None,
373        };
374        let placements: Vec<Option<&GridPlacement>> = vec![Some(&gp), None, None];
375        let result = place_items(&placements, 3);
376
377        let spanning = result.iter().find(|p| p.child_index == 0).unwrap();
378        assert_eq!(spanning.col_start, 0);
379        assert_eq!(spanning.col_end, 2); // spans 2 columns
380    }
381
382    #[test]
383    fn test_span_width() {
384        let widths = vec![100.0, 200.0, 150.0];
385        assert!((span_width(0, 1, &widths, 10.0) - 100.0).abs() < 0.001);
386        assert!((span_width(0, 2, &widths, 10.0) - 310.0).abs() < 0.001); // 100 + 10 + 200
387        assert!((span_width(0, 3, &widths, 10.0) - 470.0).abs() < 0.001); // 100 + 10 + 200 + 10 + 150
388    }
389
390    #[test]
391    fn test_column_x_offset() {
392        let widths = vec![100.0, 200.0, 150.0];
393        assert!((column_x_offset(0, &widths, 10.0) - 0.0).abs() < 0.001);
394        assert!((column_x_offset(1, &widths, 10.0) - 110.0).abs() < 0.001);
395        assert!((column_x_offset(2, &widths, 10.0) - 320.0).abs() < 0.001);
396    }
397}