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}