Skip to main content

rae/
layout.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4use crate::math::Vec2;
5
6/// Axis-aligned rectangle in logical UI coordinates.
7#[derive(Clone, Copy, Debug, Default, PartialEq)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub struct Rect {
10    pub x: f32,
11    pub y: f32,
12    pub width: f32,
13    pub height: f32,
14}
15
16impl Rect {
17    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);
18
19    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
20        Self {
21            x,
22            y,
23            width,
24            height,
25        }
26    }
27
28    pub fn right(self) -> f32 {
29        self.x + self.width
30    }
31
32    pub fn bottom(self) -> f32 {
33        self.y + self.height
34    }
35
36    /// Returns the center point of the rectangle.
37    pub fn center(self) -> Vec2 {
38        Vec2::new(self.x + self.width * 0.5, self.y + self.height * 0.5)
39    }
40
41    /// Returns this rectangle offset by the provided delta.
42    pub fn translate(self, delta: Vec2) -> Self {
43        Self::new(self.x + delta.x, self.y + delta.y, self.width, self.height)
44    }
45
46    pub fn is_empty(self) -> bool {
47        self.width <= 0.0 || self.height <= 0.0
48    }
49
50    /// Returns true when coordinates, dimensions, and derived edges are finite numbers.
51    pub fn is_finite(self) -> bool {
52        self.x.is_finite()
53            && self.y.is_finite()
54            && self.width.is_finite()
55            && self.height.is_finite()
56            && self.right().is_finite()
57            && self.bottom().is_finite()
58    }
59
60    pub fn inset(self, inset: Insets) -> Self {
61        if !self.is_finite() || !inset.is_finite() {
62            return Self::ZERO;
63        }
64        let x = self.x + inset.left;
65        let y = self.y + inset.top;
66        let width = (self.width - inset.left - inset.right).max(0.0);
67        let height = (self.height - inset.top - inset.bottom).max(0.0);
68        let rect = Self::new(x, y, width, height);
69        if rect.is_finite() {
70            rect
71        } else {
72            Self::ZERO
73        }
74    }
75
76    pub fn contains(self, x: f32, y: f32) -> bool {
77        self.is_finite()
78            && !self.is_empty()
79            && x.is_finite()
80            && y.is_finite()
81            && x >= self.x
82            && x <= self.right()
83            && y >= self.y
84            && y <= self.bottom()
85    }
86
87    /// Returns the overlapping region, or `None` when the rectangles do not overlap.
88    pub fn intersection(self, other: Self) -> Option<Self> {
89        if !self.is_finite() || !other.is_finite() {
90            return None;
91        }
92        let x = self.x.max(other.x);
93        let y = self.y.max(other.y);
94        let right = self.right().min(other.right());
95        let bottom = self.bottom().min(other.bottom());
96        let width = right - x;
97        let height = bottom - y;
98        (width > 0.0 && height > 0.0).then(|| Self::new(x, y, width, height))
99    }
100
101    /// Returns the smallest rectangle that contains both rectangles.
102    pub fn union(self, other: Self) -> Self {
103        if !self.is_finite() {
104            return if other.is_finite() { other } else { Self::ZERO };
105        }
106        if !other.is_finite() {
107            return self;
108        }
109        if self.is_empty() {
110            return other;
111        }
112        if other.is_empty() {
113            return self;
114        }
115        let x = self.x.min(other.x);
116        let y = self.y.min(other.y);
117        let right = self.right().max(other.right());
118        let bottom = self.bottom().max(other.bottom());
119        let rect = Self::new(x, y, right - x, bottom - y);
120        if rect.is_finite() {
121            rect
122        } else {
123            Self::ZERO
124        }
125    }
126
127    /// Returns a rectangle moved and shrunk as needed to fit inside `bounds`.
128    pub fn clamp_within(self, bounds: Self) -> Self {
129        if !self.is_finite() || !bounds.is_finite() || self.is_empty() || bounds.is_empty() {
130            return Self::ZERO;
131        }
132        let width = self.width.min(bounds.width).max(0.0);
133        let height = self.height.min(bounds.height).max(0.0);
134        let max_x = bounds.x + (bounds.width - width).max(0.0);
135        let max_y = bounds.y + (bounds.height - height).max(0.0);
136        let rect = Self::new(
137            self.x.clamp(bounds.x, max_x),
138            self.y.clamp(bounds.y, max_y),
139            width,
140            height,
141        );
142        if rect.is_finite() {
143            rect
144        } else {
145            Self::ZERO
146        }
147    }
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq)]
151#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
152pub struct Insets {
153    pub top: f32,
154    pub right: f32,
155    pub bottom: f32,
156    pub left: f32,
157}
158
159impl Insets {
160    pub const fn all(value: f32) -> Self {
161        Self {
162            top: value,
163            right: value,
164            bottom: value,
165            left: value,
166        }
167    }
168
169    pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
170        Self {
171            top: vertical,
172            right: horizontal,
173            bottom: vertical,
174            left: horizontal,
175        }
176    }
177
178    pub fn is_finite(self) -> bool {
179        self.top.is_finite()
180            && self.right.is_finite()
181            && self.bottom.is_finite()
182            && self.left.is_finite()
183    }
184}
185
186/// Width/height pair in logical UI pixels.
187#[derive(Clone, Copy, Debug, PartialEq)]
188#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
189pub struct Size {
190    pub width: f32,
191    pub height: f32,
192}
193
194impl Size {
195    pub const ZERO: Self = Self::new(0.0, 0.0);
196    pub const INFINITE: Self = Self::new(f32::INFINITY, f32::INFINITY);
197
198    pub const fn new(width: f32, height: f32) -> Self {
199        Self { width, height }
200    }
201
202    pub fn is_finite(self) -> bool {
203        self.width.is_finite() && self.height.is_finite()
204    }
205
206    pub fn sanitized(self) -> Self {
207        Self::new(finite_px(self.width, 0.0), finite_px(self.height, 0.0))
208    }
209
210    pub fn clamp(self, min: Self, max: Self) -> Self {
211        let min = min.sanitized();
212        let max = max.sanitized_or_infinite();
213        Self::new(
214            finite_px(self.width, 0.0).clamp(min.width.min(max.width), max.width.max(min.width)),
215            finite_px(self.height, 0.0)
216                .clamp(min.height.min(max.height), max.height.max(min.height)),
217        )
218    }
219
220    fn sanitized_or_infinite(self) -> Self {
221        Self::new(
222            if self.width.is_finite() {
223                self.width.max(0.0)
224            } else {
225                f32::INFINITY
226            },
227            if self.height.is_finite() {
228                self.height.max(0.0)
229            } else {
230                f32::INFINITY
231            },
232        )
233    }
234}
235
236impl Default for Size {
237    fn default() -> Self {
238        Self::ZERO
239    }
240}
241
242#[derive(Clone, Copy, Debug, PartialEq)]
243#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
244#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
245pub enum FluidConstraint {
246    Min(f32),
247    Max,
248}
249
250impl FluidConstraint {
251    fn stack(self, other: Self) -> Self {
252        match (self, other) {
253            (Self::Min(left), Self::Min(right)) => {
254                Self::Min(finite_px(left, 0.0) + finite_px(right, 0.0))
255            }
256            (Self::Min(min), Self::Max) | (Self::Max, Self::Min(min)) => {
257                Self::Min(finite_px(min, 0.0))
258            }
259            (Self::Max, Self::Max) => Self::Max,
260        }
261    }
262
263    fn cross(self, other: Self) -> Self {
264        match (self, other) {
265            (Self::Min(left), Self::Min(right)) => {
266                Self::Min(finite_px(left, 0.0).max(finite_px(right, 0.0)))
267            }
268            (Self::Min(min), Self::Max) | (Self::Max, Self::Min(min)) => {
269                Self::Min(finite_px(min, 0.0))
270            }
271            (Self::Max, Self::Max) => Self::Max,
272        }
273    }
274}
275
276#[derive(Clone, Copy, Debug, PartialEq)]
277#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
278#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
279pub enum LengthBounds {
280    Min(f32),
281    Max(f32),
282    Both { min: f32, max: f32 },
283}
284
285impl LengthBounds {
286    pub fn min(self, min: f32) -> Self {
287        let min = finite_px(min, 0.0);
288        match self {
289            Self::Min(_) => Self::Min(min),
290            Self::Max(max) | Self::Both { max, .. } => Self::Both {
291                min,
292                max: finite_px(max, min).max(min),
293            },
294        }
295    }
296
297    pub fn max(self, max: f32) -> Self {
298        let max = finite_px(max, 0.0);
299        match self {
300            Self::Max(_) => Self::Max(max),
301            Self::Min(min) | Self::Both { min, .. } => Self::Both {
302                min: finite_px(min, 0.0).min(max),
303                max,
304            },
305        }
306    }
307
308    fn constraint(self) -> FluidConstraint {
309        match self {
310            Self::Min(min) | Self::Both { min, .. } => FluidConstraint::Min(finite_px(min, 0.0)),
311            Self::Max(_) => FluidConstraint::Max,
312        }
313    }
314
315    fn apply(self, min_value: &mut f32, max_value: &mut f32) {
316        match self {
317            Self::Min(min) => *min_value = finite_px(min, 0.0).min(*max_value).max(*min_value),
318            Self::Max(max) => *max_value = finite_px(max, 0.0).min(*max_value).max(*min_value),
319            Self::Both { min, max } => {
320                *min_value = finite_px(min, 0.0).min(*max_value).max(*min_value);
321                *max_value = finite_px(max, *min_value).min(*max_value).max(*min_value);
322            }
323        }
324    }
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
329#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
330pub enum LengthSizing {
331    Fit,
332    Fill(u16),
333    Shrink,
334}
335
336/// Strategy used to resolve one layout axis.
337#[derive(Clone, Copy, Debug, PartialEq)]
338#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
339#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
340pub enum Length {
341    Fit,
342    Fill,
343    FillPortion(u16),
344    Shrink,
345    Fixed(f32),
346    Bounded {
347        bounds: LengthBounds,
348        sizing: LengthSizing,
349    },
350    Fluid(FluidConstraint),
351}
352
353impl Length {
354    pub const fn fixed(px: f32) -> Self {
355        Self::Fixed(px)
356    }
357
358    pub const fn fill_portion(portion: u16) -> Self {
359        Self::FillPortion(portion)
360    }
361
362    pub fn min(self, min: f32) -> Self {
363        let min = finite_px(min, 0.0);
364        match self {
365            Self::Fixed(_) => self,
366            Self::Bounded { bounds, sizing } => Self::Bounded {
367                bounds: bounds.min(min),
368                sizing,
369            },
370            length => Self::Bounded {
371                bounds: LengthBounds::Min(min),
372                sizing: length.sizing(),
373            },
374        }
375    }
376
377    pub fn max(self, max: f32) -> Self {
378        let max = finite_px(max, 0.0);
379        match self {
380            Self::Fixed(_) => self,
381            Self::Bounded { bounds, sizing } => Self::Bounded {
382                bounds: bounds.max(max),
383                sizing,
384            },
385            length => Self::Bounded {
386                bounds: LengthBounds::Max(max),
387                sizing: length.sizing(),
388            },
389        }
390    }
391
392    pub fn fill_factor(self) -> u16 {
393        match self {
394            Self::Fill | Self::Fluid(_) => 1,
395            Self::FillPortion(portion) => portion.max(1),
396            Self::Bounded {
397                sizing: LengthSizing::Fill(portion),
398                ..
399            } => portion.max(1),
400            Self::Fit | Self::Shrink | Self::Fixed(_) | Self::Bounded { .. } => 0,
401        }
402    }
403
404    pub fn is_fill(self) -> bool {
405        self.fill_factor() != 0
406    }
407
408    pub fn is_fit(self) -> bool {
409        matches!(self, Self::Fit)
410    }
411
412    pub fn stack(self, other: Self) -> Self {
413        self.merge_with(other, FluidConstraint::stack)
414    }
415
416    pub fn cross(self, other: Self) -> Self {
417        self.merge_with(other, FluidConstraint::cross)
418    }
419
420    fn sizing(self) -> LengthSizing {
421        match self {
422            Self::Fill => LengthSizing::Fill(1),
423            Self::FillPortion(portion) => LengthSizing::Fill(portion.max(1)),
424            Self::Shrink => LengthSizing::Shrink,
425            _ => LengthSizing::Fit,
426        }
427    }
428
429    fn merge_with(
430        self,
431        other: Self,
432        merge: impl Fn(FluidConstraint, FluidConstraint) -> FluidConstraint,
433    ) -> Self {
434        match (self, other) {
435            (Self::Shrink | Self::Fixed(_) | Self::Fill | Self::FillPortion(_), _) => self,
436            (Self::Fluid(left), Self::Fluid(right)) => Self::Fluid(merge(left, right)),
437            (
438                Self::Fluid(left),
439                Self::Bounded {
440                    bounds,
441                    sizing: LengthSizing::Fill(_),
442                },
443            ) => Self::Fluid(merge(left, bounds.constraint())),
444            (Self::Fluid(constraint), _) => Self::Fluid(constraint),
445            (Self::Bounded { bounds, sizing }, _) => Self::Bounded { bounds, sizing },
446            (
447                _,
448                Self::Bounded {
449                    bounds,
450                    sizing: LengthSizing::Fill(_),
451                },
452            ) => Self::Fluid(bounds.constraint()),
453            (_, Self::Fluid(constraint)) => Self::Fluid(constraint),
454            (_, Self::Fill | Self::FillPortion(_)) => Self::Fill,
455            _ => Self::Fit,
456        }
457    }
458
459    fn resolve_axis(self, min: f32, max: f32, intrinsic: f32, compressed: bool) -> f32 {
460        let min = finite_px(min, 0.0);
461        let max = if max.is_finite() {
462            max.max(min)
463        } else {
464            f32::INFINITY
465        };
466        let intrinsic = finite_px(intrinsic, min);
467        match self {
468            Self::Fill
469            | Self::FillPortion(_)
470            | Self::Fluid(_)
471            | Self::Bounded {
472                sizing: LengthSizing::Fill(_),
473                ..
474            } if !compressed => {
475                if max.is_finite() {
476                    max
477                } else {
478                    intrinsic.max(min)
479                }
480            }
481            Self::Fixed(px) => finite_px(px, min).clamp(min, max),
482            Self::Bounded { bounds, sizing } => {
483                let mut bounded_min = min;
484                let mut bounded_max = max;
485                bounds.apply(&mut bounded_min, &mut bounded_max);
486                match sizing {
487                    LengthSizing::Fill(_) if !compressed => bounded_max,
488                    LengthSizing::Shrink => {
489                        bounded_min.min(intrinsic).clamp(bounded_min, bounded_max)
490                    }
491                    _ => intrinsic.clamp(bounded_min, bounded_max),
492                }
493            }
494            Self::Shrink => min.min(intrinsic).clamp(min, max),
495            _ => intrinsic.clamp(min, max),
496        }
497    }
498}
499
500impl From<f32> for Length {
501    fn from(value: f32) -> Self {
502        Self::Fixed(value)
503    }
504}
505
506/// Min/max constraints used by renderer-neutral layout computations.
507#[derive(Clone, Copy, Debug, PartialEq)]
508#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
509pub struct LayoutLimits {
510    pub min: Size,
511    pub max: Size,
512    pub compress_width: bool,
513    pub compress_height: bool,
514}
515
516impl LayoutLimits {
517    pub const NONE: Self = Self {
518        min: Size::ZERO,
519        max: Size::INFINITE,
520        compress_width: false,
521        compress_height: false,
522    };
523
524    pub const fn new(min: Size, max: Size) -> Self {
525        Self {
526            min,
527            max,
528            compress_width: false,
529            compress_height: false,
530        }
531    }
532
533    pub fn width(mut self, width: impl Into<Length>) -> Self {
534        match width.into() {
535            Length::Shrink => self.compress_width = true,
536            Length::Fit | Length::Fluid(_) => self.compress_width = false,
537            Length::Fixed(px) => {
538                let px = finite_px(px, self.min.width).clamp(self.min.width, self.max.width);
539                self.min.width = px;
540                self.max.width = px;
541                self.compress_width = false;
542            }
543            Length::Bounded { bounds, sizing } => {
544                bounds.apply(&mut self.min.width, &mut self.max.width);
545                if matches!(sizing, LengthSizing::Shrink) {
546                    self.compress_width = true;
547                } else if matches!(sizing, LengthSizing::Fit) {
548                    self.compress_width = false;
549                }
550            }
551            Length::Fill | Length::FillPortion(_) => {}
552        }
553        self
554    }
555
556    pub fn height(mut self, height: impl Into<Length>) -> Self {
557        match height.into() {
558            Length::Shrink => self.compress_height = true,
559            Length::Fit | Length::Fluid(_) => self.compress_height = false,
560            Length::Fixed(px) => {
561                let px = finite_px(px, self.min.height).clamp(self.min.height, self.max.height);
562                self.min.height = px;
563                self.max.height = px;
564                self.compress_height = false;
565            }
566            Length::Bounded { bounds, sizing } => {
567                bounds.apply(&mut self.min.height, &mut self.max.height);
568                if matches!(sizing, LengthSizing::Shrink) {
569                    self.compress_height = true;
570                } else if matches!(sizing, LengthSizing::Fit) {
571                    self.compress_height = false;
572                }
573            }
574            Length::Fill | Length::FillPortion(_) => {}
575        }
576        self
577    }
578
579    pub fn shrink(self, size: Size) -> Self {
580        let size = size.sanitized();
581        Self {
582            min: Size::new(
583                (self.min.width - size.width).max(0.0),
584                (self.min.height - size.height).max(0.0),
585            ),
586            max: Size::new(
587                (self.max.width - size.width).max(0.0),
588                (self.max.height - size.height).max(0.0),
589            ),
590            compress_width: self.compress_width,
591            compress_height: self.compress_height,
592        }
593    }
594
595    pub fn loose(self) -> Self {
596        Self {
597            min: Size::ZERO,
598            ..self
599        }
600    }
601
602    pub fn resolve(
603        self,
604        width: impl Into<Length>,
605        height: impl Into<Length>,
606        intrinsic: Size,
607    ) -> Size {
608        Size::new(
609            width.into().resolve_axis(
610                self.min.width,
611                self.max.width,
612                intrinsic.width,
613                self.compress_width,
614            ),
615            height.into().resolve_axis(
616                self.min.height,
617                self.max.height,
618                intrinsic.height,
619                self.compress_height,
620            ),
621        )
622    }
623}
624
625#[derive(Clone, Copy, Debug, PartialEq, Eq)]
626#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
627#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
628pub enum LayoutAxis {
629    Horizontal,
630    Vertical,
631}
632
633#[derive(Clone, Copy, Debug, PartialEq, Eq)]
634#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
635#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
636pub enum LayoutAlign {
637    Start,
638    Center,
639    End,
640    Stretch,
641}
642
643#[derive(Clone, Debug, PartialEq)]
644#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
645pub struct LayoutItem {
646    pub width: Length,
647    pub height: Length,
648    pub intrinsic_size: Size,
649    pub min_size: Size,
650}
651
652impl LayoutItem {
653    pub fn new(width: impl Into<Length>, height: impl Into<Length>, intrinsic_size: Size) -> Self {
654        Self {
655            width: width.into(),
656            height: height.into(),
657            intrinsic_size: intrinsic_size.sanitized(),
658            min_size: Size::ZERO,
659        }
660    }
661
662    pub fn with_min_size(mut self, min_size: Size) -> Self {
663        self.min_size = min_size.sanitized();
664        self
665    }
666}
667
668#[derive(Clone, Debug, Default, PartialEq)]
669#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
670pub struct LayoutNode {
671    pub bounds: Rect,
672    pub children: Vec<LayoutNode>,
673}
674
675impl LayoutNode {
676    pub fn new(bounds: Rect) -> Self {
677        Self {
678            bounds: sanitize_rect(bounds),
679            children: Vec::new(),
680        }
681    }
682
683    pub fn with_children(bounds: Rect, children: Vec<LayoutNode>) -> Self {
684        Self {
685            bounds: sanitize_rect(bounds),
686            children,
687        }
688    }
689
690    pub fn size(&self) -> Size {
691        Size::new(self.bounds.width, self.bounds.height)
692    }
693
694    pub fn translate(mut self, delta: Vec2) -> Self {
695        self.bounds = self.bounds.translate(delta);
696        self.children = self
697            .children
698            .into_iter()
699            .map(|child| child.translate(delta))
700            .collect();
701        self
702    }
703}
704
705#[derive(Clone, Copy, Debug, PartialEq)]
706#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
707pub struct FlexLayout {
708    pub axis: LayoutAxis,
709    pub bounds: Rect,
710    pub gap_px: f32,
711    pub padding: Insets,
712    pub align: LayoutAlign,
713}
714
715impl FlexLayout {
716    pub fn row(bounds: Rect) -> Self {
717        Self::new(LayoutAxis::Horizontal, bounds)
718    }
719
720    pub fn column(bounds: Rect) -> Self {
721        Self::new(LayoutAxis::Vertical, bounds)
722    }
723
724    pub fn new(axis: LayoutAxis, bounds: Rect) -> Self {
725        Self {
726            axis,
727            bounds: sanitize_rect(bounds),
728            gap_px: 0.0,
729            padding: Insets::default(),
730            align: LayoutAlign::Stretch,
731        }
732    }
733
734    pub fn with_gap(mut self, gap_px: f32) -> Self {
735        self.gap_px = finite_px(gap_px, 0.0).clamp(0.0, 512.0);
736        self
737    }
738
739    pub fn with_padding(mut self, padding: Insets) -> Self {
740        self.padding = if padding.is_finite() {
741            padding
742        } else {
743            Insets::default()
744        };
745        self
746    }
747
748    pub fn with_align(mut self, align: LayoutAlign) -> Self {
749        self.align = align;
750        self
751    }
752
753    pub fn resolve(self, items: &[LayoutItem]) -> LayoutNode {
754        let content = sanitize_rect(self.bounds).inset(self.padding);
755        if content.is_empty() || items.is_empty() {
756            return LayoutNode::new(content);
757        }
758
759        let gap = finite_px(self.gap_px, 0.0).clamp(0.0, 512.0);
760        let total_gap = gap * items.len().saturating_sub(1) as f32;
761        let available_main = (main_len(content, self.axis) - total_gap).max(0.0);
762        let mut fixed_main = 0.0;
763        let mut fill_factor = 0_u32;
764
765        for item in items {
766            let length = item.main_length(self.axis);
767            let factor = length.fill_factor();
768            if factor == 0 {
769                fixed_main += item.resolve_main(self.axis, available_main);
770            } else {
771                fill_factor = fill_factor.saturating_add(factor as u32);
772            }
773        }
774
775        let remaining = (available_main - fixed_main).max(0.0);
776        let mut cursor = main_start(content, self.axis);
777        let mut children = Vec::with_capacity(items.len());
778        for item in items {
779            let main = if item.main_length(self.axis).fill_factor() == 0 {
780                item.resolve_main(self.axis, available_main)
781            } else if fill_factor == 0 {
782                0.0
783            } else {
784                remaining * item.main_length(self.axis).fill_factor() as f32 / fill_factor as f32
785            };
786            let cross = item.resolve_cross(self.axis, cross_len(content, self.axis), self.align);
787            let cross_origin = align_cross(content, self.axis, cross, self.align);
788            let rect = rect_from_axes(content, self.axis, cursor, main, cross_origin, cross);
789            children.push(LayoutNode::new(rect));
790            cursor += main + gap;
791        }
792
793        LayoutNode::with_children(content, children)
794    }
795}
796
797impl LayoutItem {
798    fn main_length(&self, axis: LayoutAxis) -> Length {
799        match axis {
800            LayoutAxis::Horizontal => self.width,
801            LayoutAxis::Vertical => self.height,
802        }
803    }
804
805    fn cross_length(&self, axis: LayoutAxis) -> Length {
806        match axis {
807            LayoutAxis::Horizontal => self.height,
808            LayoutAxis::Vertical => self.width,
809        }
810    }
811
812    fn intrinsic_main(&self, axis: LayoutAxis) -> f32 {
813        match axis {
814            LayoutAxis::Horizontal => self.intrinsic_size.width,
815            LayoutAxis::Vertical => self.intrinsic_size.height,
816        }
817    }
818
819    fn intrinsic_cross(&self, axis: LayoutAxis) -> f32 {
820        match axis {
821            LayoutAxis::Horizontal => self.intrinsic_size.height,
822            LayoutAxis::Vertical => self.intrinsic_size.width,
823        }
824    }
825
826    fn min_main(&self, axis: LayoutAxis) -> f32 {
827        match axis {
828            LayoutAxis::Horizontal => self.min_size.width,
829            LayoutAxis::Vertical => self.min_size.height,
830        }
831    }
832
833    fn min_cross(&self, axis: LayoutAxis) -> f32 {
834        match axis {
835            LayoutAxis::Horizontal => self.min_size.height,
836            LayoutAxis::Vertical => self.min_size.width,
837        }
838    }
839
840    fn resolve_main(&self, axis: LayoutAxis, available: f32) -> f32 {
841        self.main_length(axis)
842            .resolve_axis(
843                self.min_main(axis),
844                available.max(self.min_main(axis)),
845                self.intrinsic_main(axis),
846                false,
847            )
848            .max(0.0)
849    }
850
851    fn resolve_cross(&self, axis: LayoutAxis, available: f32, align: LayoutAlign) -> f32 {
852        if matches!(align, LayoutAlign::Stretch) && self.cross_length(axis).fill_factor() != 0 {
853            return available.max(0.0);
854        }
855        self.cross_length(axis)
856            .resolve_axis(
857                self.min_cross(axis),
858                available.max(self.min_cross(axis)),
859                self.intrinsic_cross(axis),
860                false,
861            )
862            .max(0.0)
863    }
864}
865
866pub fn layout_row(bounds: Rect, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
867    FlexLayout::row(bounds).with_gap(gap_px).resolve(items)
868}
869
870pub fn layout_column(bounds: Rect, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
871    FlexLayout::column(bounds).with_gap(gap_px).resolve(items)
872}
873
874pub fn layout_stack(bounds: Rect, items: &[LayoutItem]) -> LayoutNode {
875    let bounds = sanitize_rect(bounds);
876    let children = items.iter().map(|_| LayoutNode::new(bounds)).collect();
877    LayoutNode::with_children(bounds, children)
878}
879
880pub fn layout_grid(bounds: Rect, columns: usize, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
881    let bounds = sanitize_rect(bounds);
882    if bounds.is_empty() || columns == 0 || items.is_empty() {
883        return LayoutNode::new(bounds);
884    }
885    let columns = columns.min(items.len()).max(1);
886    let rows = items.len().div_ceil(columns).max(1);
887    let gap = finite_px(gap_px, 0.0).clamp(0.0, 512.0);
888    let cell_width =
889        ((bounds.width - gap * columns.saturating_sub(1) as f32) / columns as f32).max(0.0);
890    let cell_height =
891        ((bounds.height - gap * rows.saturating_sub(1) as f32) / rows as f32).max(0.0);
892    let children = items
893        .iter()
894        .enumerate()
895        .map(|(index, _)| {
896            let column = index % columns;
897            let row = index / columns;
898            LayoutNode::new(Rect::new(
899                bounds.x + (cell_width + gap) * column as f32,
900                bounds.y + (cell_height + gap) * row as f32,
901                cell_width,
902                cell_height,
903            ))
904        })
905        .collect();
906    LayoutNode::with_children(bounds, children)
907}
908
909pub fn layout_space(
910    bounds: Rect,
911    width: impl Into<Length>,
912    height: impl Into<Length>,
913    intrinsic: Size,
914) -> LayoutNode {
915    let bounds = sanitize_rect(bounds);
916    let limits = LayoutLimits::new(Size::ZERO, Size::new(bounds.width, bounds.height));
917    let size = limits.resolve(width, height, intrinsic);
918    LayoutNode::new(Rect::new(bounds.x, bounds.y, size.width, size.height))
919}
920
921fn main_len(rect: Rect, axis: LayoutAxis) -> f32 {
922    match axis {
923        LayoutAxis::Horizontal => rect.width,
924        LayoutAxis::Vertical => rect.height,
925    }
926}
927
928fn cross_len(rect: Rect, axis: LayoutAxis) -> f32 {
929    match axis {
930        LayoutAxis::Horizontal => rect.height,
931        LayoutAxis::Vertical => rect.width,
932    }
933}
934
935fn main_start(rect: Rect, axis: LayoutAxis) -> f32 {
936    match axis {
937        LayoutAxis::Horizontal => rect.x,
938        LayoutAxis::Vertical => rect.y,
939    }
940}
941
942fn align_cross(rect: Rect, axis: LayoutAxis, cross: f32, align: LayoutAlign) -> f32 {
943    let start = match axis {
944        LayoutAxis::Horizontal => rect.y,
945        LayoutAxis::Vertical => rect.x,
946    };
947    let available = cross_len(rect, axis);
948    match align {
949        LayoutAlign::Start | LayoutAlign::Stretch => start,
950        LayoutAlign::Center => start + (available - cross).max(0.0) * 0.5,
951        LayoutAlign::End => start + (available - cross).max(0.0),
952    }
953}
954
955fn rect_from_axes(
956    _content: Rect,
957    axis: LayoutAxis,
958    main: f32,
959    main_len: f32,
960    cross: f32,
961    cross_len: f32,
962) -> Rect {
963    match axis {
964        LayoutAxis::Horizontal => Rect::new(main, cross, main_len, cross_len),
965        LayoutAxis::Vertical => Rect::new(cross, main, cross_len, main_len),
966    }
967}
968
969fn sanitize_rect(rect: Rect) -> Rect {
970    if rect.is_finite() && rect.width >= 0.0 && rect.height >= 0.0 {
971        rect
972    } else {
973        Rect::ZERO
974    }
975}
976
977fn finite_px(value: f32, fallback: f32) -> f32 {
978    if value.is_finite() {
979        value.max(0.0)
980    } else if fallback.is_finite() {
981        fallback.max(0.0)
982    } else {
983        0.0
984    }
985}
986
987#[derive(Clone, Copy, Debug, PartialEq)]
988#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
989pub struct AppLayout {
990    pub root: Rect,
991    pub header: Rect,
992    pub main: Rect,
993    pub sidebar: Rect,
994    pub prompt: Rect,
995    pub status: Rect,
996    pub sidebar_collapsed: bool,
997}
998
999pub fn compute_shell_layout(viewport: Rect) -> AppLayout {
1000    let viewport = if viewport.is_finite() && !viewport.is_empty() {
1001        viewport
1002    } else {
1003        Rect::ZERO
1004    };
1005    let outer_gap = 16.0;
1006    let inner_gap = 12.0;
1007    let status_height = 32.0;
1008    let prompt_height = if viewport.height < 680.0 { 92.0 } else { 126.0 };
1009    let header_height = if viewport.height < 620.0 { 0.0 } else { 58.0 };
1010
1011    let content = viewport.inset(Insets::all(outer_gap));
1012    if content.height < status_height + inner_gap + prompt_height || content.width <= 0.0 {
1013        return AppLayout {
1014            root: viewport,
1015            header: Rect::ZERO,
1016            main: Rect::ZERO,
1017            sidebar: Rect::ZERO,
1018            prompt: Rect::ZERO,
1019            status: Rect::ZERO,
1020            sidebar_collapsed: true,
1021        };
1022    }
1023
1024    let sidebar_collapsed = viewport.width < 980.0;
1025    let sidebar_width = if sidebar_collapsed {
1026        0.0
1027    } else {
1028        (viewport.width * 0.27).clamp(300.0, 440.0)
1029    };
1030
1031    let header = Rect::new(content.x, content.y, content.width, header_height);
1032    let status = Rect::new(
1033        content.x,
1034        content.bottom() - status_height,
1035        content.width,
1036        status_height,
1037    );
1038    let prompt = Rect::new(
1039        content.x,
1040        status.y - inner_gap - prompt_height,
1041        content.width,
1042        prompt_height,
1043    );
1044    let body_y = header.y + header.height + if header_height > 0.0 { inner_gap } else { 0.0 };
1045    let body_h = (prompt.y - inner_gap - body_y).max(0.0);
1046    let sidebar = if sidebar_collapsed {
1047        Rect::ZERO
1048    } else {
1049        Rect::new(
1050            content.right() - sidebar_width,
1051            body_y,
1052            sidebar_width,
1053            body_h,
1054        )
1055    };
1056    let main_width = if sidebar_collapsed {
1057        content.width
1058    } else {
1059        (content.width - sidebar_width - inner_gap).max(0.0)
1060    };
1061    let main = Rect::new(content.x, body_y, main_width, body_h);
1062
1063    AppLayout {
1064        root: viewport,
1065        header,
1066        main,
1067        sidebar,
1068        prompt,
1069        status,
1070        sidebar_collapsed,
1071    }
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076    use super::*;
1077
1078    #[test]
1079    fn layout_collapses_sidebar_on_small_width() {
1080        let layout = compute_shell_layout(Rect::new(0.0, 0.0, 800.0, 700.0));
1081
1082        assert!(layout.sidebar_collapsed);
1083        assert!(layout.sidebar.is_empty());
1084        assert!(layout.main.width > 700.0);
1085    }
1086
1087    #[test]
1088    fn layout_keeps_prompt_and_status_visible() {
1089        let layout = compute_shell_layout(Rect::new(0.0, 0.0, 1440.0, 900.0));
1090
1091        assert!(layout.main.height > 500.0);
1092        assert!(layout.prompt.y > layout.main.y);
1093        assert!(layout.status.y > layout.prompt.y);
1094    }
1095
1096    #[test]
1097    fn layout_returns_empty_children_for_malformed_or_tiny_viewports() {
1098        let malformed = compute_shell_layout(Rect::new(f32::NAN, 0.0, 100.0, 100.0));
1099        let tiny = compute_shell_layout(Rect::new(0.0, 0.0, 64.0, 80.0));
1100
1101        assert_eq!(malformed.root, Rect::ZERO);
1102        assert_eq!(malformed.prompt, Rect::ZERO);
1103        assert_eq!(tiny.root, Rect::new(0.0, 0.0, 64.0, 80.0));
1104        assert_eq!(tiny.status, Rect::ZERO);
1105        assert!(tiny.sidebar_collapsed);
1106    }
1107
1108    #[test]
1109    fn length_limits_resolve_fixed_fill_and_bounded_lengths() {
1110        let limits = LayoutLimits::new(Size::new(20.0, 10.0), Size::new(300.0, 120.0));
1111
1112        assert_eq!(Length::Fixed(500.0).fill_factor(), 0);
1113        assert_eq!(Length::FillPortion(3).fill_factor(), 3);
1114        assert_eq!(
1115            limits
1116                .resolve(Length::Fixed(500.0), Length::Fit, Size::new(50.0, 30.0))
1117                .width,
1118            300.0
1119        );
1120        assert_eq!(
1121            limits
1122                .resolve(Length::Fill, Length::Fit, Size::new(50.0, 30.0))
1123                .width,
1124            300.0
1125        );
1126        assert_eq!(
1127            limits
1128                .resolve(Length::Fit.max(80.0), Length::Fit, Size::new(140.0, 30.0))
1129                .width,
1130            80.0
1131        );
1132        assert!(limits.width(Length::Shrink).compress_width);
1133    }
1134
1135    #[test]
1136    fn flex_row_distributes_fill_portions_after_fixed_space() {
1137        let items = [
1138            LayoutItem::new(100.0, Length::Fill, Size::new(100.0, 20.0)),
1139            LayoutItem::new(Length::FillPortion(1), Length::Fill, Size::new(10.0, 20.0)),
1140            LayoutItem::new(Length::FillPortion(2), Length::Fill, Size::new(10.0, 20.0)),
1141        ];
1142        let node = layout_row(Rect::new(0.0, 0.0, 320.0, 40.0), 10.0, &items);
1143
1144        assert_eq!(node.children.len(), 3);
1145        assert_eq!(node.children[0].bounds, Rect::new(0.0, 0.0, 100.0, 40.0));
1146        assert!((node.children[1].bounds.width - 66.666_67).abs() < 0.01);
1147        assert!((node.children[2].bounds.width - 133.333_34).abs() < 0.01);
1148        assert!((node.children[2].bounds.x - 186.666_67).abs() < 0.01);
1149    }
1150
1151    #[test]
1152    fn grid_stack_and_space_layouts_are_finite() {
1153        let items = [
1154            LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
1155            LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
1156            LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
1157        ];
1158        let grid = layout_grid(Rect::new(0.0, 0.0, 210.0, 100.0), 2, 10.0, &items);
1159        let stack = layout_stack(Rect::new(4.0, 5.0, 30.0, 20.0), &items[..2]);
1160        let space = layout_space(
1161            Rect::new(0.0, 0.0, 80.0, 60.0),
1162            Length::Fill,
1163            12.0,
1164            Size::ZERO,
1165        );
1166
1167        assert_eq!(grid.children.len(), 3);
1168        assert_eq!(grid.children[0].bounds, Rect::new(0.0, 0.0, 100.0, 45.0));
1169        assert_eq!(grid.children[2].bounds, Rect::new(0.0, 55.0, 100.0, 45.0));
1170        assert_eq!(stack.children[0].bounds, Rect::new(4.0, 5.0, 30.0, 20.0));
1171        assert_eq!(space.bounds, Rect::new(0.0, 0.0, 80.0, 12.0));
1172    }
1173
1174    #[test]
1175    fn rect_intersection_union_and_translation_are_stable() {
1176        let a = Rect::new(10.0, 20.0, 100.0, 80.0);
1177        let b = Rect::new(80.0, 60.0, 80.0, 80.0);
1178
1179        assert_eq!(a.center(), Vec2::new(60.0, 60.0));
1180        assert_eq!(
1181            a.translate(Vec2::new(5.0, -10.0)),
1182            Rect::new(15.0, 10.0, 100.0, 80.0)
1183        );
1184        assert_eq!(a.intersection(b), Some(Rect::new(80.0, 60.0, 30.0, 40.0)));
1185        assert_eq!(a.union(b), Rect::new(10.0, 20.0, 150.0, 120.0));
1186        assert_eq!(a.intersection(Rect::new(120.0, 20.0, 10.0, 10.0)), None);
1187    }
1188
1189    #[test]
1190    fn rect_clamp_keeps_popovers_inside_bounds() {
1191        let bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
1192
1193        assert_eq!(
1194            Rect::new(260.0, 180.0, 90.0, 60.0).clamp_within(bounds),
1195            Rect::new(210.0, 140.0, 90.0, 60.0)
1196        );
1197        assert_eq!(
1198            Rect::new(-20.0, -30.0, 360.0, 260.0).clamp_within(bounds),
1199            bounds
1200        );
1201        assert_eq!(Rect::ZERO.clamp_within(bounds), Rect::ZERO);
1202    }
1203
1204    #[test]
1205    fn rect_clamp_handles_tiny_rounding_ranges_without_panic() {
1206        let x = f32::from_bits(0x2042_a815);
1207        let width = f32::from_bits(0x2390_0b54);
1208        let bounds = Rect::new(x, 0.0, width, 1.0);
1209        let clamped = bounds.clamp_within(bounds);
1210
1211        assert!(clamped.is_finite());
1212        assert_eq!(clamped.x, bounds.x);
1213    }
1214
1215    #[test]
1216    fn rect_helpers_tolerate_non_finite_public_values() {
1217        let valid = Rect::new(0.0, 0.0, 10.0, 10.0);
1218        let malformed = Rect::new(f32::NAN, 0.0, 10.0, 10.0);
1219        let overflowing = Rect::new(f32::MAX, 0.0, f32::MAX, 10.0);
1220
1221        assert!(!malformed.is_finite());
1222        assert!(!overflowing.is_finite());
1223        assert_eq!(malformed.intersection(valid), None);
1224        assert_eq!(overflowing.intersection(valid), None);
1225        assert_eq!(malformed.union(valid), valid);
1226        assert_eq!(overflowing.union(valid), valid);
1227        assert_eq!(valid.clamp_within(malformed), Rect::ZERO);
1228        assert_eq!(valid.clamp_within(overflowing), Rect::ZERO);
1229        assert_eq!(valid.inset(Insets::all(f32::INFINITY)), Rect::ZERO);
1230        assert_eq!(overflowing.inset(Insets::all(1.0)), Rect::ZERO);
1231        assert!(!valid.contains(f32::NAN, 1.0));
1232        assert!(!malformed.contains(1.0, 1.0));
1233        assert!(!overflowing.contains(f32::MAX, 1.0));
1234    }
1235}