1use crate::geom::{Rect, Size};
2use crate::style::Length;
3
4#[derive(Debug, Clone, Copy)]
8pub struct Constraint {
9 pub min: Size,
11 pub max: Size,
13}
14
15impl Constraint {
16 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
28pub struct LayoutItem {
32 pub width: Length,
34 pub height: Length,
36 pub margin: crate::geom::Insets,
38 pub flex_grow: u16,
40 pub flex_shrink: bool,
42}
43
44pub 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 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 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
120pub 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 for r in &result { assert!(r.height > 0); }
248 }
249}