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