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