1use crate::geometry::Rect;
4use alloc::vec::Vec;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[cfg(feature = "layout-cache")]
10use core::num::NonZeroUsize;
11#[cfg(feature = "layout-cache")]
12use lru::LruCache;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub enum Constraint {
20 Length(u16),
22 Min(u16),
24 Max(u16),
26 Fill(u16),
28 Ratio(u16, u16),
30 Percentage(u16),
32}
33
34impl Constraint {
35 #[must_use]
37 pub fn apply(self, available: u16) -> u16 {
38 match self {
39 Self::Length(len) => len.min(available),
40 Self::Min(min) => min.min(available),
41 Self::Max(max) => available.min(max),
42 Self::Fill(_) => available,
43 Self::Ratio(num, den) => {
44 if den == 0 {
45 0
46 } else {
47 ((available as u32 * num as u32) / den as u32) as u16
48 }
49 }
50 Self::Percentage(pct) => ((available as u32 * pct as u32) / 100) as u16,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58pub enum Flex {
59 Start,
61 Center,
63 End,
65 SpaceBetween,
67 SpaceAround,
69}
70
71impl Default for Flex {
72 fn default() -> Self {
73 Self::Start
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
80pub enum Spacing {
81 Gap(u16),
83 Overlap(u16),
85}
86
87impl Default for Spacing {
88 fn default() -> Self {
89 Self::Gap(0)
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
96pub enum Direction {
97 Horizontal,
99 Vertical,
101}
102
103#[derive(Debug, Clone)]
122pub struct Layout {
123 direction: Direction,
124 constraints: Vec<Constraint>,
125 flex: Flex,
126 spacing: Spacing,
127 #[cfg(feature = "layout-cache")]
128 cache: Option<LruCache<LayoutCacheKey, Vec<Rect>>>,
129}
130
131impl Default for Layout {
132 fn default() -> Self {
133 Self {
134 direction: Direction::Vertical,
135 constraints: Vec::new(),
136 flex: Flex::default(),
137 spacing: Spacing::default(),
138 #[cfg(feature = "layout-cache")]
139 cache: None,
140 }
141 }
142}
143
144impl Layout {
145 #[must_use]
147 pub fn new() -> Self {
148 Self::default()
149 }
150
151 #[must_use]
153 pub fn direction(mut self, direction: Direction) -> Self {
154 self.direction = direction;
155 self
156 }
157
158 #[must_use]
160 pub fn constraints<I>(mut self, constraints: I) -> Self
161 where
162 I: IntoIterator,
163 I::Item: Into<Constraint>,
164 {
165 self.constraints = constraints.into_iter().map(Into::into).collect();
166 self
167 }
168
169 #[must_use]
171 pub fn flex(mut self, flex: Flex) -> Self {
172 self.flex = flex;
173 self
174 }
175
176 #[must_use]
178 pub fn spacing(mut self, spacing: Spacing) -> Self {
179 self.spacing = spacing;
180 self
181 }
182
183 #[cfg(feature = "layout-cache")]
185 #[must_use]
186 pub fn cache(mut self, capacity: NonZeroUsize) -> Self {
187 self.cache = Some(LruCache::new(capacity));
188 self
189 }
190
191 #[must_use]
195 pub fn horizontal<I>(constraints: I) -> Self
196 where
197 I: IntoIterator,
198 I::Item: Into<Constraint>,
199 {
200 Self::default()
201 .direction(Direction::Horizontal)
202 .constraints(constraints)
203 }
204
205 #[must_use]
209 pub fn vertical<I>(constraints: I) -> Self
210 where
211 I: IntoIterator,
212 I::Item: Into<Constraint>,
213 {
214 Self::default()
215 .direction(Direction::Vertical)
216 .constraints(constraints)
217 }
218
219 #[must_use]
223 pub fn split(&mut self, area: Rect) -> Vec<Rect> {
224 #[cfg(feature = "layout-cache")]
225 {
226 let key = LayoutCacheKey {
227 area,
228 direction: self.direction,
229 constraints: self.constraints.clone(),
230 flex: self.flex,
231 spacing: self.spacing,
232 };
233
234 if let Some(cache) = &mut self.cache {
235 if let Some(rects) = cache.get(&key) {
236 return rects.clone();
237 }
238 }
239
240 let rects = self.calculate_layout(area);
241
242 if let Some(cache) = &mut self.cache {
243 cache.put(key, rects.clone());
244 }
245
246 rects
247 }
248
249 #[cfg(not(feature = "layout-cache"))]
250 self.calculate_layout(area)
251 }
252
253 fn calculate_layout(&self, area: Rect) -> Vec<Rect> {
254 if self.constraints.is_empty() {
255 return Vec::new();
256 }
257
258 let (total_space, cross_size) = match self.direction {
259 Direction::Horizontal => (area.width, area.height),
260 Direction::Vertical => (area.height, area.width),
261 };
262
263 let mut sizes = Vec::with_capacity(self.constraints.len());
265 let mut fixed_space = 0u16;
266 let mut fill_weights = 0u32;
267
268 for constraint in &self.constraints {
270 match constraint {
271 Constraint::Length(len) => {
272 sizes.push(*len);
273 fixed_space = fixed_space.saturating_add(*len);
274 }
275 Constraint::Min(min) => {
276 sizes.push(*min);
277 fixed_space = fixed_space.saturating_add(*min);
278 }
279 Constraint::Max(max) => {
280 sizes.push(total_space.min(*max));
281 fixed_space = fixed_space.saturating_add(total_space.min(*max));
282 }
283 Constraint::Ratio(num, den) => {
284 let size = if *den == 0 {
285 0
286 } else {
287 ((total_space as u32 * *num as u32) / *den as u32) as u16
288 };
289 sizes.push(size);
290 fixed_space = fixed_space.saturating_add(size);
291 }
292 Constraint::Percentage(pct) => {
293 let size = ((total_space as u32 * *pct as u32) / 100) as u16;
294 sizes.push(size);
295 fixed_space = fixed_space.saturating_add(size);
296 }
297 Constraint::Fill(weight) => {
298 sizes.push(0); fill_weights += *weight as u32;
300 }
301 }
302 }
303
304 let spacing_total = if self.constraints.len() > 1 {
306 match self.spacing {
307 Spacing::Gap(gap) => gap.saturating_mul((self.constraints.len() - 1) as u16),
308 Spacing::Overlap(overlap) => {
309 0u16.saturating_sub(overlap.saturating_mul((self.constraints.len() - 1) as u16))
310 }
311 }
312 } else {
313 0
314 };
315
316 let available_for_fill = total_space
317 .saturating_sub(fixed_space)
318 .saturating_sub(spacing_total);
319
320 if fill_weights > 0 {
322 for (i, constraint) in self.constraints.iter().enumerate() {
323 if let Constraint::Fill(weight) = constraint {
324 let fill_size =
325 ((available_for_fill as u32 * *weight as u32) / fill_weights) as u16;
326 sizes[i] = fill_size;
327 }
328 }
329 }
330
331 let used_space: u16 = sizes.iter().sum();
333 let flex_space = total_space.saturating_sub(used_space);
334
335 let (mut x, mut y) = match self.flex {
336 Flex::Start => (area.x, area.y),
337 Flex::Center => match self.direction {
338 Direction::Horizontal => (area.x + flex_space / 2, area.y),
339 Direction::Vertical => (area.x, area.y + flex_space / 2),
340 },
341 Flex::End => match self.direction {
342 Direction::Horizontal => (area.x + flex_space, area.y),
343 Direction::Vertical => (area.x, area.y + flex_space),
344 },
345 Flex::SpaceBetween | Flex::SpaceAround => (area.x, area.y),
346 };
347
348 let mut rects = Vec::with_capacity(self.constraints.len());
349 let gap = match self.spacing {
350 Spacing::Gap(g) => g,
351 Spacing::Overlap(o) => 0u16.saturating_sub(o),
352 };
353
354 for size in sizes {
355 let rect = match self.direction {
356 Direction::Horizontal => Rect::new(x, y, size, cross_size),
357 Direction::Vertical => Rect::new(x, y, cross_size, size),
358 };
359 rects.push(rect);
360
361 match self.direction {
362 Direction::Horizontal => x = x.saturating_add(size).saturating_add(gap),
363 Direction::Vertical => y = y.saturating_add(size).saturating_add(gap),
364 }
365 }
366
367 rects
368 }
369}
370
371#[cfg(feature = "layout-cache")]
372#[derive(Debug, Clone, PartialEq, Eq, Hash)]
373struct LayoutCacheKey {
374 area: Rect,
375 direction: Direction,
376 constraints: Vec<Constraint>,
377 flex: Flex,
378 spacing: Spacing,
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_constraint_apply() {
387 assert_eq!(Constraint::Length(10).apply(100), 10);
388 assert_eq!(Constraint::Min(50).apply(100), 50);
389 assert_eq!(Constraint::Max(50).apply(100), 50);
390 assert_eq!(Constraint::Ratio(1, 2).apply(100), 50);
391 assert_eq!(Constraint::Percentage(50).apply(100), 50);
392 }
393
394 #[test]
395 fn test_layout_split() {
396 let area = Rect::new(0, 0, 100, 100);
397 let mut layout = Layout::default()
398 .direction(Direction::Vertical)
399 .constraints([
400 Constraint::Length(10),
401 Constraint::Fill(1),
402 Constraint::Length(20),
403 ]);
404
405 let rects = layout.split(area);
406 assert_eq!(rects.len(), 3);
407 assert_eq!(rects[0].height, 10);
408 assert_eq!(rects[2].height, 20);
409 }
410
411 #[test]
412 fn test_layout_horizontal() {
413 let area = Rect::new(0, 0, 100, 50);
414 let mut layout = Layout::default()
415 .direction(Direction::Horizontal)
416 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]);
417
418 let rects = layout.split(area);
419 assert_eq!(rects.len(), 2);
420 assert_eq!(rects[0].width, 50);
421 assert_eq!(rects[1].width, 50);
422 }
423}