Skip to main content

tessera_ui/component_tree/
constraint.rs

1//! # Layout Constraint System
2//!
3//! This module defines Tessera's interval-based layout constraints.
4//!
5//! A parent layout can only bound a child's size. Exact size is represented as
6//! a tight interval where `min == max`.
7
8use std::ops::Sub;
9
10use crate::{Dp, Px};
11
12/// A single-axis layout constraint expressed as an allowed interval.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct AxisConstraint {
15    /// The minimum allowed size on this axis.
16    pub min: Px,
17    /// The maximum allowed size on this axis.
18    ///
19    /// When `None`, the axis is unbounded above.
20    pub max: Option<Px>,
21}
22
23impl AxisConstraint {
24    /// An unconstrained axis with minimum `0`.
25    pub const NONE: Self = Self {
26        min: Px::ZERO,
27        max: None,
28    };
29
30    /// Creates a new interval constraint.
31    pub fn new(min: Px, max: Option<Px>) -> Self {
32        let normalized_max = match max {
33            Some(value) if value < min => Some(min),
34            other => other,
35        };
36        Self {
37            min,
38            max: normalized_max,
39        }
40    }
41
42    /// Creates a tight axis constraint.
43    pub const fn exact(size: Px) -> Self {
44        Self {
45            min: size,
46            max: Some(size),
47        }
48    }
49
50    /// Creates an axis with only a lower bound.
51    pub const fn at_least(min: Px) -> Self {
52        Self { min, max: None }
53    }
54
55    /// Creates an axis with only an upper bound.
56    pub const fn at_most(max: Px) -> Self {
57        Self {
58            min: Px::ZERO,
59            max: Some(max),
60        }
61    }
62
63    /// Returns the preferred minimum size for this axis.
64    pub const fn resolve_min(self) -> Px {
65        self.min
66    }
67
68    /// Returns the upper bound for this axis, if present.
69    pub const fn resolve_max(self) -> Option<Px> {
70        self.max
71    }
72
73    /// Returns the intersection of two axis constraints.
74    pub fn intersect(self, parent: Self) -> Self {
75        let min = self.min.max(parent.min);
76        let max = match (self.max, parent.max) {
77            (Some(lhs), Some(rhs)) => Some(lhs.min(rhs)),
78            (Some(lhs), None) => Some(lhs),
79            (None, Some(rhs)) => Some(rhs),
80            (None, None) => None,
81        };
82        Self::new(min, max)
83    }
84
85    /// Returns a version of this axis with the lower bound cleared.
86    pub const fn without_min(self) -> Self {
87        Self {
88            min: Px::ZERO,
89            max: self.max,
90        }
91    }
92
93    /// Clamps a measured size into this interval.
94    pub fn clamp(self, value: Px) -> Px {
95        let mut value = value.max(self.min);
96        if let Some(max) = self.max {
97            value = value.min(max);
98        }
99        value
100    }
101}
102
103impl Default for AxisConstraint {
104    fn default() -> Self {
105        Self::NONE
106    }
107}
108
109impl From<Px> for AxisConstraint {
110    fn from(value: Px) -> Self {
111        Self::exact(value)
112    }
113}
114
115impl From<Dp> for AxisConstraint {
116    fn from(value: Dp) -> Self {
117        Self::exact(value.into())
118    }
119}
120
121impl Sub<Px> for AxisConstraint {
122    type Output = AxisConstraint;
123
124    fn sub(self, rhs: Px) -> Self::Output {
125        let min = (self.min - rhs).max(Px::ZERO);
126        let max = self.max.map(|value| (value - rhs).max(Px::ZERO));
127        Self::new(min, max)
128    }
129}
130
131impl std::ops::Add<Px> for AxisConstraint {
132    type Output = AxisConstraint;
133
134    fn add(self, rhs: Px) -> Self::Output {
135        let min = self.min + rhs;
136        let max = self.max.map(|value| value + rhs);
137        Self::new(min, max)
138    }
139}
140
141impl std::ops::AddAssign<Px> for AxisConstraint {
142    fn add_assign(&mut self, rhs: Px) {
143        *self = *self + rhs;
144    }
145}
146
147impl std::ops::SubAssign<Px> for AxisConstraint {
148    fn sub_assign(&mut self, rhs: Px) {
149        *self = *self - rhs;
150    }
151}
152
153/// The constraint inherited from a parent node during measurement.
154#[derive(Clone, Copy, Debug)]
155pub struct ParentConstraint<'a>(&'a Constraint);
156
157impl<'a> ParentConstraint<'a> {
158    pub(crate) fn new(constraint: &'a Constraint) -> Self {
159        Self(constraint)
160    }
161
162    /// Returns the inherited width constraint.
163    pub const fn width(self) -> AxisConstraint {
164        self.0.width
165    }
166
167    /// Returns the inherited height constraint.
168    pub const fn height(self) -> AxisConstraint {
169        self.0.height
170    }
171
172    /// Returns a reference to the underlying constraint.
173    pub const fn as_ref(self) -> &'a Constraint {
174        self.0
175    }
176
177    /// Returns the inherited constraint with both lower bounds cleared.
178    pub const fn without_min(self) -> Constraint {
179        Constraint {
180            width: self.0.width.without_min(),
181            height: self.0.height.without_min(),
182        }
183    }
184}
185
186/// A two-dimensional interval constraint.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
188pub struct Constraint {
189    /// The width interval.
190    pub width: AxisConstraint,
191    /// The height interval.
192    pub height: AxisConstraint,
193}
194
195impl Constraint {
196    /// An unconstrained width/height interval.
197    pub const NONE: Self = Self {
198        width: AxisConstraint::NONE,
199        height: AxisConstraint::NONE,
200    };
201
202    /// Creates a new 2D constraint from width and height intervals.
203    pub fn new(width: impl Into<AxisConstraint>, height: impl Into<AxisConstraint>) -> Self {
204        Self {
205            width: width.into(),
206            height: height.into(),
207        }
208    }
209
210    /// Creates a tight 2D constraint.
211    pub fn exact(width: Px, height: Px) -> Self {
212        Self {
213            width: AxisConstraint::exact(width),
214            height: AxisConstraint::exact(height),
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn axis_exact_is_tight_interval() {
225        let axis = AxisConstraint::exact(Px(100));
226        assert_eq!(axis.min, Px(100));
227        assert_eq!(axis.max, Some(Px(100)));
228    }
229
230    #[test]
231    fn axis_new_clamps_max_to_min() {
232        let axis = AxisConstraint::new(Px(40), Some(Px(20)));
233        assert_eq!(axis.min, Px(40));
234        assert_eq!(axis.max, Some(Px(40)));
235    }
236
237    #[test]
238    fn axis_intersect_returns_interval_overlap() {
239        let parent = Constraint::new(
240            AxisConstraint::new(Px(20), Some(Px(100))),
241            AxisConstraint::new(Px(10), Some(Px(80))),
242        );
243        let child = Constraint::new(
244            AxisConstraint::new(Px(30), Some(Px(120))),
245            AxisConstraint::new(Px(0), Some(Px(40))),
246        );
247
248        let width = child.width.intersect(parent.width);
249        let height = child.height.intersect(parent.height);
250
251        assert_eq!(width, AxisConstraint::new(Px(30), Some(Px(100))));
252        assert_eq!(height, AxisConstraint::new(Px(10), Some(Px(40))));
253    }
254
255    #[test]
256    fn axis_intersect_keeps_unbounded_max_when_parent_is_unbounded() {
257        let parent = Constraint::new(AxisConstraint::at_least(Px(50)), AxisConstraint::NONE);
258        let child = Constraint::new(
259            AxisConstraint::new(Px(20), None),
260            AxisConstraint::new(Px(10), Some(Px(30))),
261        );
262
263        let width = child.width.intersect(parent.width);
264        let height = child.height.intersect(parent.height);
265
266        assert_eq!(width, AxisConstraint::at_least(Px(50)));
267        assert_eq!(height, AxisConstraint::new(Px(10), Some(Px(30))));
268    }
269
270    #[test]
271    fn arithmetic_adjusts_intervals() {
272        let mut axis = AxisConstraint::new(Px(20), Some(Px(60)));
273        axis -= Px(5);
274        assert_eq!(axis, AxisConstraint::new(Px(15), Some(Px(55))));
275        axis += Px(10);
276        assert_eq!(axis, AxisConstraint::new(Px(25), Some(Px(65))));
277    }
278}