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#[must_use]
115#[allow(dead_code)]
116pub(crate) fn distribute_flex(items: &[FlexItem], sizes: &[f32], available: f32) -> Vec<f32> {
117    if items.is_empty() {
118        return Vec::new();
119    }
120
121    // UX-107: Collapsed items keep size 0 and don't participate in flex distribution
122    let collapsed: Vec<bool> = items
123        .iter()
124        .zip(sizes.iter())
125        .map(|(item, &size)| item.collapse_if_empty && size == 0.0)
126        .collect();
127
128    // Calculate total size excluding collapsed items
129    let total_size: f32 = sizes
130        .iter()
131        .zip(collapsed.iter())
132        .filter(|(_, &is_collapsed)| !is_collapsed)
133        .map(|(&s, _)| s)
134        .sum();
135
136    let remaining = available - total_size;
137
138    if remaining.abs() < 0.001 {
139        return sizes.to_vec();
140    }
141
142    if remaining > 0.0 {
143        // Grow items (only non-collapsed items participate)
144        let total_grow: f32 = items
145            .iter()
146            .zip(collapsed.iter())
147            .filter(|(_, &is_collapsed)| !is_collapsed)
148            .map(|(i, _)| i.grow)
149            .sum();
150
151        if total_grow > 0.0 {
152            return sizes
153                .iter()
154                .zip(items.iter())
155                .zip(collapsed.iter())
156                .map(|((&size, item), &is_collapsed)| {
157                    if is_collapsed {
158                        0.0
159                    } else {
160                        size + (remaining * item.grow / total_grow)
161                    }
162                })
163                .collect();
164        }
165    } else {
166        // Shrink items (only non-collapsed items participate)
167        let total_shrink: f32 = items
168            .iter()
169            .zip(collapsed.iter())
170            .filter(|(_, &is_collapsed)| !is_collapsed)
171            .map(|(i, _)| i.shrink)
172            .sum();
173
174        if total_shrink > 0.0 {
175            return sizes
176                .iter()
177                .zip(items.iter())
178                .zip(collapsed.iter())
179                .map(|((&size, item), &is_collapsed)| {
180                    if is_collapsed {
181                        0.0
182                    } else {
183                        (size + (remaining * item.shrink / total_shrink)).max(0.0)
184                    }
185                })
186                .collect();
187        }
188    }
189
190    // Keep collapsed items at 0
191    sizes
192        .iter()
193        .zip(collapsed.iter())
194        .map(|(&size, &is_collapsed)| if is_collapsed { 0.0 } else { size })
195        .collect()
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_flex_direction_default() {
204        assert_eq!(FlexDirection::default(), FlexDirection::Row);
205    }
206
207    #[test]
208    fn test_flex_justify_default() {
209        assert_eq!(FlexJustify::default(), FlexJustify::Start);
210    }
211
212    #[test]
213    fn test_flex_align_default() {
214        assert_eq!(FlexAlign::default(), FlexAlign::Center);
215    }
216
217    #[test]
218    fn test_flex_item_builder() {
219        let item = FlexItem::new()
220            .grow(1.0)
221            .shrink(0.0)
222            .basis(100.0)
223            .align_self(FlexAlign::Start);
224
225        assert_eq!(item.grow, 1.0);
226        assert_eq!(item.shrink, 0.0);
227        assert_eq!(item.basis, Some(100.0));
228        assert_eq!(item.align_self, Some(FlexAlign::Start));
229    }
230
231    #[test]
232    fn test_distribute_flex_empty() {
233        let result = distribute_flex(&[], &[], 100.0);
234        assert!(result.is_empty());
235    }
236
237    #[test]
238    fn test_distribute_flex_exact_fit() {
239        let items = vec![FlexItem::new(), FlexItem::new()];
240        let sizes = vec![50.0, 50.0];
241        let result = distribute_flex(&items, &sizes, 100.0);
242        assert_eq!(result, vec![50.0, 50.0]);
243    }
244
245    #[test]
246    fn test_distribute_flex_grow() {
247        let items = vec![FlexItem::new().grow(1.0), FlexItem::new().grow(1.0)];
248        let sizes = vec![25.0, 25.0];
249        let result = distribute_flex(&items, &sizes, 100.0);
250        assert_eq!(result, vec![50.0, 50.0]);
251    }
252
253    #[test]
254    fn test_distribute_flex_grow_uneven() {
255        let items = vec![FlexItem::new().grow(1.0), FlexItem::new().grow(3.0)];
256        let sizes = vec![0.0, 0.0];
257        let result = distribute_flex(&items, &sizes, 100.0);
258        assert_eq!(result, vec![25.0, 75.0]);
259    }
260
261    #[test]
262    fn test_distribute_flex_shrink() {
263        let items = vec![FlexItem::new().shrink(1.0), FlexItem::new().shrink(1.0)];
264        let sizes = vec![75.0, 75.0];
265        let result = distribute_flex(&items, &sizes, 100.0);
266        assert_eq!(result, vec![50.0, 50.0]);
267    }
268
269    // =========================================================================
270    // FlexDirection Tests
271    // =========================================================================
272
273    #[test]
274    fn test_flex_direction_clone() {
275        let dir = FlexDirection::Column;
276        let cloned = dir;
277        assert_eq!(dir, cloned);
278    }
279
280    #[test]
281    fn test_flex_direction_all_variants() {
282        assert_eq!(FlexDirection::Row, FlexDirection::Row);
283        assert_eq!(FlexDirection::RowReverse, FlexDirection::RowReverse);
284        assert_eq!(FlexDirection::Column, FlexDirection::Column);
285        assert_eq!(FlexDirection::ColumnReverse, FlexDirection::ColumnReverse);
286    }
287
288    #[test]
289    fn test_flex_direction_debug() {
290        let dir = FlexDirection::Row;
291        let debug = format!("{:?}", dir);
292        assert!(debug.contains("Row"));
293    }
294
295    // =========================================================================
296    // FlexJustify Tests
297    // =========================================================================
298
299    #[test]
300    fn test_flex_justify_all_variants() {
301        assert_eq!(FlexJustify::Start, FlexJustify::Start);
302        assert_eq!(FlexJustify::End, FlexJustify::End);
303        assert_eq!(FlexJustify::Center, FlexJustify::Center);
304        assert_eq!(FlexJustify::SpaceBetween, FlexJustify::SpaceBetween);
305        assert_eq!(FlexJustify::SpaceAround, FlexJustify::SpaceAround);
306        assert_eq!(FlexJustify::SpaceEvenly, FlexJustify::SpaceEvenly);
307    }
308
309    #[test]
310    fn test_flex_justify_clone() {
311        let justify = FlexJustify::SpaceBetween;
312        let cloned = justify;
313        assert_eq!(justify, cloned);
314    }
315
316    #[test]
317    fn test_flex_justify_debug() {
318        let justify = FlexJustify::Center;
319        let debug = format!("{:?}", justify);
320        assert!(debug.contains("Center"));
321    }
322
323    // =========================================================================
324    // FlexAlign Tests
325    // =========================================================================
326
327    #[test]
328    fn test_flex_align_all_variants() {
329        assert_eq!(FlexAlign::Start, FlexAlign::Start);
330        assert_eq!(FlexAlign::End, FlexAlign::End);
331        assert_eq!(FlexAlign::Center, FlexAlign::Center);
332        assert_eq!(FlexAlign::Stretch, FlexAlign::Stretch);
333        assert_eq!(FlexAlign::Baseline, FlexAlign::Baseline);
334    }
335
336    #[test]
337    fn test_flex_align_clone() {
338        let align = FlexAlign::Stretch;
339        let cloned = align;
340        assert_eq!(align, cloned);
341    }
342
343    #[test]
344    fn test_flex_align_debug() {
345        let align = FlexAlign::Baseline;
346        let debug = format!("{:?}", align);
347        assert!(debug.contains("Baseline"));
348    }
349
350    // =========================================================================
351    // FlexItem Tests
352    // =========================================================================
353
354    #[test]
355    fn test_flex_item_default() {
356        let item = FlexItem::default();
357        assert_eq!(item.grow, 0.0);
358        assert_eq!(item.shrink, 0.0);
359        assert_eq!(item.basis, None);
360        assert_eq!(item.align_self, None);
361    }
362
363    #[test]
364    fn test_flex_item_new() {
365        let item = FlexItem::new();
366        assert_eq!(item.grow, 0.0);
367        assert_eq!(item.shrink, 0.0);
368    }
369
370    #[test]
371    fn test_flex_item_grow_only() {
372        let item = FlexItem::new().grow(2.5);
373        assert_eq!(item.grow, 2.5);
374        assert_eq!(item.shrink, 0.0);
375    }
376
377    #[test]
378    fn test_flex_item_shrink_only() {
379        let item = FlexItem::new().shrink(0.5);
380        assert_eq!(item.shrink, 0.5);
381        assert_eq!(item.grow, 0.0);
382    }
383
384    #[test]
385    fn test_flex_item_basis_only() {
386        let item = FlexItem::new().basis(200.0);
387        assert_eq!(item.basis, Some(200.0));
388    }
389
390    #[test]
391    fn test_flex_item_align_self_only() {
392        let item = FlexItem::new().align_self(FlexAlign::End);
393        assert_eq!(item.align_self, Some(FlexAlign::End));
394    }
395
396    #[test]
397    fn test_flex_item_clone() {
398        let item = FlexItem::new().grow(1.0).shrink(0.5);
399        let cloned = item;
400        assert_eq!(item.grow, cloned.grow);
401        assert_eq!(item.shrink, cloned.shrink);
402    }
403
404    #[test]
405    fn test_flex_item_debug() {
406        let item = FlexItem::new().grow(1.0);
407        let debug = format!("{:?}", item);
408        assert!(debug.contains("FlexItem"));
409    }
410
411    // =========================================================================
412    // distribute_flex Tests
413    // =========================================================================
414
415    #[test]
416    fn test_distribute_flex_no_grow_no_shrink() {
417        let items = vec![FlexItem::new(), FlexItem::new()];
418        let sizes = vec![30.0, 30.0];
419        let result = distribute_flex(&items, &sizes, 100.0);
420        // No grow factor, so sizes remain unchanged
421        assert_eq!(result, vec![30.0, 30.0]);
422    }
423
424    #[test]
425    fn test_distribute_flex_single_item_grow() {
426        let items = vec![FlexItem::new().grow(1.0)];
427        let sizes = vec![50.0];
428        let result = distribute_flex(&items, &sizes, 100.0);
429        assert_eq!(result, vec![100.0]);
430    }
431
432    #[test]
433    fn test_distribute_flex_single_item_shrink() {
434        let items = vec![FlexItem::new().shrink(1.0)];
435        let sizes = vec![150.0];
436        let result = distribute_flex(&items, &sizes, 100.0);
437        assert_eq!(result, vec![100.0]);
438    }
439
440    #[test]
441    fn test_distribute_flex_shrink_uneven() {
442        let items = vec![FlexItem::new().shrink(1.0), FlexItem::new().shrink(3.0)];
443        let sizes = vec![100.0, 100.0];
444        let result = distribute_flex(&items, &sizes, 100.0);
445        // Total: 200, need to shrink by 100
446        // item1: 100 - 100 * 1/4 = 75
447        // item2: 100 - 100 * 3/4 = 25
448        assert_eq!(result, vec![75.0, 25.0]);
449    }
450
451    #[test]
452    fn test_distribute_flex_shrink_to_zero() {
453        let items = vec![FlexItem::new().shrink(1.0)];
454        let sizes = vec![50.0];
455        // Need to shrink more than available
456        let result = distribute_flex(&items, &sizes, 0.0);
457        assert_eq!(result, vec![0.0]); // Can't go below 0
458    }
459
460    #[test]
461    fn test_distribute_flex_mixed_grow() {
462        let items = vec![
463            FlexItem::new().grow(0.0), // Won't grow
464            FlexItem::new().grow(1.0), // Will take all remaining
465        ];
466        let sizes = vec![50.0, 0.0];
467        let result = distribute_flex(&items, &sizes, 100.0);
468        assert_eq!(result, vec![50.0, 50.0]);
469    }
470
471    #[test]
472    fn test_distribute_flex_three_items() {
473        let items = vec![
474            FlexItem::new().grow(1.0),
475            FlexItem::new().grow(2.0),
476            FlexItem::new().grow(1.0),
477        ];
478        let sizes = vec![0.0, 0.0, 0.0];
479        let result = distribute_flex(&items, &sizes, 100.0);
480        assert_eq!(result, vec![25.0, 50.0, 25.0]);
481    }
482
483    #[test]
484    fn test_distribute_flex_near_exact_fit() {
485        let items = vec![FlexItem::new().grow(1.0), FlexItem::new().grow(1.0)];
486        let sizes = vec![49.9995, 50.0005];
487        let result = distribute_flex(&items, &sizes, 100.0);
488        // Should be treated as exact fit (within 0.001 tolerance)
489        assert_eq!(result, vec![49.9995, 50.0005]);
490    }
491
492    // =========================================================================
493    // UX-107: collapse_if_empty Tests
494    // =========================================================================
495
496    #[test]
497    fn test_flex_item_collapse_if_empty() {
498        let item = FlexItem::new().collapse_if_empty();
499        assert!(item.collapse_if_empty);
500    }
501
502    #[test]
503    fn test_flex_item_collapse_if_empty_default_false() {
504        let item = FlexItem::new();
505        assert!(!item.collapse_if_empty);
506    }
507
508    #[test]
509    fn test_distribute_flex_collapsed_item_stays_zero() {
510        let items = vec![
511            FlexItem::new().grow(1.0).collapse_if_empty(),
512            FlexItem::new().grow(1.0),
513        ];
514        let sizes = vec![0.0, 50.0]; // First item empty (collapsed), second has content
515        let result = distribute_flex(&items, &sizes, 100.0);
516        // Collapsed item stays 0, second item gets all the extra space
517        assert_eq!(result, vec![0.0, 100.0]);
518    }
519
520    #[test]
521    fn test_distribute_flex_collapsed_doesnt_participate_in_grow() {
522        let items = vec![
523            FlexItem::new().grow(1.0).collapse_if_empty(),
524            FlexItem::new().grow(1.0),
525            FlexItem::new().grow(1.0),
526        ];
527        let sizes = vec![0.0, 25.0, 25.0]; // First collapsed, others have content
528        let result = distribute_flex(&items, &sizes, 100.0);
529        // Collapsed stays 0, remaining 50 is split evenly between items 2 and 3
530        assert_eq!(result, vec![0.0, 50.0, 50.0]);
531    }
532
533    #[test]
534    fn test_distribute_flex_collapsed_with_size_not_collapsed() {
535        // collapse_if_empty only collapses if size is 0
536        let items = vec![
537            FlexItem::new().grow(1.0).collapse_if_empty(),
538            FlexItem::new().grow(1.0),
539        ];
540        let sizes = vec![30.0, 30.0]; // Both have content, collapse flag doesn't apply
541        let result = distribute_flex(&items, &sizes, 100.0);
542        // Both participate in grow
543        assert_eq!(result, vec![50.0, 50.0]);
544    }
545
546    #[test]
547    fn test_distribute_flex_all_collapsed() {
548        let items = vec![
549            FlexItem::new().grow(1.0).collapse_if_empty(),
550            FlexItem::new().grow(1.0).collapse_if_empty(),
551        ];
552        let sizes = vec![0.0, 0.0]; // Both empty
553        let result = distribute_flex(&items, &sizes, 100.0);
554        // Both stay at 0
555        assert_eq!(result, vec![0.0, 0.0]);
556    }
557
558    #[test]
559    fn test_distribute_flex_collapsed_in_shrink() {
560        let items = vec![
561            FlexItem::new().shrink(1.0).collapse_if_empty(),
562            FlexItem::new().shrink(1.0),
563        ];
564        let sizes = vec![0.0, 120.0]; // First empty, second needs shrinking
565        let result = distribute_flex(&items, &sizes, 100.0);
566        // Collapsed stays 0, second shrinks to fit
567        assert_eq!(result, vec![0.0, 100.0]);
568    }
569}