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 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}