Skip to main content

embedded_gui/
layout.rs

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    /// Request at least this many pixels in the current single-pass solver.
97    Min(u32),
98    /// Request no more than this many pixels in the current single-pass solver.
99    Max(u32),
100    /// Request an exact number of pixels.
101    Length(u32),
102    /// Request a percentage of the available main-axis space after gaps.
103    Percent(u8),
104    /// Request a ratio of the available main-axis space after gaps.
105    Ratio(u32, u32),
106    /// Share remaining main-axis space with other fill items by weight.
107    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    /// Arranges items in a deterministic single pass.
259    ///
260    /// Fixed, percentage, ratio, min, and max requests are assigned before
261    /// fill space. If those requests exceed the available main-axis space,
262    /// items keep their requested sizes and later items may extend beyond the
263    /// layout area; render-time clipping is responsible for trimming pixels.
264    /// Weighted fill receives remaining pixels, with any rounding remainder
265    /// assigned to the final fill item.
266    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}