Skip to main content

presentar_layout/
flex.rs

1//! Flexbox layout types.
2
3use serde::{Deserialize, Serialize};
4
5/// Direction for flex layout.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7pub enum FlexDirection {
8    /// Horizontal (left to right)
9    #[default]
10    Row,
11    /// Horizontal (right to left)
12    RowReverse,
13    /// Vertical (top to bottom)
14    Column,
15    /// Vertical (bottom to top)
16    ColumnReverse,
17}
18
19/// Main axis alignment for flex layout.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
21pub enum FlexJustify {
22    /// Pack items at the start
23    #[default]
24    Start,
25    /// Pack items at the end
26    End,
27    /// Center items
28    Center,
29    /// Distribute space evenly between items
30    SpaceBetween,
31    /// Distribute space evenly around items
32    SpaceAround,
33    /// Distribute space evenly, including edges
34    SpaceEvenly,
35}
36
37/// Cross axis alignment for flex layout.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
39pub enum FlexAlign {
40    /// Align to the start
41    Start,
42    /// Align to the end
43    End,
44    /// Center items
45    #[default]
46    Center,
47    /// Stretch to fill
48    Stretch,
49    /// Align to baseline
50    Baseline,
51}
52
53/// Flex item properties.
54#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
55pub struct FlexItem {
56    /// Flex grow factor
57    pub grow: f32,
58    /// Flex shrink factor
59    pub shrink: f32,
60    /// Flex basis (initial size)
61    pub basis: Option<f32>,
62    /// Self alignment override
63    pub align_self: Option<FlexAlign>,
64    /// UX-107: Collapse to zero size when content is empty.
65    /// When true, items with no content will have 0 size in layout.
66    pub collapse_if_empty: bool,
67}
68
69impl FlexItem {
70    /// Create a new flex item with default values.
71    #[must_use]
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Set the grow factor.
77    #[must_use]
78    pub const fn grow(mut self, grow: f32) -> Self {
79        self.grow = grow;
80        self
81    }
82
83    /// Set the shrink factor.
84    #[must_use]
85    pub const fn shrink(mut self, shrink: f32) -> Self {
86        self.shrink = shrink;
87        self
88    }
89
90    /// Set the basis.
91    #[must_use]
92    pub const fn basis(mut self, basis: f32) -> Self {
93        self.basis = Some(basis);
94        self
95    }
96
97    /// Set self alignment.
98    #[must_use]
99    pub const fn align_self(mut self, align: FlexAlign) -> Self {
100        self.align_self = Some(align);
101        self
102    }
103
104    /// UX-107: Enable auto-collapse when content is empty.
105    #[must_use]
106    pub const fn collapse_if_empty(mut self) -> Self {
107        self.collapse_if_empty = true;
108        self
109    }
110}
111
112/// Distribute available space among flex items.
113/// UX-107: Items with `collapse_if_empty=true` and size=0 are excluded from distribution.
114/// Compute which items are collapsed (empty and collapse_if_empty=true).
115fn compute_collapsed(items: &[FlexItem], sizes: &[f32]) -> Vec<bool> {
116    items
117        .iter()
118        .zip(sizes.iter())
119        .map(|(item, &size)| item.collapse_if_empty && size == 0.0)
120        .collect()
121}
122
123/// Sum a flex factor (grow or shrink) for non-collapsed items.
124fn sum_flex_factor(items: &[FlexItem], collapsed: &[bool], get_factor: fn(&FlexItem) -> f32) -> f32 {
125    items
126        .iter()
127        .zip(collapsed.iter())
128        .filter(|(_, &is_collapsed)| !is_collapsed)
129        .map(|(item, _)| get_factor(item))
130        .sum()
131}
132
133/// Apply flex adjustment to sizes.
134fn apply_flex_adjustment(
135    sizes: &[f32],
136    items: &[FlexItem],
137    collapsed: &[bool],
138    remaining: f32,
139    total_factor: f32,
140    get_factor: fn(&FlexItem) -> f32,
141    clamp: bool,
142) -> Vec<f32> {
143    sizes
144        .iter()
145        .zip(items.iter())
146        .zip(collapsed.iter())
147        .map(|((&size, item), &is_collapsed)| {
148            if is_collapsed {
149                0.0
150            } else {
151                let adjusted = size + (remaining * get_factor(item) / total_factor);
152                if clamp { adjusted.max(0.0) } else { adjusted }
153            }
154        })
155        .collect()
156}
157
158#[must_use]
159#[allow(dead_code)]
160pub(crate) fn distribute_flex(items: &[FlexItem], sizes: &[f32], available: f32) -> Vec<f32> {
161    if items.is_empty() {
162        return Vec::new();
163    }
164
165    // UX-107: Collapsed items keep size 0 and don't participate in flex distribution
166    let collapsed = compute_collapsed(items, sizes);
167
168    let total_size: f32 = sizes
169        .iter()
170        .zip(collapsed.iter())
171        .filter(|(_, &is_collapsed)| !is_collapsed)
172        .map(|(&s, _)| s)
173        .sum();
174
175    let remaining = available - total_size;
176
177    if remaining.abs() < 0.001 {
178        return sizes.to_vec();
179    }
180
181    let get_grow: fn(&FlexItem) -> f32 = |i| i.grow;
182    let get_shrink: fn(&FlexItem) -> f32 = |i| i.shrink;
183
184    if remaining > 0.0 {
185        let total_grow = sum_flex_factor(items, &collapsed, get_grow);
186        if total_grow > 0.0 {
187            return apply_flex_adjustment(sizes, items, &collapsed, remaining, total_grow, get_grow, false);
188        }
189    } else {
190        let total_shrink = sum_flex_factor(items, &collapsed, get_shrink);
191        if total_shrink > 0.0 {
192            return apply_flex_adjustment(sizes, items, &collapsed, remaining, total_shrink, get_shrink, true);
193        }
194    }
195
196    // Keep collapsed items at 0
197    sizes
198        .iter()
199        .zip(collapsed.iter())
200        .map(|(&size, &is_collapsed)| if is_collapsed { 0.0 } else { size })
201        .collect()
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_flex_direction_default() {
210        assert_eq!(FlexDirection::default(), FlexDirection::Row);
211    }
212
213    #[test]
214    fn test_flex_justify_default() {
215        assert_eq!(FlexJustify::default(), FlexJustify::Start);
216    }
217
218    #[test]
219    fn test_flex_align_default() {
220        assert_eq!(FlexAlign::default(), FlexAlign::Center);
221    }
222
223    #[test]
224    fn test_flex_item_builder() {
225        let item = FlexItem::new()
226            .grow(1.0)
227            .shrink(0.0)
228            .basis(100.0)
229            .align_self(FlexAlign::Start);
230
231        assert_eq!(item.grow, 1.0);
232        assert_eq!(item.shrink, 0.0);
233        assert_eq!(item.basis, Some(100.0));
234        assert_eq!(item.align_self, Some(FlexAlign::Start));
235    }
236
237    #[test]
238    fn test_distribute_flex_empty() {
239        let result = distribute_flex(&[], &[], 100.0);
240        assert!(result.is_empty());
241    }
242
243    #[test]
244    fn test_distribute_flex_exact_fit() {
245        let items = vec![FlexItem::new(), FlexItem::new()];
246        let sizes = vec![50.0, 50.0];
247        let result = distribute_flex(&items, &sizes, 100.0);
248        assert_eq!(result, vec![50.0, 50.0]);
249    }
250
251    #[test]
252    fn test_distribute_flex_grow() {
253        let items = vec![FlexItem::new().grow(1.0), FlexItem::new().grow(1.0)];
254        let sizes = vec![25.0, 25.0];
255        let result = distribute_flex(&items, &sizes, 100.0);
256        assert_eq!(result, vec![50.0, 50.0]);
257    }
258
259    #[test]
260    fn test_distribute_flex_grow_uneven() {
261        let items = vec![FlexItem::new().grow(1.0), FlexItem::new().grow(3.0)];
262        let sizes = vec![0.0, 0.0];
263        let result = distribute_flex(&items, &sizes, 100.0);
264        assert_eq!(result, vec![25.0, 75.0]);
265    }
266
267    #[test]
268    fn test_distribute_flex_shrink() {
269        let items = vec![FlexItem::new().shrink(1.0), FlexItem::new().shrink(1.0)];
270        let sizes = vec![75.0, 75.0];
271        let result = distribute_flex(&items, &sizes, 100.0);
272        assert_eq!(result, vec![50.0, 50.0]);
273    }
274
275    // =========================================================================
276    // FlexDirection Tests
277    // =========================================================================
278
279    #[test]
280    fn test_flex_direction_clone() {
281        let dir = FlexDirection::Column;
282        let cloned = dir;
283        assert_eq!(dir, cloned);
284    }
285
286    #[test]
287    fn test_flex_direction_all_variants() {
288        assert_eq!(FlexDirection::Row, FlexDirection::Row);
289        assert_eq!(FlexDirection::RowReverse, FlexDirection::RowReverse);
290        assert_eq!(FlexDirection::Column, FlexDirection::Column);
291        assert_eq!(FlexDirection::ColumnReverse, FlexDirection::ColumnReverse);
292    }
293
294    #[test]
295    fn test_flex_direction_debug() {
296        let dir = FlexDirection::Row;
297        let debug = format!("{:?}", dir);
298        assert!(debug.contains("Row"));
299    }
300
301    // =========================================================================
302    // FlexJustify Tests
303    // =========================================================================
304
305    #[test]
306    fn test_flex_justify_all_variants() {
307        assert_eq!(FlexJustify::Start, FlexJustify::Start);
308        assert_eq!(FlexJustify::End, FlexJustify::End);
309        assert_eq!(FlexJustify::Center, FlexJustify::Center);
310        assert_eq!(FlexJustify::SpaceBetween, FlexJustify::SpaceBetween);
311        assert_eq!(FlexJustify::SpaceAround, FlexJustify::SpaceAround);
312        assert_eq!(FlexJustify::SpaceEvenly, FlexJustify::SpaceEvenly);
313    }
314
315    #[test]
316    fn test_flex_justify_clone() {
317        let justify = FlexJustify::SpaceBetween;
318        let cloned = justify;
319        assert_eq!(justify, cloned);
320    }
321
322    #[test]
323    fn test_flex_justify_debug() {
324        let justify = FlexJustify::Center;
325        let debug = format!("{:?}", justify);
326        assert!(debug.contains("Center"));
327    }
328
329    // =========================================================================
330    // FlexAlign Tests
331    // =========================================================================
332
333    #[test]
334    fn test_flex_align_all_variants() {
335        assert_eq!(FlexAlign::Start, FlexAlign::Start);
336        assert_eq!(FlexAlign::End, FlexAlign::End);
337        assert_eq!(FlexAlign::Center, FlexAlign::Center);
338        assert_eq!(FlexAlign::Stretch, FlexAlign::Stretch);
339        assert_eq!(FlexAlign::Baseline, FlexAlign::Baseline);
340    }
341
342    #[test]
343    fn test_flex_align_clone() {
344        let align = FlexAlign::Stretch;
345        let cloned = align;
346        assert_eq!(align, cloned);
347    }
348
349    #[test]
350    fn test_flex_align_debug() {
351        let align = FlexAlign::Baseline;
352        let debug = format!("{:?}", align);
353        assert!(debug.contains("Baseline"));
354    }
355
356    // =========================================================================
357    // FlexItem Tests
358    // =========================================================================
359
360    #[test]
361    fn test_flex_item_default() {
362        let item = FlexItem::default();
363        assert_eq!(item.grow, 0.0);
364        assert_eq!(item.shrink, 0.0);
365        assert_eq!(item.basis, None);
366        assert_eq!(item.align_self, None);
367    }
368
369    #[test]
370    fn test_flex_item_new() {
371        let item = FlexItem::new();
372        assert_eq!(item.grow, 0.0);
373        assert_eq!(item.shrink, 0.0);
374    }
375
376    #[test]
377    fn test_flex_item_grow_only() {
378        let item = FlexItem::new().grow(2.5);
379        assert_eq!(item.grow, 2.5);
380        assert_eq!(item.shrink, 0.0);
381    }
382
383    #[test]
384    fn test_flex_item_shrink_only() {
385        let item = FlexItem::new().shrink(0.5);
386        assert_eq!(item.shrink, 0.5);
387        assert_eq!(item.grow, 0.0);
388    }
389
390    #[test]
391    fn test_flex_item_basis_only() {
392        let item = FlexItem::new().basis(200.0);
393        assert_eq!(item.basis, Some(200.0));
394    }
395
396    #[test]
397    fn test_flex_item_align_self_only() {
398        let item = FlexItem::new().align_self(FlexAlign::End);
399        assert_eq!(item.align_self, Some(FlexAlign::End));
400    }
401
402    #[test]
403    fn test_flex_item_clone() {
404        let item = FlexItem::new().grow(1.0).shrink(0.5);
405        let cloned = item;
406        assert_eq!(item.grow, cloned.grow);
407        assert_eq!(item.shrink, cloned.shrink);
408    }
409
410    #[test]
411    fn test_flex_item_debug() {
412        let item = FlexItem::new().grow(1.0);
413        let debug = format!("{:?}", item);
414        assert!(debug.contains("FlexItem"));
415    }
416
417    // =========================================================================
418    // distribute_flex Tests
419    // =========================================================================
420
421    #[test]
422    fn test_distribute_flex_no_grow_no_shrink() {
423        let items = vec![FlexItem::new(), FlexItem::new()];
424        let sizes = vec![30.0, 30.0];
425        let result = distribute_flex(&items, &sizes, 100.0);
426        // No grow factor, so sizes remain unchanged
427        assert_eq!(result, vec![30.0, 30.0]);
428    }
429
430    #[test]
431    fn test_distribute_flex_single_item_grow() {
432        let items = vec![FlexItem::new().grow(1.0)];
433        let sizes = vec![50.0];
434        let result = distribute_flex(&items, &sizes, 100.0);
435        assert_eq!(result, vec![100.0]);
436    }
437
438    #[test]
439    fn test_distribute_flex_single_item_shrink() {
440        let items = vec![FlexItem::new().shrink(1.0)];
441        let sizes = vec![150.0];
442        let result = distribute_flex(&items, &sizes, 100.0);
443        assert_eq!(result, vec![100.0]);
444    }
445
446    #[test]
447    fn test_distribute_flex_shrink_uneven() {
448        let items = vec![FlexItem::new().shrink(1.0), FlexItem::new().shrink(3.0)];
449        let sizes = vec![100.0, 100.0];
450        let result = distribute_flex(&items, &sizes, 100.0);
451        // Total: 200, need to shrink by 100
452        // item1: 100 - 100 * 1/4 = 75
453        // item2: 100 - 100 * 3/4 = 25
454        assert_eq!(result, vec![75.0, 25.0]);
455    }
456
457    #[test]
458    fn test_distribute_flex_shrink_to_zero() {
459        let items = vec![FlexItem::new().shrink(1.0)];
460        let sizes = vec![50.0];
461        // Need to shrink more than available
462        let result = distribute_flex(&items, &sizes, 0.0);
463        assert_eq!(result, vec![0.0]); // Can't go below 0
464    }
465
466    #[test]
467    fn test_distribute_flex_mixed_grow() {
468        let items = vec![
469            FlexItem::new().grow(0.0), // Won't grow
470            FlexItem::new().grow(1.0), // Will take all remaining
471        ];
472        let sizes = vec![50.0, 0.0];
473        let result = distribute_flex(&items, &sizes, 100.0);
474        assert_eq!(result, vec![50.0, 50.0]);
475    }
476
477    #[test]
478    fn test_distribute_flex_three_items() {
479        let items = vec![
480            FlexItem::new().grow(1.0),
481            FlexItem::new().grow(2.0),
482            FlexItem::new().grow(1.0),
483        ];
484        let sizes = vec![0.0, 0.0, 0.0];
485        let result = distribute_flex(&items, &sizes, 100.0);
486        assert_eq!(result, vec![25.0, 50.0, 25.0]);
487    }
488
489    #[test]
490    fn test_distribute_flex_near_exact_fit() {
491        let items = vec![FlexItem::new().grow(1.0), FlexItem::new().grow(1.0)];
492        let sizes = vec![49.9995, 50.0005];
493        let result = distribute_flex(&items, &sizes, 100.0);
494        // Should be treated as exact fit (within 0.001 tolerance)
495        assert_eq!(result, vec![49.9995, 50.0005]);
496    }
497
498    // =========================================================================
499    // UX-107: collapse_if_empty Tests
500    // =========================================================================
501
502    #[test]
503    fn test_flex_item_collapse_if_empty() {
504        let item = FlexItem::new().collapse_if_empty();
505        assert!(item.collapse_if_empty);
506    }
507
508    #[test]
509    fn test_flex_item_collapse_if_empty_default_false() {
510        let item = FlexItem::new();
511        assert!(!item.collapse_if_empty);
512    }
513
514    #[test]
515    fn test_distribute_flex_collapsed_item_stays_zero() {
516        let items = vec![
517            FlexItem::new().grow(1.0).collapse_if_empty(),
518            FlexItem::new().grow(1.0),
519        ];
520        let sizes = vec![0.0, 50.0]; // First item empty (collapsed), second has content
521        let result = distribute_flex(&items, &sizes, 100.0);
522        // Collapsed item stays 0, second item gets all the extra space
523        assert_eq!(result, vec![0.0, 100.0]);
524    }
525
526    #[test]
527    fn test_distribute_flex_collapsed_doesnt_participate_in_grow() {
528        let items = vec![
529            FlexItem::new().grow(1.0).collapse_if_empty(),
530            FlexItem::new().grow(1.0),
531            FlexItem::new().grow(1.0),
532        ];
533        let sizes = vec![0.0, 25.0, 25.0]; // First collapsed, others have content
534        let result = distribute_flex(&items, &sizes, 100.0);
535        // Collapsed stays 0, remaining 50 is split evenly between items 2 and 3
536        assert_eq!(result, vec![0.0, 50.0, 50.0]);
537    }
538
539    #[test]
540    fn test_distribute_flex_collapsed_with_size_not_collapsed() {
541        // collapse_if_empty only collapses if size is 0
542        let items = vec![
543            FlexItem::new().grow(1.0).collapse_if_empty(),
544            FlexItem::new().grow(1.0),
545        ];
546        let sizes = vec![30.0, 30.0]; // Both have content, collapse flag doesn't apply
547        let result = distribute_flex(&items, &sizes, 100.0);
548        // Both participate in grow
549        assert_eq!(result, vec![50.0, 50.0]);
550    }
551
552    #[test]
553    fn test_distribute_flex_all_collapsed() {
554        let items = vec![
555            FlexItem::new().grow(1.0).collapse_if_empty(),
556            FlexItem::new().grow(1.0).collapse_if_empty(),
557        ];
558        let sizes = vec![0.0, 0.0]; // Both empty
559        let result = distribute_flex(&items, &sizes, 100.0);
560        // Both stay at 0
561        assert_eq!(result, vec![0.0, 0.0]);
562    }
563
564    #[test]
565    fn test_distribute_flex_collapsed_in_shrink() {
566        let items = vec![
567            FlexItem::new().shrink(1.0).collapse_if_empty(),
568            FlexItem::new().shrink(1.0),
569        ];
570        let sizes = vec![0.0, 120.0]; // First empty, second needs shrinking
571        let result = distribute_flex(&items, &sizes, 100.0);
572        // Collapsed stays 0, second shrinks to fit
573        assert_eq!(result, vec![0.0, 100.0]);
574    }
575}