Skip to main content

sequoia_layout/
grid.rs

1use crate::container::Align;
2use crate::dock::sanitize_gap;
3
4#[derive(Clone, Debug, PartialEq)]
5#[non_exhaustive]
6#[derive(Default)]
7pub enum Track {
8    #[default]
9    Fit,
10    Px(f32),
11    Percent(f32),
12    Fill(f32),
13
14    MinMax {
15        min: f32,
16        max: Box<Track>,
17    },
18}
19
20impl Track {
21    pub fn minmax(min: f32, max: Track) -> Self {
22        let min = if min.is_finite() { min.max(0.0) } else { 0.0 };
23
24        let max = match max {
25            Self::MinMax { min: _, max: inner } => *inner,
26            other => other,
27        };
28        Self::MinMax {
29            min,
30            max: Box::new(max),
31        }
32    }
33
34    pub fn is_fill_like(&self) -> bool {
35        match self {
36            Self::Fill(w) => *w > 0.0,
37            Self::MinMax { max, .. } => (**max).is_fill_like(),
38            _ => false,
39        }
40    }
41
42    pub fn fill_weight(&self) -> f32 {
43        match self {
44            Self::Fill(w) if w.is_finite() => w.max(0.0),
45            Self::MinMax { max, .. } => (**max).fill_weight(),
46            _ => 0.0,
47        }
48    }
49
50    pub fn min_floor(&self) -> f32 {
51        match self {
52            Self::MinMax { min, .. } => *min,
53            _ => 0.0,
54        }
55    }
56}
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub struct GridPlacement {
60    pub column: Option<usize>,
61    pub row: Option<usize>,
62    pub col_span: usize,
63    pub row_span: usize,
64}
65
66impl GridPlacement {
67    pub const AUTO: Self = Self {
68        column: None,
69        row: None,
70        col_span: 1,
71        row_span: 1,
72    };
73
74    pub fn new() -> Self {
75        Self::AUTO
76    }
77
78    pub fn at(mut self, column: usize, row: usize) -> Self {
79        self.column = Some(column);
80        self.row = Some(row);
81        self
82    }
83
84    pub fn column(mut self, column: usize) -> Self {
85        self.column = Some(column);
86        self
87    }
88
89    pub fn row(mut self, row: usize) -> Self {
90        self.row = Some(row);
91        self
92    }
93
94    pub fn span(mut self, col_span: usize, row_span: usize) -> Self {
95        self.col_span = col_span.max(1);
96        self.row_span = row_span.max(1);
97        self
98    }
99
100    pub fn col_span(mut self, span: usize) -> Self {
101        self.col_span = span.max(1);
102        self
103    }
104
105    pub fn row_span(mut self, span: usize) -> Self {
106        self.row_span = span.max(1);
107        self
108    }
109
110    pub fn is_auto(self) -> bool {
111        self.column.is_none() && self.row.is_none() && self.col_span <= 1 && self.row_span <= 1
112    }
113}
114
115#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
116pub enum GridFlow {
117    #[default]
118    Row,
119    Column,
120}
121
122#[derive(Clone, Debug, PartialEq)]
123pub struct GridStyle {
124    pub columns: Vec<Track>,
125    pub rows: Vec<Track>,
126    pub column_gap: f32,
127    pub row_gap: f32,
128    pub auto_flow: GridFlow,
129
130    pub align_items: Align,
131    pub justify_items: Align,
132}
133
134impl Default for GridStyle {
135    fn default() -> Self {
136        Self {
137            columns: Vec::new(),
138            rows: Vec::new(),
139            column_gap: 0.0,
140            row_gap: 0.0,
141            auto_flow: GridFlow::Row,
142            align_items: Align::Stretch,
143            justify_items: Align::Stretch,
144        }
145    }
146}
147
148impl GridStyle {
149    pub fn new(columns: impl IntoIterator<Item = Track>) -> Self {
150        Self {
151            columns: columns.into_iter().collect(),
152            ..Self::default()
153        }
154    }
155
156    pub fn rows(mut self, rows: impl IntoIterator<Item = Track>) -> Self {
157        self.rows = rows.into_iter().collect();
158        self
159    }
160
161    pub fn gap(mut self, gap: f32) -> Self {
162        let gap = sanitize_gap(gap);
163        self.column_gap = gap;
164        self.row_gap = gap;
165        self
166    }
167
168    pub fn column_gap(mut self, gap: f32) -> Self {
169        self.column_gap = sanitize_gap(gap);
170        self
171    }
172
173    pub fn row_gap(mut self, gap: f32) -> Self {
174        self.row_gap = sanitize_gap(gap);
175        self
176    }
177
178    pub fn auto_flow(mut self, flow: GridFlow) -> Self {
179        self.auto_flow = flow;
180        self
181    }
182
183    pub fn column_flow(mut self) -> Self {
184        self.auto_flow = GridFlow::Column;
185        self
186    }
187
188    pub fn align_items(mut self, align: Align) -> Self {
189        self.align_items = align;
190        self
191    }
192
193    pub fn justify_items(mut self, align: Align) -> Self {
194        self.justify_items = align;
195        self
196    }
197
198    pub fn place_items(mut self, align: Align) -> Self {
199        self.align_items = align;
200        self.justify_items = align;
201        self
202    }
203}
204
205fn resolve_single(track: &Track, content: f32, available: f32) -> (f32, f32) {
206    let content = if content.is_finite() {
207        content.max(0.0)
208    } else {
209        0.0
210    };
211    match track {
212        Track::Px(v) => (if v.is_finite() { v.max(0.0) } else { content }, 0.0),
213        Track::Percent(p) => {
214            if available.is_finite() && p.is_finite() {
215                ((available * p / 100.0).max(0.0), 0.0)
216            } else {
217                (content, 0.0)
218            }
219        }
220        Track::Fit => (content, 0.0),
221        Track::Fill(w) => (0.0, if w.is_finite() { w.max(0.0) } else { 0.0 }),
222        Track::MinMax { min, max } => {
223            let (fixed, fill) = resolve_single(max, content, available);
224
225            if fill > 0.0 {
226                (min.max(fixed).max(0.0), fill)
227            } else {
228                (fixed.max(*min).max(0.0), 0.0)
229            }
230        }
231    }
232}
233
234pub fn resolve_tracks(tracks: &[Track], content: &[f32], available: f32, gap: f32) -> Vec<f32> {
235    resolve_tracks_clamped(tracks, content, available, gap, None, None)
236}
237
238pub fn resolve_tracks_clamped(
239    tracks: &[Track],
240    content: &[f32],
241    available: f32,
242    gap: f32,
243    mins: Option<&[f32]>,
244    maxs: Option<&[f32]>,
245) -> Vec<f32> {
246    let gap = sanitize_gap(gap);
247    let gaps = gap * tracks.len().saturating_sub(1) as f32;
248    let mut sizes = vec![0.0; tracks.len()];
249    let mut fill_weight_sum = 0.0;
250
251    for (i, track) in tracks.iter().enumerate() {
252        let c = content.get(i).copied().unwrap_or(0.0);
253        let (fixed, fill) = resolve_single(track, c, available);
254
255        let floor = track.min_floor();
256        let mut size = fixed.max(floor);
257        if let Track::MinMax { min, .. } = track {
258            size = size.max(*min).max(0.0);
259
260            let _ = c;
261        }
262        if let Some(mns) = mins {
263            let mn = mns.get(i).copied().unwrap_or(0.0);
264            let mn = if mn.is_finite() { mn.max(0.0) } else { 0.0 };
265            size = size.max(mn);
266        }
267        if let Some(mxs) = maxs {
268            let mx = mxs.get(i).copied().unwrap_or(f32::INFINITY);
269            let mx = if mx.is_nan() {
270                f32::INFINITY
271            } else if mx.is_infinite() {
272                if mx > 0.0 { f32::INFINITY } else { 0.0 }
273            } else {
274                mx.max(0.0)
275            };
276            size = size.min(mx);
277        }
278        sizes[i] = size.max(0.0);
279        fill_weight_sum += fill;
280    }
281
282    if fill_weight_sum > 0.0 {
283        let max_of = |i: usize| -> Option<f32> {
284            maxs.and_then(|m| m.get(i).copied()).map(|mx| {
285                if mx.is_nan() {
286                    f32::INFINITY
287                } else if mx.is_infinite() {
288                    if mx > 0.0 { f32::INFINITY } else { 0.0 }
289                } else {
290                    mx.max(0.0)
291                }
292            })
293        };
294        let capped = |i: usize, size: f32| max_of(i).is_some_and(|mx| size >= mx - 0.005);
295
296        if available.is_finite() {
297            // Fill tracks share the free space; a track that reaches its max
298            // stops taking space so the rest is handed to its siblings.
299            for _ in 0..tracks.len().max(1) {
300                let used: f32 = sizes.iter().sum::<f32>() + gaps;
301                let free = available - used;
302                if free <= 0.005 {
303                    break;
304                }
305                let active: f32 = tracks
306                    .iter()
307                    .enumerate()
308                    .filter(|(i, t)| t.fill_weight() > 0.0 && !capped(*i, sizes[*i]))
309                    .map(|(_, t)| t.fill_weight())
310                    .sum();
311                if active <= 0.0 {
312                    break;
313                }
314                for (i, track) in tracks.iter().enumerate() {
315                    let w = track.fill_weight();
316                    if w <= 0.0 || capped(i, sizes[i]) {
317                        continue;
318                    }
319                    let floor = sizes[i];
320                    let mut v = floor + free * w / active;
321                    if let Some(mx) = max_of(i) {
322                        v = v.min(mx.max(floor));
323                    }
324                    sizes[i] = v.max(floor).max(0.0);
325                }
326            }
327        } else {
328            for (i, track) in tracks.iter().enumerate() {
329                let w = track.fill_weight();
330                if w > 0.0 {
331                    let floor = sizes[i];
332                    let extra = content.get(i).copied().unwrap_or(0.0).max(0.0).max(floor) - floor;
333                    sizes[i] = (floor + extra.max(0.0)).max(floor).max(0.0);
334                }
335            }
336        }
337    }
338
339    sizes
340}
341
342pub fn auto_cells(count: usize, ncols: usize, flow: GridFlow) -> Vec<(usize, usize)> {
343    let ncols = ncols.max(1);
344    (0..count)
345        .map(|i| match flow {
346            GridFlow::Row => (i % ncols, i / ncols),
347            GridFlow::Column => {
348                let nrows = count.div_ceil(ncols).max(1);
349                (i / nrows, i % nrows)
350            }
351        })
352        .collect()
353}
354
355pub fn track_offsets(sizes: &[f32], gap: f32) -> Vec<f32> {
356    let mut offsets = Vec::with_capacity(sizes.len());
357    let mut cursor = 0.0;
358    for &size in sizes {
359        offsets.push(cursor);
360        cursor += size + gap;
361    }
362    offsets
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    fn approx(a: f32, b: f32) -> bool {
370        (a - b).abs() < 0.01
371    }
372
373    #[test]
374    fn px_and_auto_tracks_take_fixed_and_content() {
375        let sizes = resolve_tracks(
376            &[Track::Px(100.0), Track::Fit, Track::Px(50.0)],
377            &[0.0, 70.0, 0.0],
378            400.0,
379            0.0,
380        );
381        assert_eq!(sizes.len(), 3);
382        assert!(approx(sizes[0], 100.0));
383        assert!(approx(sizes[1], 70.0));
384        assert!(approx(sizes[2], 50.0));
385    }
386
387    #[test]
388    fn fill_tracks_split_remaining_space() {
389        let sizes = resolve_tracks(
390            &[Track::Px(100.0), Track::Fill(1.0), Track::Fill(3.0)],
391            &[0.0; 3],
392            400.0,
393            0.0,
394        );
395        assert!(approx(sizes[0], 100.0));
396        assert!(approx(sizes[1], 75.0));
397        assert!(approx(sizes[2], 225.0));
398    }
399
400    #[test]
401    fn fill_respects_gaps_and_overflow_clamps_to_zero() {
402        let with_gap = resolve_tracks(
403            &[Track::Fill(1.0), Track::Fill(1.0)],
404            &[0.0; 2],
405            110.0,
406            10.0,
407        );
408        assert!(approx(with_gap[0], 50.0));
409        assert!(approx(with_gap[1], 50.0));
410
411        let overflow = resolve_tracks(&[Track::Px(300.0), Track::Fill(2.0)], &[0.0; 2], 100.0, 0.0);
412        assert!(approx(overflow[0], 300.0));
413        assert!(approx(overflow[1], 0.0));
414    }
415
416    #[test]
417    fn unbounded_axis_falls_back_to_content() {
418        let sizes = resolve_tracks(
419            &[Track::Percent(50.0), Track::Fill(1.0), Track::Px(20.0)],
420            &[80.0, 40.0, 0.0],
421            f32::INFINITY,
422            0.0,
423        );
424        assert!(approx(sizes[0], 80.0));
425        assert!(approx(sizes[1], 40.0));
426        assert!(approx(sizes[2], 20.0));
427    }
428
429    #[test]
430    fn track_offsets_place_tracks_after_gaps() {
431        let offsets = track_offsets(&[100.0, 50.0, 30.0], 10.0);
432        assert!(approx(offsets[0], 0.0));
433        assert!(approx(offsets[1], 110.0));
434        assert!(approx(offsets[2], 170.0));
435    }
436}