Skip to main content

forme/layout/
flex.rs

1//! # Flex Layout Utilities
2//!
3//! Helper functions for the flexbox algorithm. The main flex logic lives
4//! in the layout engine's `layout_flex_row` method. This module provides
5//! the lower-level distribution calculations.
6
7/// Distribute remaining space among items based on flex-grow factors.
8pub fn distribute_grow(items: &mut [(f64, f64)], remaining: f64) {
9    // items: [(current_width, flex_grow)]
10    let total_grow: f64 = items.iter().map(|(_, g)| g).sum();
11    if total_grow <= 0.0 || remaining <= 0.0 {
12        return;
13    }
14    for (width, grow) in items.iter_mut() {
15        *width += remaining * (*grow / total_grow);
16    }
17}
18
19/// A single line of items in a wrapping flex row.
20#[derive(Debug, Clone)]
21pub struct WrapLine {
22    /// Index of the first item in this line.
23    pub start: usize,
24    /// One past the last item (exclusive end).
25    pub end: usize,
26}
27
28/// Partition items into wrap lines based on available width.
29/// Always adds at least one item per line (prevents infinite loops on oversized items).
30pub fn partition_into_lines(
31    base_widths: &[f64],
32    column_gap: f64,
33    available_width: f64,
34) -> Vec<WrapLine> {
35    if base_widths.is_empty() {
36        return vec![];
37    }
38
39    let mut lines = Vec::new();
40    let mut line_start = 0;
41    let mut line_width = 0.0;
42
43    for (i, &w) in base_widths.iter().enumerate() {
44        let needed = if i == line_start { w } else { column_gap + w };
45        if i > line_start && line_width + needed > available_width {
46            lines.push(WrapLine {
47                start: line_start,
48                end: i,
49            });
50            line_start = i;
51            line_width = w;
52        } else {
53            line_width += needed;
54        }
55    }
56
57    // Close the last line
58    if line_start < base_widths.len() {
59        lines.push(WrapLine {
60            start: line_start,
61            end: base_widths.len(),
62        });
63    }
64
65    lines
66}
67
68/// Shrink items to fit within available space based on flex-shrink factors.
69pub fn distribute_shrink(items: &mut [(f64, f64)], overflow: f64) {
70    // items: [(current_width, flex_shrink)]
71    let total_shrink_weighted: f64 = items.iter().map(|(w, s)| w * s).sum();
72    if total_shrink_weighted <= 0.0 || overflow >= 0.0 {
73        return;
74    }
75    let overflow = overflow.abs();
76    for (width, shrink) in items.iter_mut() {
77        let factor = (*width * *shrink) / total_shrink_weighted;
78        *width -= overflow * factor;
79        *width = width.max(0.0);
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_grow_distribution() {
89        let mut items = vec![(100.0, 1.0), (100.0, 2.0)];
90        distribute_grow(&mut items, 90.0);
91        assert!((items[0].0 - 130.0).abs() < 0.01);
92        assert!((items[1].0 - 160.0).abs() < 0.01);
93    }
94
95    #[test]
96    fn test_shrink_distribution() {
97        let mut items = vec![(200.0, 1.0), (100.0, 1.0)];
98        distribute_shrink(&mut items, -60.0);
99        // 200 gets shrunk more because it's wider
100        assert!(items[0].0 < 200.0);
101        assert!(items[1].0 < 100.0);
102        assert!((items[0].0 + items[1].0 - 240.0).abs() < 0.01);
103    }
104
105    #[test]
106    fn test_partition_single_line_fits() {
107        let widths = vec![100.0, 100.0, 100.0];
108        let lines = partition_into_lines(&widths, 10.0, 400.0);
109        assert_eq!(lines.len(), 1);
110        assert_eq!(lines[0].start, 0);
111        assert_eq!(lines[0].end, 3);
112    }
113
114    #[test]
115    fn test_partition_two_line_split() {
116        // 3 items × 100pt + 2 gaps × 10pt = 320pt; available = 250pt
117        let widths = vec![100.0, 100.0, 100.0];
118        let lines = partition_into_lines(&widths, 10.0, 250.0);
119        assert_eq!(lines.len(), 2);
120        assert_eq!(lines[0].start, 0);
121        assert_eq!(lines[0].end, 2); // first two fit: 100 + 10 + 100 = 210 <= 250
122        assert_eq!(lines[1].start, 2);
123        assert_eq!(lines[1].end, 3);
124    }
125
126    #[test]
127    fn test_partition_oversized_item() {
128        // Single item wider than available — must still get its own line
129        let widths = vec![500.0];
130        let lines = partition_into_lines(&widths, 10.0, 200.0);
131        assert_eq!(lines.len(), 1);
132        assert_eq!(lines[0].start, 0);
133        assert_eq!(lines[0].end, 1);
134    }
135
136    #[test]
137    fn test_partition_empty_input() {
138        let lines = partition_into_lines(&[], 10.0, 200.0);
139        assert!(lines.is_empty());
140    }
141
142    #[test]
143    fn test_partition_exact_fit() {
144        // 2 items × 100pt + 1 gap × 10pt = 210pt; available = 210pt — should fit on one line
145        let widths = vec![100.0, 100.0];
146        let lines = partition_into_lines(&widths, 10.0, 210.0);
147        assert_eq!(lines.len(), 1);
148    }
149}