ailloli_ui_core/style/
layout_resolve.rs

1//! Resolution of declarative lengths against parent layout constraints.
2
3use crate::geometry::{Constraints, Size};
4use crate::style::Length;
5
6use super::LayoutStyle;
7
8/// Resolved explicit sizes from [`LayoutStyle`] (fill / px / percent).
9///
10/// `None` on an axis means intrinsic sizing (children sum, text measure, etc.).
11///
12/// # Examples
13///
14/// ```
15/// use ailloli_ui_core::{Constraints, Size};
16/// use ailloli_ui_core::style::{LayoutStyle, resolve_widget_size};
17/// let size = resolve_widget_size(Size::new(20.0, 10.0), LayoutStyle::new().width(40.0), Constraints::loose(100.0, 100.0));
18/// assert_eq!(size, Size::new(40.0, 10.0));
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Default)]
21pub struct ResolvedLayout {
22    /// Explicit width in logical pixels, or `None` for intrinsic width.
23    pub width: Option<f32>,
24    /// Explicit height in logical pixels, or `None` for intrinsic height.
25    pub height: Option<f32>,
26    /// Resolved minimum width, or `None` when no minimum was specified.
27    pub min_width: Option<f32>,
28    /// Resolved maximum width, or `None` when no maximum was specified.
29    pub max_width: Option<f32>,
30    /// Resolved minimum height, or `None` when no minimum was specified.
31    pub min_height: Option<f32>,
32    /// Resolved maximum height, or `None` when no maximum was specified.
33    pub max_height: Option<f32>,
34}
35
36impl ResolvedLayout {
37    /// Combines intrinsic size with explicit axes and min/max bounds under `parent`.
38    ///
39    /// Per-widget minimums are applied before maximums. An explicit axis is
40    /// clamped to the ordered parent interval; an intrinsic axis is capped only
41    /// at the parent maximum and is not expanded to the parent minimum.
42    ///
43    /// # Panics
44    ///
45    /// May panic when an explicit axis is clamped against parent bounds that
46    /// remain NaN after ordering.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use ailloli_ui_core::Constraints;
52    /// use ailloli_ui_core::style::ResolvedLayout;
53    /// let resolved = ResolvedLayout { width: Some(40.0), ..ResolvedLayout::default() };
54    /// assert_eq!(resolved.size(10.0, 20.0, Constraints::loose(100.0, 100.0)), (40.0, 20.0));
55    /// ```
56    pub fn size(self, intrinsic_w: f32, intrinsic_h: f32, parent: Constraints) -> (f32, f32) {
57        let mut w = self.width.unwrap_or(intrinsic_w);
58        let mut h = self.height.unwrap_or(intrinsic_h);
59
60        if let Some(min_w) = self.min_width {
61            w = w.max(min_w);
62        }
63        if let Some(max_w) = self.max_width {
64            w = w.min(max_w);
65        }
66        if let Some(min_h) = self.min_height {
67            h = h.max(min_h);
68        }
69        if let Some(max_h) = self.max_height {
70            h = h.min(max_h);
71        }
72
73        // Auto axes: cap at parent max only (do not expand to parent min).
74        let parent_w_min = parent.min_w.min(parent.max_w);
75        let parent_w_max = parent.max_w.max(parent.min_w);
76        let parent_h_min = parent.min_h.min(parent.max_h);
77        let parent_h_max = parent.max_h.max(parent.min_h);
78
79        if self.width.is_none() {
80            w = w.min(parent_w_max);
81        } else {
82            w = w.clamp(parent_w_min, parent_w_max);
83        }
84        if self.height.is_none() {
85            h = h.min(parent_h_max);
86        } else {
87            h = h.clamp(parent_h_min, parent_h_max);
88        }
89
90        (w, h)
91    }
92}
93
94/// Resolves one declarative bound against parent availability.
95fn resolve_bound(length: Length, available: f32) -> Option<f32> {
96    length.resolve(available)
97}
98
99impl LayoutStyle {
100    /// Resolves width, height, and min/max lengths against parent maximums.
101    ///
102    /// Every percentage and fill value uses the corresponding stored parent
103    /// maximum. Margin and padding do not participate in this operation.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use ailloli_ui_core::Constraints;
109    /// use ailloli_ui_core::style::LayoutStyle;
110    /// assert_eq!(LayoutStyle::new().width(50.0).resolve(Constraints::loose(100.0, 80.0)).width, Some(50.0));
111    /// ```
112    pub fn resolve(self, parent: Constraints) -> ResolvedLayout {
113        ResolvedLayout {
114            width: resolve_bound(self.width, parent.max_w),
115            height: resolve_bound(self.height, parent.max_h),
116            min_width: resolve_bound(self.min_width, parent.max_w),
117            max_width: resolve_bound(self.max_width, parent.max_w),
118            min_height: resolve_bound(self.min_height, parent.max_h),
119            max_height: resolve_bound(self.max_height, parent.max_h),
120        }
121    }
122
123    /// Derives normalized constraints for children from an already inner box.
124    ///
125    /// The caller is responsible for removing margin/padding before passing
126    /// `inner`. Explicit dimensions narrow both bounds; min/max declarations
127    /// then tighten their matching side. The final intervals are reordered if
128    /// the declarations cross.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use ailloli_ui_core::Constraints;
134    /// use ailloli_ui_core::style::LayoutStyle;
135    /// let children = LayoutStyle::new().width(40.0).constraints_for_children(Constraints::loose(100.0, 80.0));
136    /// assert_eq!((children.min_w, children.max_w), (40.0, 40.0));
137    /// ```
138    pub fn constraints_for_children(self, inner: Constraints) -> Constraints {
139        let resolved = self.resolve(inner);
140        let mut max_w = inner.max_w;
141        let mut max_h = inner.max_h;
142        let mut min_w = inner.min_w;
143        let mut min_h = inner.min_h;
144
145        if let Some(w) = resolved.width {
146            max_w = w.min(max_w);
147            min_w = w.max(min_w);
148        }
149        if let Some(h) = resolved.height {
150            max_h = h.min(max_h);
151            min_h = h.max(min_h);
152        }
153        if let Some(v) = resolved.min_width {
154            min_w = v.max(min_w);
155        }
156        if let Some(v) = resolved.max_width {
157            max_w = v.min(max_w);
158        }
159        if let Some(v) = resolved.min_height {
160            min_h = v.max(min_h);
161        }
162        if let Some(v) = resolved.max_height {
163            max_h = v.min(max_h);
164        }
165
166        Constraints {
167            min_w,
168            max_w,
169            min_h,
170            max_h,
171        }
172        .normalized()
173    }
174}
175
176/// Applies a [`LayoutStyle`] to an intrinsic size under parent constraints.
177///
178/// The returned [`Size`] uses logical pixels and follows
179/// [`ResolvedLayout::size`] for explicit versus intrinsic parent-min behavior.
180///
181/// # Examples
182///
183/// ```
184/// use ailloli_ui_core::{Constraints, Size};
185/// use ailloli_ui_core::style::{LayoutStyle, resolve_widget_size};
186/// assert_eq!(resolve_widget_size(Size::new(20.0, 10.0), LayoutStyle::new().height(30.0), Constraints::loose(100.0, 100.0)), Size::new(20.0, 30.0));
187/// ```
188pub fn resolve_widget_size(intrinsic: Size, layout: LayoutStyle, parent: Constraints) -> Size {
189    let resolved = layout.resolve(parent);
190    let (w, h) = resolved.size(intrinsic.w, intrinsic.h, parent);
191    Size::new(w, h)
192}