Skip to main content

lv_tui/
layout.rs

1use crate::geom::{Rect, Size};
2use crate::style::Length;
3
4/// Layout constraint passed to `Component::measure`.
5///
6/// Specifies the minimum and maximum size a component may occupy.
7#[derive(Debug, Clone, Copy)]
8pub struct Constraint {
9    /// Minimum acceptable size.
10    pub min: Size,
11    /// Maximum allowed size.
12    pub max: Size,
13}
14
15impl Constraint {
16    /// Creates a constraint with zero minimum and the given maximum.
17    pub fn loose(max_width: u16, max_height: u16) -> Self {
18        Self {
19            min: Size::default(),
20            max: Size {
21                width: max_width,
22                height: max_height,
23            },
24        }
25    }
26}
27
28/// Describes a single item in a layout pass.
29///
30/// Used by [`layout_vertical`] and [`layout_horizontal`] to allocate space.
31pub struct LayoutItem {
32    /// Width constraint for this item.
33    pub width: Length,
34    /// Height constraint for this item.
35    pub height: Length,
36    /// Margin to subtract from available space around this item.
37    pub margin: crate::geom::Insets,
38    /// Flex grow factor for remaining space allocation.
39    pub flex_grow: u16,
40    /// Whether this item can shrink below its intrinsic size.
41    pub flex_shrink: bool,
42}
43
44/// 垂直布局:分配每个子项的 Rect
45///
46/// 分配顺序:Fixed → Percent → Fraction → Auto
47/// 考虑 margin 偏移。flex_grow > 0 的项参与 Fraction 空间分配。
48pub fn layout_vertical(rect: Rect, items: &[LayoutItem], gap: u16) -> Vec<Rect> {
49    let mut rects = Vec::with_capacity(items.len());
50
51    if items.is_empty() {
52        return rects;
53    }
54
55    let total_gap = gap.saturating_mul(items.len().saturating_sub(1) as u16);
56    // Subtract vertical margins from available height
57    let margin_v: u16 = items.iter().map(|item| item.margin.top.saturating_add(item.margin.bottom)).sum();
58    let available_height = rect.height.saturating_sub(total_gap).saturating_sub(margin_v);
59
60    let mut fixed_total: u16 = 0;
61    let mut fraction_total: u16 = 0;
62    let mut auto_count: u16 = 0;
63
64    for item in items {
65        let h = if item.flex_grow > 0 { Length::Fraction(item.flex_grow) } else { item.height };
66        match h {
67            Length::Fixed(h) => fixed_total = fixed_total.saturating_add(h),
68            Length::Fraction(w) => fraction_total = fraction_total.saturating_add(w),
69            Length::Percent(_) | Length::Auto => auto_count = auto_count.saturating_add(1),
70        }
71    }
72
73    let fixed_total = fixed_total.min(available_height);
74    let auto_total = auto_count;
75    let fraction_space = available_height.saturating_sub(fixed_total).saturating_sub(auto_total);
76    let fraction_unit = if fraction_total > 0 { fraction_space / fraction_total } else { 0 };
77    let mut fraction_remaining = fraction_total;
78
79    let mut y_offset = rect.y;
80
81    for item in items {
82        // Apply top margin
83        y_offset = y_offset.saturating_add(item.margin.top);
84
85        let h = if item.flex_grow > 0 { Length::Fraction(item.flex_grow) } else { item.height };
86        let height = match h {
87            Length::Fixed(h) => h.min(available_height),
88            Length::Percent(p) => available_height.saturating_mul(p) / 100,
89            Length::Fraction(w) => {
90                let base = w.saturating_mul(fraction_unit);
91                fraction_remaining = fraction_remaining.saturating_sub(w);
92                if fraction_remaining == 0 {
93                    rect.y.saturating_add(available_height).saturating_sub(y_offset)
94                } else {
95                    base
96                }
97            }
98            Length::Auto => 1,
99        };
100
101        let height = if y_offset.saturating_add(height) > rect.y.saturating_add(available_height) {
102            rect.y.saturating_add(available_height).saturating_sub(y_offset)
103        } else {
104            height
105        };
106
107        rects.push(Rect {
108            x: rect.x.saturating_add(item.margin.left),
109            y: y_offset,
110            width: rect.width.saturating_sub(item.margin.left.saturating_add(item.margin.right)),
111            height,
112        });
113
114        y_offset = y_offset.saturating_add(height).saturating_add(item.margin.bottom).saturating_add(gap);
115    }
116
117    rects
118}
119
120/// 水平布局:分配每个子项的 Rect
121///
122/// 分配顺序:Fixed → Percent → Fraction → Auto
123/// 考虑 margin 偏移。flex_grow > 0 的项参与 Fraction 空间分配。
124pub fn layout_horizontal(rect: Rect, items: &[LayoutItem], gap: u16) -> Vec<Rect> {
125    let mut rects = Vec::with_capacity(items.len());
126
127    if items.is_empty() {
128        return rects;
129    }
130
131    let total_gap = gap.saturating_mul(items.len().saturating_sub(1) as u16);
132    let margin_h: u16 = items.iter().map(|item| item.margin.left.saturating_add(item.margin.right)).sum();
133    let available_width = rect.width.saturating_sub(total_gap).saturating_sub(margin_h);
134
135    let mut fixed_total: u16 = 0;
136    let mut fraction_total: u16 = 0;
137    let mut auto_count: u16 = 0;
138
139    for item in items {
140        let w = if item.flex_grow > 0 { Length::Fraction(item.flex_grow) } else { item.width };
141        match w {
142            Length::Fixed(w) => fixed_total = fixed_total.saturating_add(w),
143            Length::Fraction(w) => fraction_total = fraction_total.saturating_add(w),
144            Length::Percent(_) | Length::Auto => auto_count = auto_count.saturating_add(1),
145        }
146    }
147
148    let fixed_total = fixed_total.min(available_width);
149    let auto_total = auto_count;
150    let fraction_space = available_width.saturating_sub(fixed_total).saturating_sub(auto_total);
151    let fraction_unit = if fraction_total > 0 { fraction_space / fraction_total } else { 0 };
152    let mut fraction_remaining = fraction_total;
153
154    let mut x_offset = rect.x;
155
156    for item in items {
157        x_offset = x_offset.saturating_add(item.margin.left);
158
159        let w = if item.flex_grow > 0 { Length::Fraction(item.flex_grow) } else { item.width };
160        let width = match w {
161            Length::Fixed(w) => w.min(available_width),
162            Length::Percent(p) => available_width.saturating_mul(p) / 100,
163            Length::Fraction(w) => {
164                let base = w.saturating_mul(fraction_unit);
165                fraction_remaining = fraction_remaining.saturating_sub(w);
166                if fraction_remaining == 0 {
167                    rect.x.saturating_add(available_width).saturating_sub(x_offset)
168                } else {
169                    base
170                }
171            }
172            Length::Auto => 1,
173        };
174
175        let width = if x_offset.saturating_add(width) > rect.x.saturating_add(available_width) {
176            rect.x.saturating_add(available_width).saturating_sub(x_offset)
177        } else {
178            width
179        };
180
181        rects.push(Rect {
182            x: x_offset,
183            y: rect.y.saturating_add(item.margin.top),
184            width,
185            height: rect.height.saturating_sub(item.margin.top.saturating_add(item.margin.bottom)),
186        });
187
188        x_offset = x_offset.saturating_add(width).saturating_add(item.margin.right).saturating_add(gap);
189    }
190
191    rects
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_layout_vertical_fixed() {
200        let rect = Rect { x: 0, y: 0, width: 10, height: 10 };
201        let items = [
202            LayoutItem { width: Length::Auto, height: Length::Fixed(3), margin: crate::geom::Insets::ZERO, flex_grow: 0, flex_shrink: true },
203            LayoutItem { width: Length::Auto, height: Length::Fixed(4), margin: crate::geom::Insets::ZERO, flex_grow: 0, flex_shrink: true },
204        ];
205        let result = layout_vertical(rect, &items, 1);
206        assert_eq!(result.len(), 2);
207        assert_eq!(result[0], Rect { x: 0, y: 0, width: 10, height: 3 });
208        assert_eq!(result[1], Rect { x: 0, y: 4, width: 10, height: 4 });
209    }
210
211    #[test]
212    fn test_layout_horizontal_fixed() {
213        let rect = Rect { x: 0, y: 0, width: 10, height: 5 };
214        let items = [
215            LayoutItem { width: Length::Fixed(3), height: Length::Auto, margin: crate::geom::Insets::ZERO, flex_grow: 0, flex_shrink: true },
216            LayoutItem { width: Length::Fixed(4), height: Length::Auto, margin: crate::geom::Insets::ZERO, flex_grow: 0, flex_shrink: true },
217        ];
218        let result = layout_horizontal(rect, &items, 1);
219        assert_eq!(result.len(), 2);
220        assert_eq!(result[0], Rect { x: 0, y: 0, width: 3, height: 5 });
221        assert_eq!(result[1], Rect { x: 4, y: 0, width: 4, height: 5 });
222    }
223
224    #[test]
225    fn test_layout_horizontal_fraction() {
226        let rect = Rect { x: 0, y: 0, width: 10, height: 5 };
227        let items = [
228            LayoutItem { width: Length::Fraction(1), height: Length::Auto, margin: crate::geom::Insets::ZERO, flex_grow: 0, flex_shrink: true },
229            LayoutItem { width: Length::Fraction(1), height: Length::Auto, margin: crate::geom::Insets::ZERO, flex_grow: 0, flex_shrink: true },
230        ];
231        let result = layout_horizontal(rect, &items, 0);
232        assert_eq!(result.len(), 2);
233        assert_eq!(result[0].width, 5);
234        assert_eq!(result[1].width, 5);
235    }
236
237    #[test]
238    fn bench_layout_large() {
239        let rect = Rect { x: 0, y: 0, width: 200, height: 200 };
240        let items: Vec<LayoutItem> = (0..100).map(|_| LayoutItem {
241            width: Length::Fraction(1), height: Length::Fixed(1),
242            margin: crate::geom::Insets::ZERO, flex_grow: 0, flex_shrink: true,
243        }).collect();
244        let result = layout_vertical(rect, &items, 0);
245        assert_eq!(result.len(), 100);
246        // All items should have non-zero height
247        for r in &result { assert!(r.height > 0); }
248    }
249}