Skip to main content

zest_core/
layout.rs

1//! Layout protocol: [`Constraints`] flow down, [`Size`] flows up.
2//!
3//! `zest` uses a WPF/Flutter-style two-pass layout:
4//!
5//! 1. **Measure**: parent calls `child.measure(constraints)`, child returns
6//!    its desired [`Size`] within `constraints`.
7//! 2. **Arrange**: parent calls `child.arrange(rect)` with a concrete
8//!    rectangle. Child stores `rect`, containers recursively arrange
9//!    children.
10//!
11//! Each widget exposes a [`Length`] on each axis via
12//! [`Widget::preferred_size`](crate::Widget::preferred_size). Containers
13//! interpret these to allocate space:
14//!
15//! * [`Length::Fixed`] children get exactly that many pixels.
16//! * [`Length::Shrink`] children get whatever they ask for via `measure`.
17//! * [`Length::Fill`] / [`Length::FillPortion`] children share the
18//!   remaining space proportionally (`Fill` is `FillPortion(1)`).
19
20use embedded_graphics::prelude::*;
21
22/// Sentinel for "unbounded" in a constraint's `max` field. Equal to
23/// `u16::MAX as u32`; actual screen dimensions on every target this
24/// framework supports fit comfortably under it.
25pub const UNBOUNDED: u32 = u16::MAX as u32;
26
27/// Min and max bounds a parent gives a child during the measure pass.
28///
29/// Children should return a size satisfying `min <= size <= max` on both
30/// axes. The [`UNBOUNDED`] sentinel in a `max` field means "as much as you
31/// want."
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
33pub struct Constraints {
34    /// Minimum size the child must produce.
35    pub min: Size,
36    /// Maximum size the child may produce.
37    pub max: Size,
38}
39
40impl Constraints {
41    /// Constraints permitting any size up to `max`.
42    #[must_use]
43    pub const fn loose(max: Size) -> Self {
44        Self {
45            min: Size::zero(),
46            max,
47        }
48    }
49
50    /// Constraints requiring exactly `size`.
51    #[must_use]
52    pub const fn tight(size: Size) -> Self {
53        Self {
54            min: size,
55            max: size,
56        }
57    }
58
59    /// Constraints permitting `min..=max`.
60    #[must_use]
61    pub const fn new(min: Size, max: Size) -> Self {
62        Self { min, max }
63    }
64
65    /// Constraints from zero up to [`UNBOUNDED`] on both axes.
66    #[must_use]
67    pub const fn unbounded() -> Self {
68        Self {
69            min: Size::zero(),
70            max: Size::new(UNBOUNDED, UNBOUNDED),
71        }
72    }
73
74    /// Clamp `size` to fall within the constraints on both axes.
75    #[must_use]
76    pub fn clamp(&self, size: Size) -> Size {
77        Size::new(
78            size.width.clamp(self.min.width, self.max.width),
79            size.height.clamp(self.min.height, self.max.height),
80        )
81    }
82
83    /// Constraints with `max.width = w`. `min.width` is also clamped to `w`
84    /// to preserve `min <= max`.
85    #[must_use]
86    pub fn with_width(self, w: u32) -> Self {
87        Self {
88            min: Size::new(self.min.width.min(w), self.min.height),
89            max: Size::new(w, self.max.height),
90        }
91    }
92
93    /// Constraints with `max.height = h`. `min.height` is also clamped.
94    #[must_use]
95    pub fn with_height(self, h: u32) -> Self {
96        Self {
97            min: Size::new(self.min.width, self.min.height.min(h)),
98            max: Size::new(self.max.width, h),
99        }
100    }
101
102    /// Subtract `dx, dy` from both min and max (saturating). Used to apply
103    /// padding before measuring a child.
104    #[must_use]
105    pub fn shrink(self, dx: u32, dy: u32) -> Self {
106        Self {
107            min: Size::new(
108                self.min.width.saturating_sub(dx),
109                self.min.height.saturating_sub(dy),
110            ),
111            max: Size::new(
112                self.max.width.saturating_sub(dx),
113                self.max.height.saturating_sub(dy),
114            ),
115        }
116    }
117}
118
119impl Default for Constraints {
120    fn default() -> Self {
121        Self::unbounded()
122    }
123}
124
125/// Per-axis sizing intent. Pattern modeled on iced / libcosmic.
126///
127/// Containers use this to decide how much room each child gets before
128/// they call `measure` and `arrange`.
129#[derive(Copy, Clone, Debug, PartialEq, Eq)]
130pub enum Length {
131    /// Take exactly this many pixels.
132    Fixed(u32),
133    /// Ask the child what it wants (calls `measure` with loose
134    /// constraints) and give it that.
135    Shrink,
136    /// Take all remaining space. Equivalent to `FillPortion(1)`.
137    Fill,
138    /// Take a share of remaining space proportional to `portion`.
139    /// Multiple `FillPortion` (and `Fill`) siblings split the residual
140    /// in proportion to their portion sums.
141    FillPortion(u32),
142}
143
144impl Length {
145    /// Returns the explicit pixel count if this is `Fixed`, else `None`.
146    /// Convenience for the common "is this a fixed slot" check during
147    /// container layout.
148    #[must_use]
149    pub fn fixed(self) -> Option<u32> {
150        match self {
151            Length::Fixed(n) => Some(n),
152            _ => None,
153        }
154    }
155
156    /// Portion weight for `Fill` / `FillPortion`. `0` for non-flex
157    /// variants.
158    #[must_use]
159    pub fn portion(self) -> u32 {
160        match self {
161            Length::Fill => 1,
162            Length::FillPortion(p) => p.max(1),
163            _ => 0,
164        }
165    }
166
167    /// Resolve to a concrete pixel count given the widget's intrinsic
168    /// size on this axis and the max available from constraints.
169    #[must_use]
170    pub fn resolve(self, intrinsic: u32, max: u32) -> u32 {
171        match self {
172            Length::Fixed(n) => n.min(max),
173            Length::Shrink => intrinsic.min(max),
174            Length::Fill | Length::FillPortion(_) => max,
175        }
176    }
177}
178
179impl From<u32> for Length {
180    fn from(n: u32) -> Self {
181        Length::Fixed(n)
182    }
183}
184
185impl From<u16> for Length {
186    fn from(n: u16) -> Self {
187        Length::Fixed(n as u32)
188    }
189}
190
191// Accept default integer literals (`i32`) and `usize` so call sites
192// can write `.width(64)` without a type suffix. Negative values are
193// clamped to zero — negative widths/heights are nonsensical.
194impl From<i32> for Length {
195    fn from(n: i32) -> Self {
196        Length::Fixed(n.max(0) as u32)
197    }
198}
199
200impl From<usize> for Length {
201    fn from(n: usize) -> Self {
202        Length::Fixed(n.min(u32::MAX as usize) as u32)
203    }
204}
205
206/// Horizontal alignment for child or text content within a slot.
207#[derive(Copy, Clone, Debug, PartialEq, Eq)]
208pub enum Horizontal {
209    /// Align to the left edge.
210    Left,
211    /// Center horizontally.
212    Center,
213    /// Align to the right edge.
214    Right,
215}
216
217/// Vertical alignment for child or text content within a slot.
218#[derive(Copy, Clone, Debug, PartialEq, Eq)]
219pub enum Vertical {
220    /// Align to the top edge.
221    Top,
222    /// Center vertically.
223    Center,
224    /// Align to the bottom edge.
225    Bottom,
226}