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 enum JustifyContent {
19    Start,
20    Center,
21    End,
22    SpaceBetween,
23    SpaceAround,
24    SpaceEvenly,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct LinearLayout {
29    pub axis: Axis,
30    pub gap: u16,
31    pub padding: EdgeInsets,
32    pub cross_align: Align,
33    pub justify: JustifyContent,
34}
35
36impl LinearLayout {
37    pub const fn column() -> Self {
38        Self {
39            axis: Axis::Vertical,
40            gap: 2,
41            padding: EdgeInsets::all(0),
42            cross_align: Align::Stretch,
43            justify: JustifyContent::Start,
44        }
45    }
46
47    pub const fn row() -> Self {
48        Self {
49            axis: Axis::Horizontal,
50            gap: 2,
51            padding: EdgeInsets::all(0),
52            cross_align: Align::Stretch,
53            justify: JustifyContent::Start,
54        }
55    }
56
57    pub const fn flex_row() -> Self {
58        Self::row()
59    }
60
61    pub const fn flex_column() -> Self {
62        Self::column()
63    }
64
65    pub const fn with_gap(mut self, gap: u16) -> Self {
66        self.gap = gap;
67        self
68    }
69
70    pub const fn with_padding(mut self, padding: EdgeInsets) -> Self {
71        self.padding = padding;
72        self
73    }
74
75    pub const fn with_justify(mut self, justify: JustifyContent) -> Self {
76        self.justify = justify;
77        self
78    }
79
80    pub const fn with_cross_align(mut self, cross_align: Align) -> Self {
81        self.cross_align = cross_align;
82        self
83    }
84
85    pub fn arrange(&self, area: Rect, item_count: usize, out: &mut [Rect]) -> usize {
86        if item_count == 0 || out.is_empty() {
87            return 0;
88        }
89
90        let count = item_count.min(out.len());
91        let inner = area.inset(self.padding);
92        let gap_total = self.gap as u32 * count.saturating_sub(1) as u32;
93
94        match self.axis {
95            Axis::Vertical => {
96                let each_h = inner.h.saturating_sub(gap_total) / count as u32;
97                let mut y = inner.y;
98                for slot in out.iter_mut().take(count) {
99                    *slot = Rect::new(inner.x, y, inner.w, each_h);
100                    y += each_h as i32 + self.gap as i32;
101                }
102            }
103            Axis::Horizontal => {
104                let each_w = inner.w.saturating_sub(gap_total) / count as u32;
105                let mut x = inner.x;
106                for slot in out.iter_mut().take(count) {
107                    *slot = Rect::new(x, inner.y, each_w, inner.h);
108                    x += each_w as i32 + self.gap as i32;
109                }
110            }
111        }
112
113        count
114    }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum Constraint {
119    /// Request at least this many pixels in the current single-pass solver.
120    Min(u32),
121    /// Request no more than this many pixels in the current single-pass solver.
122    Max(u32),
123    /// Request an exact number of pixels.
124    Length(u32),
125    /// Request a percentage of the available main-axis space after gaps.
126    Percent(u8),
127    /// Request a ratio of the available main-axis space after gaps.
128    Ratio(u32, u32),
129    /// Share remaining main-axis space with other fill items by weight.
130    Fill(u16),
131}
132
133impl Constraint {
134    pub const fn length(px: u32) -> Self {
135        Self::Length(px)
136    }
137
138    pub const fn min(px: u32) -> Self {
139        Self::Min(px)
140    }
141
142    pub const fn max(px: u32) -> Self {
143        Self::Max(px)
144    }
145
146    pub const fn percent(percent: u8) -> Self {
147        Self::Percent(percent)
148    }
149
150    pub const fn ratio(numerator: u32, denominator: u32) -> Self {
151        Self::Ratio(numerator, denominator)
152    }
153
154    pub const fn fill(weight: u16) -> Self {
155        Self::Fill(weight)
156    }
157
158    fn fixed_size(self, total: u32) -> Option<u32> {
159        match self {
160            Self::Length(px) | Self::Min(px) | Self::Max(px) => Some(px),
161            Self::Percent(pct) => Some(total.saturating_mul(pct.min(100) as u32) / 100),
162            Self::Ratio(num, den) => Some(total.saturating_mul(num) / den.max(1)),
163            Self::Fill(_) => None,
164        }
165    }
166
167    fn clamp(self, value: u32) -> u32 {
168        match self {
169            Self::Min(px) => value.max(px),
170            Self::Max(px) => value.min(px),
171            _ => value,
172        }
173    }
174
175    fn fill_weight(self) -> u32 {
176        match self {
177            Self::Fill(weight) => weight.max(1) as u32,
178            _ => 0,
179        }
180    }
181}
182
183pub type Length = Constraint;
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
186pub struct LayoutItem {
187    pub main: Constraint,
188    pub cross: Constraint,
189    pub grow: u16,
190    pub shrink: u16,
191}
192
193impl LayoutItem {
194    pub const fn fixed(main: u32) -> Self {
195        Self::length(main)
196    }
197
198    pub const fn length(main: u32) -> Self {
199        Self {
200            main: Constraint::Length(main),
201            cross: Constraint::Fill(1),
202            grow: 0,
203            shrink: 1,
204        }
205    }
206
207    pub const fn fill() -> Self {
208        Self::fill_weight(1)
209    }
210
211    pub const fn fill_weight(weight: u16) -> Self {
212        Self {
213            main: Constraint::Fill(weight),
214            cross: Constraint::Fill(1),
215            grow: if weight == 0 { 1 } else { weight },
216            shrink: 1,
217        }
218    }
219
220    pub const fn percent(main: u8) -> Self {
221        Self {
222            main: Constraint::Percent(main),
223            cross: Constraint::Fill(1),
224            grow: 0,
225            shrink: 1,
226        }
227    }
228
229    pub const fn min(main: u32) -> Self {
230        Self {
231            main: Constraint::Min(main),
232            cross: Constraint::Fill(1),
233            grow: 0,
234            shrink: 1,
235        }
236    }
237
238    pub const fn max(main: u32) -> Self {
239        Self {
240            main: Constraint::Max(main),
241            cross: Constraint::Fill(1),
242            grow: 0,
243            shrink: 1,
244        }
245    }
246
247    pub const fn ratio(numerator: u32, denominator: u32) -> Self {
248        Self {
249            main: Constraint::Ratio(numerator, denominator),
250            cross: Constraint::Fill(1),
251            grow: 0,
252            shrink: 1,
253        }
254    }
255
256    pub const fn with_cross(mut self, cross: Constraint) -> Self {
257        self.cross = cross;
258        self
259    }
260
261    pub const fn with_grow(mut self, grow: u16) -> Self {
262        self.grow = grow;
263        self
264    }
265
266    pub const fn with_shrink(mut self, shrink: u16) -> Self {
267        self.shrink = shrink;
268        self
269    }
270
271    pub const fn flex(main: u32) -> Self {
272        Self::length(main).with_grow(1).with_shrink(1)
273    }
274
275    pub const fn rigid(main: u32) -> Self {
276        Self::length(main).with_grow(0).with_shrink(0)
277    }
278}
279
280impl LinearLayout {
281    /// Arranges items in a deterministic single pass.
282    ///
283    /// Fixed, percentage, ratio, min, and max requests are assigned before
284    /// fill space. If those requests exceed the available main-axis space,
285    /// items keep their requested sizes and later items may extend beyond the
286    /// layout area; render-time clipping is responsible for trimming pixels.
287    /// Weighted fill receives remaining pixels, with any rounding remainder
288    /// assigned to the final fill item.
289    pub fn arrange_items(&self, area: Rect, items: &[LayoutItem], out: &mut [Rect]) -> usize {
290        if items.is_empty() || out.is_empty() {
291            return 0;
292        }
293
294        let count = items.len().min(out.len());
295        let inner = area.inset(self.padding);
296        let main_total = match self.axis {
297            Axis::Horizontal => inner.w,
298            Axis::Vertical => inner.h,
299        };
300        let cross_total = match self.axis {
301            Axis::Horizontal => inner.h,
302            Axis::Vertical => inner.w,
303        };
304        let gap_total = self.gap as u32 * count.saturating_sub(1) as u32;
305        let available = main_total.saturating_sub(gap_total);
306        let mut fixed = 0u32;
307        let mut fill_weight = 0u32;
308
309        for item in items.iter().take(count) {
310            if let Some(px) = item.main.fixed_size(available) {
311                fixed = fixed.saturating_add(px);
312            } else {
313                fill_weight = fill_weight.saturating_add(item.main.fill_weight());
314            }
315        }
316
317        let remaining = available.saturating_sub(fixed);
318        let fill_unit = remaining.checked_div(fill_weight).unwrap_or(0);
319
320        let (mut cursor, item_gap) = if fill_weight == 0 && remaining > 0 {
321            let total_slack = remaining + gap_total;
322            match self.justify {
323                JustifyContent::Start => (
324                    match self.axis {
325                        Axis::Horizontal => inner.x,
326                        Axis::Vertical => inner.y,
327                    },
328                    self.gap as i32,
329                ),
330                JustifyContent::Center => (
331                    match self.axis {
332                        Axis::Horizontal => inner.x + (remaining as i32 / 2),
333                        Axis::Vertical => inner.y + (remaining as i32 / 2),
334                    },
335                    self.gap as i32,
336                ),
337                JustifyContent::End => (
338                    match self.axis {
339                        Axis::Horizontal => inner.x + remaining as i32,
340                        Axis::Vertical => inner.y + remaining as i32,
341                    },
342                    self.gap as i32,
343                ),
344                JustifyContent::SpaceBetween => {
345                    let step = if count > 1 {
346                        total_slack / (count as u32 - 1)
347                    } else {
348                        0
349                    };
350                    (
351                        match self.axis {
352                            Axis::Horizontal => inner.x,
353                            Axis::Vertical => inner.y,
354                        },
355                        step as i32,
356                    )
357                }
358                JustifyContent::SpaceAround => {
359                    let step = total_slack / count as u32;
360                    let initial = step / 2;
361                    (
362                        match self.axis {
363                            Axis::Horizontal => inner.x + initial as i32,
364                            Axis::Vertical => inner.y + initial as i32,
365                        },
366                        step as i32,
367                    )
368                }
369                JustifyContent::SpaceEvenly => {
370                    let step = total_slack / (count as u32 + 1);
371                    (
372                        match self.axis {
373                            Axis::Horizontal => inner.x + step as i32,
374                            Axis::Vertical => inner.y + step as i32,
375                        },
376                        step as i32,
377                    )
378                }
379            }
380        } else {
381            (
382                match self.axis {
383                    Axis::Horizontal => inner.x,
384                    Axis::Vertical => inner.y,
385                },
386                self.gap as i32,
387            )
388        };
389        let mut used_fill = 0u32;
390        let mut seen_fill_weight = 0u32;
391
392        for (slot, item) in out.iter_mut().zip(items.iter()).take(count) {
393            let main = if let Some(px) = item.main.fixed_size(available) {
394                px
395            } else {
396                let weight = item.main.fill_weight();
397                seen_fill_weight = seen_fill_weight.saturating_add(weight);
398                if seen_fill_weight >= fill_weight {
399                    remaining.saturating_sub(used_fill)
400                } else {
401                    let px = fill_unit.saturating_mul(weight);
402                    used_fill = used_fill.saturating_add(px);
403                    px
404                }
405            }
406            .min(available);
407            let main = item.main.clamp(main).min(available);
408            let cross = item
409                .cross
410                .fixed_size(cross_total)
411                .unwrap_or(cross_total)
412                .min(cross_total);
413            let cross = item.cross.clamp(cross).min(cross_total);
414            let cross_offset = match self.cross_align {
415                Align::Start | Align::Stretch => 0,
416                Align::Center => cross_total.saturating_sub(cross) as i32 / 2,
417                Align::End => cross_total.saturating_sub(cross) as i32,
418            };
419            let cross_size = if matches!(self.cross_align, Align::Stretch) {
420                cross_total
421            } else {
422                cross.min(cross_total)
423            };
424
425            *slot = match self.axis {
426                Axis::Horizontal => Rect::new(
427                    cursor,
428                    inner.y + cross_offset,
429                    main.min(available),
430                    cross_size,
431                ),
432                Axis::Vertical => Rect::new(
433                    inner.x + cross_offset,
434                    cursor,
435                    cross_size,
436                    main.min(available),
437                ),
438            };
439            cursor += main as i32 + item_gap;
440        }
441
442        count
443    }
444
445    pub fn arrange_items_flex(
446        &self,
447        area: Rect,
448        items: &[LayoutItem],
449        out: &mut [Rect],
450        enable_grow: bool,
451        enable_shrink: bool,
452    ) -> usize {
453        if items.is_empty() || out.is_empty() {
454            return 0;
455        }
456        let count = items.len().min(out.len());
457        let inner = area.inset(self.padding);
458        let main_total = match self.axis {
459            Axis::Horizontal => inner.w,
460            Axis::Vertical => inner.h,
461        };
462        let cross_total = match self.axis {
463            Axis::Horizontal => inner.h,
464            Axis::Vertical => inner.w,
465        };
466        let gap_total = self.gap as u32 * count.saturating_sub(1) as u32;
467        let available = main_total.saturating_sub(gap_total);
468
469        let mut grow_total = 0u32;
470        let mut shrink_total = 0u32;
471        let mut used = 0u32;
472        let mut fill_weight = 0u32;
473        for (idx, item) in items.iter().take(count).enumerate() {
474            if let Some(px) = item.main.fixed_size(available) {
475                let main = item.main.clamp(px).min(available);
476                out[idx].w = main;
477                used = used.saturating_add(main);
478            } else {
479                out[idx].w = 0;
480                fill_weight = fill_weight.saturating_add(item.main.fill_weight());
481            }
482            grow_total = grow_total.saturating_add(item.grow as u32);
483            shrink_total = shrink_total.saturating_add(item.shrink.max(1) as u32);
484        }
485        let remaining = available.saturating_sub(used);
486        let unit = remaining.checked_div(fill_weight).unwrap_or(0);
487        if fill_weight > 0 {
488            let mut seen = 0u32;
489            let mut used_fill = 0u32;
490            for (idx, item) in items.iter().take(count).enumerate() {
491                if item.main.fill_weight() == 0 {
492                    continue;
493                }
494                let w = item.main.fill_weight();
495                seen = seen.saturating_add(w);
496                let px = if seen >= fill_weight {
497                    remaining.saturating_sub(used_fill)
498                } else {
499                    let part = unit.saturating_mul(w);
500                    used_fill = used_fill.saturating_add(part);
501                    part
502                };
503                let main = item.main.clamp(px).min(available);
504                out[idx].w = main;
505                used = used.saturating_add(main);
506            }
507        }
508
509        if enable_grow && used < available && grow_total > 0 {
510            let extra = available - used;
511            let unit = extra / grow_total;
512            let mut seen = 0u32;
513            let mut given = 0u32;
514            for (idx, item) in items.iter().take(count).enumerate() {
515                let w = item.grow as u32;
516                if w == 0 {
517                    continue;
518                }
519                seen = seen.saturating_add(w);
520                let add = if seen >= grow_total {
521                    extra.saturating_sub(given)
522                } else {
523                    let part = unit.saturating_mul(w);
524                    given = given.saturating_add(part);
525                    part
526                };
527                out[idx].w = out[idx].w.saturating_add(add);
528            }
529        }
530
531        if enable_shrink && used > available && shrink_total > 0 {
532            let overflow = used - available;
533            let unit = overflow / shrink_total;
534            let mut seen = 0u32;
535            let mut taken = 0u32;
536            for (idx, item) in items.iter().take(count).enumerate() {
537                let w = item.shrink.max(1) as u32;
538                seen = seen.saturating_add(w);
539                let sub = if seen >= shrink_total {
540                    overflow.saturating_sub(taken)
541                } else {
542                    let part = unit.saturating_mul(w);
543                    taken = taken.saturating_add(part);
544                    part
545                };
546                out[idx].w = out[idx].w.saturating_sub(sub.min(out[idx].w));
547            }
548        }
549
550        let mut cursor = match self.axis {
551            Axis::Horizontal => inner.x,
552            Axis::Vertical => inner.y,
553        };
554        for idx in 0..count {
555            let item = items[idx];
556            let main = out[idx].w;
557            let cross = item
558                .cross
559                .fixed_size(cross_total)
560                .unwrap_or(cross_total)
561                .min(cross_total);
562            let cross = item.cross.clamp(cross).min(cross_total);
563            let cross_offset = match self.cross_align {
564                Align::Start | Align::Stretch => 0,
565                Align::Center => cross_total.saturating_sub(cross) as i32 / 2,
566                Align::End => cross_total.saturating_sub(cross) as i32,
567            };
568            let cross_size = if matches!(self.cross_align, Align::Stretch) {
569                cross_total
570            } else {
571                cross.min(cross_total)
572            };
573            out[idx] = match self.axis {
574                Axis::Horizontal => Rect::new(cursor, inner.y + cross_offset, main, cross_size),
575                Axis::Vertical => Rect::new(inner.x + cross_offset, cursor, cross_size, main),
576            };
577            cursor += main as i32 + self.gap as i32;
578        }
579        count
580    }
581}
582
583/// Sizing track definition for 2D Grid layouts.
584#[derive(Clone, Copy, Debug, PartialEq, Eq)]
585pub enum GridTrack {
586    /// Fixed pixel size.
587    Px(u32),
588    /// Fractional unit (proportional weight among remaining space).
589    Fr(u8),
590    /// Sized automatically / evenly.
591    Auto,
592}
593
594impl GridTrack {
595    pub const fn px(pixels: u32) -> Self {
596        Self::Px(pixels)
597    }
598
599    pub const fn fr(weight: u8) -> Self {
600        Self::Fr(weight)
601    }
602
603    pub const fn auto() -> Self {
604        Self::Auto
605    }
606}
607
608/// Placement and span of an item within a 2D Grid.
609#[derive(Clone, Copy, Debug, PartialEq, Eq)]
610pub struct GridPlacement {
611    pub col: usize,
612    pub row: usize,
613    pub col_span: usize,
614    pub row_span: usize,
615}
616
617impl GridPlacement {
618    pub const fn cell(col: usize, row: usize) -> Self {
619        Self {
620            col,
621            row,
622            col_span: 1,
623            row_span: 1,
624        }
625    }
626
627    pub const fn span(col: usize, row: usize, col_span: usize, row_span: usize) -> Self {
628        Self {
629            col,
630            row,
631            col_span: if col_span == 0 { 1 } else { col_span },
632            row_span: if row_span == 0 { 1 } else { row_span },
633        }
634    }
635}
636
637/// 2D CSS-style Grid Layout engine (fixed const capacity, `#![no_std]` zero-allocation).
638#[derive(Clone, Copy, Debug, PartialEq, Eq)]
639pub struct GridLayout<const COLS: usize, const ROWS: usize> {
640    pub col_tracks: [GridTrack; COLS],
641    pub row_tracks: [GridTrack; ROWS],
642    pub col_gap: u16,
643    pub row_gap: u16,
644    pub padding: EdgeInsets,
645}
646
647impl<const COLS: usize, const ROWS: usize> GridLayout<COLS, ROWS> {
648    pub const fn new(col_tracks: [GridTrack; COLS], row_tracks: [GridTrack; ROWS]) -> Self {
649        Self {
650            col_tracks,
651            row_tracks,
652            col_gap: 2,
653            row_gap: 2,
654            padding: EdgeInsets::all(0),
655        }
656    }
657
658    pub const fn uniform(col_gap: u16, row_gap: u16) -> Self {
659        Self {
660            col_tracks: [GridTrack::Auto; COLS],
661            row_tracks: [GridTrack::Auto; ROWS],
662            col_gap,
663            row_gap,
664            padding: EdgeInsets::all(0),
665        }
666    }
667
668    pub const fn with_gap(mut self, gap: u16) -> Self {
669        self.col_gap = gap;
670        self.row_gap = gap;
671        self
672    }
673
674    pub const fn with_col_gap(mut self, gap: u16) -> Self {
675        self.col_gap = gap;
676        self
677    }
678
679    pub const fn with_row_gap(mut self, gap: u16) -> Self {
680        self.row_gap = gap;
681        self
682    }
683
684    pub const fn with_padding(mut self, padding: EdgeInsets) -> Self {
685        self.padding = padding;
686        self
687    }
688
689    /// Resolves track coordinates (positions and sizes) for a set of grid tracks.
690    fn resolve_tracks(
691        available: u32,
692        start_pos: i32,
693        tracks: &[GridTrack],
694        gap: u16,
695        out_pos: &mut [i32],
696        out_sizes: &mut [u32],
697    ) {
698        let n = tracks.len();
699        if n == 0 {
700            return;
701        }
702
703        let total_gaps = ((n.saturating_sub(1)) as u32).saturating_mul(gap as u32);
704        let space = available.saturating_sub(total_gaps);
705
706        let mut fixed_sum: u32 = 0;
707        let mut total_fr: u32 = 0;
708        let mut auto_count: u32 = 0;
709
710        for track in tracks {
711            match *track {
712                GridTrack::Px(px) => fixed_sum = fixed_sum.saturating_add(px),
713                GridTrack::Fr(fr) => total_fr = total_fr.saturating_add(fr as u32),
714                GridTrack::Auto => auto_count = auto_count.saturating_add(1),
715            }
716        }
717
718        let remaining = space.saturating_sub(fixed_sum);
719
720        // Auto tracks are treated as 1fr if fr tracks are also present or evenly divided
721        if auto_count > 0 && total_fr == 0 {
722            total_fr = auto_count;
723        } else if auto_count > 0 {
724            total_fr = total_fr.saturating_add(auto_count);
725        }
726
727        // Calculate sizes
728        for (i, track) in tracks.iter().enumerate() {
729            let size = match *track {
730                GridTrack::Px(px) => px,
731                GridTrack::Fr(fr) => (remaining * fr as u32).checked_div(total_fr).unwrap_or(0),
732                GridTrack::Auto => remaining.checked_div(total_fr).unwrap_or(0),
733            };
734            out_sizes[i] = size;
735        }
736
737        // Compute starting positions
738        let mut cur_pos = start_pos;
739        for i in 0..n {
740            out_pos[i] = cur_pos;
741            cur_pos += out_sizes[i] as i32 + gap as i32;
742        }
743    }
744
745    /// Arranges items into calculated grid cell bounds according to their `GridPlacement`.
746    pub fn arrange_cells(
747        &self,
748        container: Rect,
749        placements: &[GridPlacement],
750        out: &mut [Rect],
751    ) -> usize {
752        let inner = container.inset(self.padding);
753        let mut col_pos = [0i32; COLS];
754        let mut col_sizes = [0u32; COLS];
755        let mut row_pos = [0i32; ROWS];
756        let mut row_sizes = [0u32; ROWS];
757
758        Self::resolve_tracks(
759            inner.w,
760            inner.x,
761            &self.col_tracks,
762            self.col_gap,
763            &mut col_pos,
764            &mut col_sizes,
765        );
766
767        Self::resolve_tracks(
768            inner.h,
769            inner.y,
770            &self.row_tracks,
771            self.row_gap,
772            &mut row_pos,
773            &mut row_sizes,
774        );
775
776        let count = placements.len().min(out.len());
777        for i in 0..count {
778            let p = placements[i];
779            let col = p.col.min(COLS.saturating_sub(1));
780            let row = p.row.min(ROWS.saturating_sub(1));
781            let col_end = (col + p.col_span).min(COLS);
782            let row_end = (row + p.row_span).min(ROWS);
783
784            let x = col_pos[col];
785            let y = row_pos[row];
786
787            let mut w: u32 = 0;
788            for (c, &size) in col_sizes.iter().enumerate().take(col_end).skip(col) {
789                w += size;
790                if c + 1 < col_end {
791                    w += self.col_gap as u32;
792                }
793            }
794
795            let mut h: u32 = 0;
796            for (r, &size) in row_sizes.iter().enumerate().take(row_end).skip(row) {
797                h += size;
798                if r + 1 < row_end {
799                    h += self.row_gap as u32;
800                }
801            }
802
803            out[i] = Rect::new(x, y, w, h);
804        }
805
806        count
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    #[test]
815    fn test_linear_layout_column_presets() {
816        let col = LinearLayout::column();
817        assert_eq!(col.axis, Axis::Vertical);
818        assert_eq!(col.gap, 2);
819        assert_eq!(col.cross_align, Align::Stretch);
820
821        let row = LinearLayout::row();
822        assert_eq!(row.axis, Axis::Horizontal);
823        assert_eq!(row.gap, 2);
824        assert_eq!(row.cross_align, Align::Stretch);
825    }
826
827    #[test]
828    fn test_layout_arrange_row() {
829        let layout = LinearLayout {
830            axis: Axis::Horizontal,
831            gap: 5,
832            padding: EdgeInsets::all(10),
833            cross_align: Align::Stretch,
834            justify: JustifyContent::Start,
835        };
836
837        let container = Rect::new(0, 0, 100, 50);
838        let items = [
839            LayoutItem::fixed(20),
840            LayoutItem::fill(),
841            LayoutItem::fixed(30),
842        ];
843        let mut out = [Rect::empty(); 3];
844
845        let arranged = layout.arrange_items(container, &items, &mut out);
846        assert_eq!(arranged, 3);
847
848        // Container width 100 - padding 20 = 80 main total.
849        // Item 0: w=20, x=10
850        assert_eq!(out[0].x, 10);
851        assert_eq!(out[0].w, 20);
852
853        // Item 1: x = 10 + 20 + 5 = 35, w = 20
854        assert_eq!(out[1].x, 35);
855        assert_eq!(out[1].w, 20);
856
857        // Item 2: x = 35 + 20 + 5 = 60, w = 30
858        assert_eq!(out[2].x, 60);
859        assert_eq!(out[2].w, 30);
860    }
861
862    #[test]
863    fn test_linear_layout_justify_content() {
864        let layout = LinearLayout::row()
865            .with_gap(0)
866            .with_justify(JustifyContent::SpaceBetween);
867
868        assert_eq!(layout.justify, JustifyContent::SpaceBetween);
869    }
870
871    #[test]
872    fn test_grid_layout_resolution_and_spans() {
873        let grid = GridLayout::<3, 2>::new(
874            [GridTrack::Px(50), GridTrack::Fr(1), GridTrack::Fr(2)],
875            [GridTrack::Px(30), GridTrack::Fr(1)],
876        )
877        .with_col_gap(10)
878        .with_row_gap(5)
879        .with_padding(EdgeInsets::all(10));
880
881        let container = Rect::new(0, 0, 320, 240);
882        let placements = [
883            GridPlacement::cell(0, 0),       // Top-left fixed 50x30
884            GridPlacement::span(1, 0, 2, 1), // Top row spanning cols 1 & 2
885            GridPlacement::span(0, 1, 3, 1), // Bottom row spanning all 3 cols
886        ];
887        let mut out = [Rect::empty(); 3];
888
889        let count = grid.arrange_cells(container, &placements, &mut out);
890        assert_eq!(count, 3);
891
892        // Item 0 (0,0):
893        assert_eq!(out[0].x, 10);
894        assert_eq!(out[0].y, 10);
895        assert_eq!(out[0].w, 50);
896        assert_eq!(out[0].h, 30);
897
898        // Item 1 (1,0, span col 2):
899        assert_eq!(out[1].x, 70); // 10 + 50 + 10 = 70
900        assert_eq!(out[1].y, 10);
901        assert_eq!(out[1].h, 30);
902
903        // Item 2 (0,1, span col 3):
904        assert_eq!(out[2].x, 10);
905        assert_eq!(out[2].y, 45); // 10 + 30 + 5 = 45
906    }
907}