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}