tui_lipan/style/layout.rs
1/// Layout constraints for stack sizing.
2///
3/// These constraints control how elements behave during layout:
4///
5/// - `min_w`/`min_h`: Hard minimum sizes (prevents "squishing"). Accepts any
6/// [`Length`] variant:
7/// - `Px(n)` - absolute minimum in cells (the default is `Px(0)`, meaning no minimum).
8/// - `Percent(p)` - minimum as a fraction of the parent's available size, resolved
9/// at layout time. During measurement passes where the parent size is not yet
10/// known, `Percent` mins have no effect (treated as 0).
11/// - `Auto` / `Flex(_)` - treated as 0 (no minimum).
12///
13/// These are hard author constraints only. Intrinsic content floors such as
14/// min-content and max-content are reported by the layout measurement query,
15/// not encoded in `min_w`/`min_h`.
16///
17/// - `max_w`/`max_h`: Hard maximum sizes, capped after natural size computation.
18/// Same [`Length`] variants; `None` means no cap, `Auto`/`Flex` also mean no cap.
19/// `Percent` is resolved against the parent's offered size at measurement time.
20///
21/// - `focus_min_w`/`focus_min_h`: Absolute minimum sizes (cells) when this element
22/// is focused. These are always `u16` - focus sizing is context-driven, not
23/// percentage-relative.
24///
25/// - `collapse_w`/`collapse_h` / `force_compact`: Space-pressure sizing (accordion
26/// focus mode). Always absolute `u16` values.
27///
28/// - `reflows`: The element's cross-axis size can change when its main-axis
29/// allocation changes (for example a wrapping row flow).
30///
31/// - `shrink_priority`: Whether this element should yield space before normal
32/// siblings under main-axis pressure.
33///
34/// Priority during layout: `min` > `max` > natural size.
35/// If the resolved min exceeds the resolved max, the minimum wins.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37pub struct LayoutConstraints {
38 /// Minimum width used for flexible layout.
39 pub min_w: Length,
40 /// Minimum height used for flexible layout.
41 pub min_h: Length,
42 /// Maximum width (clamped after natural size computation).
43 pub max_w: Option<Length>,
44 /// Maximum height (clamped after natural size computation).
45 pub max_h: Option<Length>,
46 /// Minimum width when this element is focused.
47 pub focus_min_w: u16,
48 /// Minimum height when this element is focused.
49 pub focus_min_h: u16,
50 /// Optional collapsed width when space is constrained.
51 pub collapse_w: Option<u16>,
52 /// Optional collapsed height when space is constrained.
53 pub collapse_h: Option<u16>,
54 /// Force compact sizing regardless of available space.
55 pub force_compact: bool,
56 /// Cross-axis size depends on the final main-axis allocation.
57 pub reflows: bool,
58 /// Main-axis shrink order under pressure.
59 pub shrink_priority: ShrinkPriority,
60}
61
62/// Main-axis shrink priority for stack children.
63#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
64pub enum ShrinkPriority {
65 /// Normal stack shrink behavior.
66 #[default]
67 Normal,
68 /// Yield space before normal siblings; useful for lower-priority wrapping
69 /// groups that may truncate before rigid siblings wrap or collapse.
70 First,
71}
72
73impl Default for LayoutConstraints {
74 fn default() -> Self {
75 Self {
76 min_w: Length::Px(0),
77 min_h: Length::Px(0),
78 max_w: None,
79 max_h: None,
80 focus_min_w: 0,
81 focus_min_h: 0,
82 collapse_w: None,
83 collapse_h: None,
84 force_compact: false,
85 reflows: false,
86 shrink_priority: ShrinkPriority::Normal,
87 }
88 }
89}
90
91impl LayoutConstraints {
92 /// Set the minimum width. Accepts any [`Length`] variant; `Auto`/`Flex` mean no minimum.
93 pub fn min_width(mut self, v: Length) -> Self {
94 self.min_w = v;
95 self
96 }
97
98 /// Set the minimum height. Accepts any [`Length`] variant; `Auto`/`Flex` mean no minimum.
99 pub fn min_height(mut self, v: Length) -> Self {
100 self.min_h = v;
101 self
102 }
103
104 /// Set the maximum width. Accepts any [`Length`] variant; `Auto`/`Flex` mean no cap.
105 pub fn max_width(mut self, v: Length) -> Self {
106 self.max_w = Some(v);
107 self
108 }
109
110 /// Set the maximum height. Accepts any [`Length`] variant; `Auto`/`Flex` mean no cap.
111 pub fn max_height(mut self, v: Length) -> Self {
112 self.max_h = Some(v);
113 self
114 }
115
116 /// Mark whether this element reflows when its main-axis allocation changes.
117 pub fn reflows(mut self, v: bool) -> Self {
118 self.reflows = v;
119 self
120 }
121
122 /// Set this element's stack shrink priority.
123 pub fn shrink_priority(mut self, v: ShrinkPriority) -> Self {
124 self.shrink_priority = v;
125 self
126 }
127
128 /// Clamp `natural` width to the min/max constraints resolved against `available`.
129 ///
130 /// `available` is the width offered by the parent container. `Percent` constraints
131 /// are resolved relative to it; `Auto`/`Flex` constraints act as 0 (min) or no cap (max).
132 pub(crate) fn clamp_width(&self, natural: u16, available: u16) -> u16 {
133 let min = self.min_w.resolve_as_min(available);
134 let mut w = natural.max(min);
135 if let Some(max) = self.max_w.and_then(|l| l.resolve_as_max(available)) {
136 w = w.min(max).max(min);
137 }
138 w
139 }
140
141 /// Clamp `natural` height to the min/max constraints resolved against `available`.
142 ///
143 /// `available` is the height offered by the parent container. `Percent` constraints
144 /// are resolved relative to it; `Auto`/`Flex` constraints act as 0 (min) or no cap (max).
145 pub(crate) fn clamp_height(&self, natural: u16, available: u16) -> u16 {
146 let min = self.min_h.resolve_as_min(available);
147 let mut h = natural.max(min);
148 if let Some(max) = self.max_h.and_then(|l| l.resolve_as_max(available)) {
149 h = h.min(max).max(min);
150 }
151 h
152 }
153}
154
155/// Flexbox-inspired sizing.
156#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
157pub enum Length {
158 /// Size to content.
159 #[default]
160 Auto,
161 /// Fixed size in cells.
162 Px(u16),
163 /// Percentage of available space (0-100).
164 Percent(u16),
165 /// Fill remaining space proportionally.
166 Flex(u16),
167}
168
169impl Length {
170 /// Resolve the length against available space and content size.
171 pub fn resolve(self, available: u16, content: u16) -> u16 {
172 match self {
173 Self::Auto => content,
174 Self::Px(px) => px,
175 Self::Percent(percent) => {
176 let percent = percent.min(100);
177 ((available as u32).saturating_mul(percent as u32) / 100).min(u16::MAX as u32)
178 as u16
179 }
180 Self::Flex(_) => available,
181 }
182 }
183
184 /// Resolve this length as a **minimum** constraint against `available` space.
185 ///
186 /// - `Px(n)` → `n` (absolute).
187 /// - `Percent(p)` → `p% of available`.
188 /// - `Auto` / `Flex(_)` → `0` (no minimum enforced).
189 pub(crate) fn resolve_as_min(self, available: u16) -> u16 {
190 match self {
191 Self::Px(px) => px,
192 Self::Percent(p) => {
193 if available == u16::MAX {
194 0
195 } else {
196 ((available as u32 * p.min(100) as u32) / 100).min(u16::MAX as u32) as u16
197 }
198 }
199 Self::Auto | Self::Flex(_) => 0,
200 }
201 }
202
203 /// Resolve this length as a **maximum** constraint against `available` space.
204 ///
205 /// - `Px(n)` → `Some(n)` (absolute cap).
206 /// - `Percent(p)` → `Some(p% of available)`.
207 /// - `Auto` / `Flex(_)` → `None` (no cap).
208 pub(crate) fn resolve_as_max(self, available: u16) -> Option<u16> {
209 match self {
210 Self::Px(px) => Some(px),
211 Self::Percent(p) => {
212 if available == u16::MAX {
213 None
214 } else {
215 Some(((available as u32 * p.min(100) as u32) / 100).min(u16::MAX as u32) as u16)
216 }
217 }
218 Self::Auto | Self::Flex(_) => None,
219 }
220 }
221}
222
223/// Sizing constraint for overlay helpers (e.g. `Center`).
224#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
225pub enum Size {
226 /// Size to content.
227 #[default]
228 Auto,
229 /// Fixed size in cells.
230 Fixed(u16),
231 /// Percentage of available space (0–100).
232 Percent(u16),
233}
234
235/// Alignment of children on the cross axis.
236#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
237pub enum Align {
238 /// Start (left/top).
239 #[default]
240 Start,
241 /// Center.
242 Center,
243 /// End (right/bottom).
244 End,
245 /// Stretch to fill.
246 Stretch,
247}
248
249/// Alignment of children along the main axis.
250///
251/// Note that `SpaceBetween`, `SpaceAround`, and `SpaceEvenly` only have a
252/// visible effect when there is slack on the main axis to distribute. In an
253/// `HStack`/`VStack` whose default child sizing is `Length::Flex(1)` on the
254/// main axis, flex children consume all remaining space and leave nothing for
255/// the spacer math, so the layout looks identical to `Start`. To use these
256/// variants, give each child an explicit non-flex main-axis size (e.g.
257/// `Length::Auto` or `Length::Px(_)`) so the container has slack to distribute.
258#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
259pub enum Justify {
260 /// Pack children toward the start edge.
261 #[default]
262 Start,
263 /// Center children in the available space.
264 Center,
265 /// Pack children toward the end edge.
266 End,
267 /// Evenly distribute extra space between children.
268 ///
269 /// Requires children with non-flex main-axis sizing — see the enum-level
270 /// note.
271 SpaceBetween,
272 /// Evenly distribute extra space around children.
273 ///
274 /// Requires children with non-flex main-axis sizing — see the enum-level
275 /// note.
276 SpaceAround,
277 /// Evenly distribute extra space between and around children.
278 ///
279 /// Requires children with non-flex main-axis sizing — see the enum-level
280 /// note.
281 SpaceEvenly,
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn clamp_width_enforces_min_and_max() {
290 let c = LayoutConstraints::default()
291 .min_width(Length::Px(10))
292 .max_width(Length::Px(50));
293
294 // Natural within range - unchanged
295 assert_eq!(c.clamp_width(30, 100), 30);
296 // Natural below min - raised to min
297 assert_eq!(c.clamp_width(5, 100), 10);
298 // Natural above max - capped to max
299 assert_eq!(c.clamp_width(80, 100), 50);
300 // No max set - only min enforced
301 let uncapped = LayoutConstraints::default().min_width(Length::Px(10));
302 assert_eq!(uncapped.clamp_width(u16::MAX, 1000), u16::MAX);
303 }
304
305 #[test]
306 fn clamp_height_min_wins_over_max() {
307 // When min > max, the minimum takes priority (documented invariant).
308 let c = LayoutConstraints::default()
309 .min_height(Length::Px(40))
310 .max_height(Length::Px(20));
311
312 // Any natural value should resolve to min because min > max.
313 assert_eq!(c.clamp_height(0, 100), 40);
314 assert_eq!(c.clamp_height(30, 100), 40);
315 assert_eq!(c.clamp_height(100, 100), 40);
316 }
317
318 #[test]
319 fn clamp_zero_constraints_and_min_eq_max() {
320 // All-zero constraints pass through zero
321 let zero = LayoutConstraints::default();
322 assert_eq!(zero.clamp_width(0, 100), 0);
323 assert_eq!(zero.clamp_height(0, 100), 0);
324
325 // min == max pins to that exact value
326 let pinned = LayoutConstraints::default()
327 .min_width(Length::Px(25))
328 .max_width(Length::Px(25));
329 assert_eq!(pinned.clamp_width(0, 100), 25);
330 assert_eq!(pinned.clamp_width(25, 100), 25);
331 assert_eq!(pinned.clamp_width(100, 100), 25);
332 }
333
334 #[test]
335 fn clamp_percent_constraints() {
336 // Percent(50) min with available=100 → min=50
337 let c = LayoutConstraints::default()
338 .min_width(Length::Percent(50))
339 .max_width(Length::Percent(80));
340 assert_eq!(c.clamp_width(0, 100), 50); // below min → clamped up
341 assert_eq!(c.clamp_width(60, 100), 60); // in range
342 assert_eq!(c.clamp_width(90, 100), 80); // above max → clamped down
343 // With a different available
344 assert_eq!(c.clamp_width(0, 200), 100); // 50% of 200
345 assert_eq!(c.clamp_width(200, 200), 160); // capped at 80% of 200
346 }
347
348 #[test]
349 fn clamp_auto_flex_constraints_mean_no_constraint() {
350 // Auto/Flex min → 0, Auto/Flex max → no cap
351 let c = LayoutConstraints::default()
352 .min_width(Length::Auto)
353 .max_width(Length::Flex(1));
354 assert_eq!(c.clamp_width(0, 100), 0);
355 assert_eq!(c.clamp_width(9999, 100), 9999);
356 }
357
358 #[test]
359 fn length_resolve_variants() {
360 // Auto returns content size regardless of available space
361 assert_eq!(Length::Auto.resolve(100, 42), 42);
362 assert_eq!(Length::Auto.resolve(0, 7), 7);
363
364 // Px returns fixed pixel value regardless of available/content
365 assert_eq!(Length::Px(60).resolve(100, 42), 60);
366 assert_eq!(Length::Px(0).resolve(100, 42), 0);
367
368 // Percent returns percentage of available space (clamped to 100)
369 assert_eq!(Length::Percent(50).resolve(100, 42), 50);
370 assert_eq!(Length::Percent(33).resolve(300, 42), 99);
371 assert_eq!(Length::Percent(0).resolve(100, 42), 0);
372 assert_eq!(Length::Percent(150).resolve(80, 42), 80);
373
374 // Flex returns available space regardless of content or weight
375 assert_eq!(Length::Flex(1).resolve(100, 42), 100);
376 assert_eq!(Length::Flex(3).resolve(200, 10), 200);
377 assert_eq!(Length::Flex(0).resolve(50, 50), 50);
378 }
379
380 #[test]
381 fn layout_constraints_builder_and_defaults() {
382 let defaults = LayoutConstraints::default();
383 assert_eq!(defaults.min_w, Length::Px(0));
384 assert_eq!(defaults.min_h, Length::Px(0));
385 assert_eq!(defaults.max_w, None);
386 assert_eq!(defaults.max_h, None);
387 assert!(!defaults.force_compact);
388 assert!(!defaults.reflows);
389 assert_eq!(defaults.shrink_priority, ShrinkPriority::Normal);
390
391 // Builder methods compose and set the right fields
392 let c = LayoutConstraints::default()
393 .min_width(Length::Px(5))
394 .min_height(Length::Px(10))
395 .max_width(Length::Px(100))
396 .max_height(Length::Px(200))
397 .reflows(true)
398 .shrink_priority(ShrinkPriority::First);
399 assert_eq!(c.min_w, Length::Px(5));
400 assert_eq!(c.min_h, Length::Px(10));
401 assert_eq!(c.max_w, Some(Length::Px(100)));
402 assert_eq!(c.max_h, Some(Length::Px(200)));
403 assert!(c.reflows);
404 assert_eq!(c.shrink_priority, ShrinkPriority::First);
405 }
406}