Skip to main content

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}