1use crate::geometry::{EdgeInsets, Rect};
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4pub enum Axis {
5 Horizontal,
6 Vertical,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Align {
11 Start,
12 Center,
13 End,
14 Stretch,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct LinearLayout {
19 pub axis: Axis,
20 pub gap: u16,
21 pub padding: EdgeInsets,
22 pub cross_align: Align,
23}
24
25impl LinearLayout {
26 pub const fn column() -> Self {
27 Self {
28 axis: Axis::Vertical,
29 gap: 2,
30 padding: EdgeInsets::all(0),
31 cross_align: Align::Stretch,
32 }
33 }
34
35 pub const fn row() -> Self {
36 Self {
37 axis: Axis::Horizontal,
38 gap: 2,
39 padding: EdgeInsets::all(0),
40 cross_align: Align::Stretch,
41 }
42 }
43
44 pub const fn flex_row() -> Self {
45 Self::row()
46 }
47
48 pub const fn flex_column() -> Self {
49 Self::column()
50 }
51
52 pub const fn with_gap(mut self, gap: u16) -> Self {
53 self.gap = gap;
54 self
55 }
56
57 pub const fn with_padding(mut self, padding: EdgeInsets) -> Self {
58 self.padding = padding;
59 self
60 }
61
62 pub fn arrange(&self, area: Rect, item_count: usize, out: &mut [Rect]) -> usize {
63 if item_count == 0 || out.is_empty() {
64 return 0;
65 }
66
67 let count = item_count.min(out.len());
68 let inner = area.inset(self.padding);
69 let gap_total = self.gap as u32 * count.saturating_sub(1) as u32;
70
71 match self.axis {
72 Axis::Vertical => {
73 let each_h = inner.h.saturating_sub(gap_total) / count as u32;
74 let mut y = inner.y;
75 for slot in out.iter_mut().take(count) {
76 *slot = Rect::new(inner.x, y, inner.w, each_h);
77 y += each_h as i32 + self.gap as i32;
78 }
79 }
80 Axis::Horizontal => {
81 let each_w = inner.w.saturating_sub(gap_total) / count as u32;
82 let mut x = inner.x;
83 for slot in out.iter_mut().take(count) {
84 *slot = Rect::new(x, inner.y, each_w, inner.h);
85 x += each_w as i32 + self.gap as i32;
86 }
87 }
88 }
89
90 count
91 }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum Constraint {
96 Min(u32),
98 Max(u32),
100 Length(u32),
102 Percent(u8),
104 Ratio(u32, u32),
106 Fill(u16),
108}
109
110impl Constraint {
111 pub const fn length(px: u32) -> Self {
112 Self::Length(px)
113 }
114
115 pub const fn min(px: u32) -> Self {
116 Self::Min(px)
117 }
118
119 pub const fn max(px: u32) -> Self {
120 Self::Max(px)
121 }
122
123 pub const fn percent(percent: u8) -> Self {
124 Self::Percent(percent)
125 }
126
127 pub const fn ratio(numerator: u32, denominator: u32) -> Self {
128 Self::Ratio(numerator, denominator)
129 }
130
131 pub const fn fill(weight: u16) -> Self {
132 Self::Fill(weight)
133 }
134
135 fn fixed_size(self, total: u32) -> Option<u32> {
136 match self {
137 Self::Length(px) | Self::Min(px) | Self::Max(px) => Some(px),
138 Self::Percent(pct) => Some(total.saturating_mul(pct.min(100) as u32) / 100),
139 Self::Ratio(num, den) => Some(total.saturating_mul(num) / den.max(1)),
140 Self::Fill(_) => None,
141 }
142 }
143
144 fn clamp(self, value: u32) -> u32 {
145 match self {
146 Self::Min(px) => value.max(px),
147 Self::Max(px) => value.min(px),
148 _ => value,
149 }
150 }
151
152 fn fill_weight(self) -> u32 {
153 match self {
154 Self::Fill(weight) => weight.max(1) as u32,
155 _ => 0,
156 }
157 }
158}
159
160pub type Length = Constraint;
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub struct LayoutItem {
164 pub main: Constraint,
165 pub cross: Constraint,
166 pub grow: u16,
167 pub shrink: u16,
168}
169
170impl LayoutItem {
171 pub const fn fixed(main: u32) -> Self {
172 Self::length(main)
173 }
174
175 pub const fn length(main: u32) -> Self {
176 Self {
177 main: Constraint::Length(main),
178 cross: Constraint::Fill(1),
179 grow: 0,
180 shrink: 1,
181 }
182 }
183
184 pub const fn fill() -> Self {
185 Self::fill_weight(1)
186 }
187
188 pub const fn fill_weight(weight: u16) -> Self {
189 Self {
190 main: Constraint::Fill(weight),
191 cross: Constraint::Fill(1),
192 grow: if weight == 0 { 1 } else { weight },
193 shrink: 1,
194 }
195 }
196
197 pub const fn percent(main: u8) -> Self {
198 Self {
199 main: Constraint::Percent(main),
200 cross: Constraint::Fill(1),
201 grow: 0,
202 shrink: 1,
203 }
204 }
205
206 pub const fn min(main: u32) -> Self {
207 Self {
208 main: Constraint::Min(main),
209 cross: Constraint::Fill(1),
210 grow: 0,
211 shrink: 1,
212 }
213 }
214
215 pub const fn max(main: u32) -> Self {
216 Self {
217 main: Constraint::Max(main),
218 cross: Constraint::Fill(1),
219 grow: 0,
220 shrink: 1,
221 }
222 }
223
224 pub const fn ratio(numerator: u32, denominator: u32) -> Self {
225 Self {
226 main: Constraint::Ratio(numerator, denominator),
227 cross: Constraint::Fill(1),
228 grow: 0,
229 shrink: 1,
230 }
231 }
232
233 pub const fn with_cross(mut self, cross: Constraint) -> Self {
234 self.cross = cross;
235 self
236 }
237
238 pub const fn with_grow(mut self, grow: u16) -> Self {
239 self.grow = grow;
240 self
241 }
242
243 pub const fn with_shrink(mut self, shrink: u16) -> Self {
244 self.shrink = shrink;
245 self
246 }
247
248 pub const fn flex(main: u32) -> Self {
249 Self::length(main).with_grow(1).with_shrink(1)
250 }
251
252 pub const fn rigid(main: u32) -> Self {
253 Self::length(main).with_grow(0).with_shrink(0)
254 }
255}
256
257impl LinearLayout {
258 pub fn arrange_items(&self, area: Rect, items: &[LayoutItem], out: &mut [Rect]) -> usize {
267 if items.is_empty() || out.is_empty() {
268 return 0;
269 }
270
271 let count = items.len().min(out.len());
272 let inner = area.inset(self.padding);
273 let main_total = match self.axis {
274 Axis::Horizontal => inner.w,
275 Axis::Vertical => inner.h,
276 };
277 let cross_total = match self.axis {
278 Axis::Horizontal => inner.h,
279 Axis::Vertical => inner.w,
280 };
281 let gap_total = self.gap as u32 * count.saturating_sub(1) as u32;
282 let available = main_total.saturating_sub(gap_total);
283 let mut fixed = 0u32;
284 let mut fill_weight = 0u32;
285
286 for item in items.iter().take(count) {
287 if let Some(px) = item.main.fixed_size(available) {
288 fixed = fixed.saturating_add(px);
289 } else {
290 fill_weight = fill_weight.saturating_add(item.main.fill_weight());
291 }
292 }
293
294 let remaining = available.saturating_sub(fixed);
295 let fill_unit = remaining.checked_div(fill_weight).unwrap_or(0);
296
297 let mut cursor = match self.axis {
298 Axis::Horizontal => inner.x,
299 Axis::Vertical => inner.y,
300 };
301 let mut used_fill = 0u32;
302 let mut seen_fill_weight = 0u32;
303
304 for (slot, item) in out.iter_mut().zip(items.iter()).take(count) {
305 let main = if let Some(px) = item.main.fixed_size(available) {
306 px
307 } else {
308 let weight = item.main.fill_weight();
309 seen_fill_weight = seen_fill_weight.saturating_add(weight);
310 if seen_fill_weight >= fill_weight {
311 remaining.saturating_sub(used_fill)
312 } else {
313 let px = fill_unit.saturating_mul(weight);
314 used_fill = used_fill.saturating_add(px);
315 px
316 }
317 }
318 .min(available);
319 let main = item.main.clamp(main).min(available);
320 let cross = item
321 .cross
322 .fixed_size(cross_total)
323 .unwrap_or(cross_total)
324 .min(cross_total);
325 let cross = item.cross.clamp(cross).min(cross_total);
326 let cross_offset = match self.cross_align {
327 Align::Start | Align::Stretch => 0,
328 Align::Center => cross_total.saturating_sub(cross) as i32 / 2,
329 Align::End => cross_total.saturating_sub(cross) as i32,
330 };
331 let cross_size = if matches!(self.cross_align, Align::Stretch) {
332 cross_total
333 } else {
334 cross.min(cross_total)
335 };
336
337 *slot = match self.axis {
338 Axis::Horizontal => Rect::new(
339 cursor,
340 inner.y + cross_offset,
341 main.min(available),
342 cross_size,
343 ),
344 Axis::Vertical => Rect::new(
345 inner.x + cross_offset,
346 cursor,
347 cross_size,
348 main.min(available),
349 ),
350 };
351 cursor += main as i32 + self.gap as i32;
352 }
353
354 count
355 }
356
357 pub fn arrange_items_flex(
358 &self,
359 area: Rect,
360 items: &[LayoutItem],
361 out: &mut [Rect],
362 enable_grow: bool,
363 enable_shrink: bool,
364 ) -> usize {
365 if items.is_empty() || out.is_empty() {
366 return 0;
367 }
368 let count = items.len().min(out.len());
369 let inner = area.inset(self.padding);
370 let main_total = match self.axis {
371 Axis::Horizontal => inner.w,
372 Axis::Vertical => inner.h,
373 };
374 let cross_total = match self.axis {
375 Axis::Horizontal => inner.h,
376 Axis::Vertical => inner.w,
377 };
378 let gap_total = self.gap as u32 * count.saturating_sub(1) as u32;
379 let available = main_total.saturating_sub(gap_total);
380
381 let mut grow_total = 0u32;
382 let mut shrink_total = 0u32;
383 let mut used = 0u32;
384 let mut fill_weight = 0u32;
385 for (idx, item) in items.iter().take(count).enumerate() {
386 if let Some(px) = item.main.fixed_size(available) {
387 let main = item.main.clamp(px).min(available);
388 out[idx].w = main;
389 used = used.saturating_add(main);
390 } else {
391 out[idx].w = 0;
392 fill_weight = fill_weight.saturating_add(item.main.fill_weight());
393 }
394 grow_total = grow_total.saturating_add(item.grow as u32);
395 shrink_total = shrink_total.saturating_add(item.shrink.max(1) as u32);
396 }
397 let remaining = available.saturating_sub(used);
398 let unit = remaining.checked_div(fill_weight).unwrap_or(0);
399 if fill_weight > 0 {
400 let mut seen = 0u32;
401 let mut used_fill = 0u32;
402 for (idx, item) in items.iter().take(count).enumerate() {
403 if item.main.fill_weight() == 0 {
404 continue;
405 }
406 let w = item.main.fill_weight();
407 seen = seen.saturating_add(w);
408 let px = if seen >= fill_weight {
409 remaining.saturating_sub(used_fill)
410 } else {
411 let part = unit.saturating_mul(w);
412 used_fill = used_fill.saturating_add(part);
413 part
414 };
415 let main = item.main.clamp(px).min(available);
416 out[idx].w = main;
417 used = used.saturating_add(main);
418 }
419 }
420
421 if enable_grow && used < available && grow_total > 0 {
422 let extra = available - used;
423 let unit = extra / grow_total;
424 let mut seen = 0u32;
425 let mut given = 0u32;
426 for (idx, item) in items.iter().take(count).enumerate() {
427 let w = item.grow as u32;
428 if w == 0 {
429 continue;
430 }
431 seen = seen.saturating_add(w);
432 let add = if seen >= grow_total {
433 extra.saturating_sub(given)
434 } else {
435 let part = unit.saturating_mul(w);
436 given = given.saturating_add(part);
437 part
438 };
439 out[idx].w = out[idx].w.saturating_add(add);
440 }
441 }
442
443 if enable_shrink && used > available && shrink_total > 0 {
444 let overflow = used - available;
445 let unit = overflow / shrink_total;
446 let mut seen = 0u32;
447 let mut taken = 0u32;
448 for (idx, item) in items.iter().take(count).enumerate() {
449 let w = item.shrink.max(1) as u32;
450 seen = seen.saturating_add(w);
451 let sub = if seen >= shrink_total {
452 overflow.saturating_sub(taken)
453 } else {
454 let part = unit.saturating_mul(w);
455 taken = taken.saturating_add(part);
456 part
457 };
458 out[idx].w = out[idx].w.saturating_sub(sub.min(out[idx].w));
459 }
460 }
461
462 let mut cursor = match self.axis {
463 Axis::Horizontal => inner.x,
464 Axis::Vertical => inner.y,
465 };
466 for idx in 0..count {
467 let item = items[idx];
468 let main = out[idx].w;
469 let cross = item
470 .cross
471 .fixed_size(cross_total)
472 .unwrap_or(cross_total)
473 .min(cross_total);
474 let cross = item.cross.clamp(cross).min(cross_total);
475 let cross_offset = match self.cross_align {
476 Align::Start | Align::Stretch => 0,
477 Align::Center => cross_total.saturating_sub(cross) as i32 / 2,
478 Align::End => cross_total.saturating_sub(cross) as i32,
479 };
480 let cross_size = if matches!(self.cross_align, Align::Stretch) {
481 cross_total
482 } else {
483 cross.min(cross_total)
484 };
485 out[idx] = match self.axis {
486 Axis::Horizontal => Rect::new(cursor, inner.y + cross_offset, main, cross_size),
487 Axis::Vertical => Rect::new(inner.x + cross_offset, cursor, cross_size, main),
488 };
489 cursor += main as i32 + self.gap as i32;
490 }
491 count
492 }
493}