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_new_clamps_max_to_min() {
225        let _runtime_session = crate::RuntimeSession::new();
226        let _runtime_scope = _runtime_session.enter();
227        let axis = AxisConstraint::new(Px(40), Some(Px(20)));
228        assert_eq!(axis.min, Px(40));
229        assert_eq!(axis.max, Some(Px(40)));
230    }
231
232    #[test]
233    fn axis_intersect_returns_interval_overlap() {
234        let _runtime_session = crate::RuntimeSession::new();
235        let _runtime_scope = _runtime_session.enter();
236        let parent = Constraint::new(
237            AxisConstraint::new(Px(20), Some(Px(100))),
238            AxisConstraint::new(Px(10), Some(Px(80))),
239        );
240        let child = Constraint::new(
241            AxisConstraint::new(Px(30), Some(Px(120))),
242            AxisConstraint::new(Px(0), Some(Px(40))),
243        );
244
245        let width = child.width.intersect(parent.width);
246        let height = child.height.intersect(parent.height);
247
248        assert_eq!(width, AxisConstraint::new(Px(30), Some(Px(100))));
249        assert_eq!(height, AxisConstraint::new(Px(10), Some(Px(40))));
250    }
251
252    #[test]
253    fn axis_intersect_keeps_unbounded_max_when_parent_is_unbounded() {
254        let _runtime_session = crate::RuntimeSession::new();
255        let _runtime_scope = _runtime_session.enter();
256        let parent = Constraint::new(AxisConstraint::at_least(Px(50)), AxisConstraint::NONE);
257        let child = Constraint::new(
258            AxisConstraint::new(Px(20), None),
259            AxisConstraint::new(Px(10), Some(Px(30))),
260        );
261
262        let width = child.width.intersect(parent.width);
263        let height = child.height.intersect(parent.height);
264
265        assert_eq!(width, AxisConstraint::at_least(Px(50)));
266        assert_eq!(height, AxisConstraint::new(Px(10), Some(Px(30))));
267    }
268
269    #[test]
270    fn arithmetic_adjusts_intervals() {
271        let _runtime_session = crate::RuntimeSession::new();
272        let _runtime_scope = _runtime_session.enter();
273        let mut axis = AxisConstraint::new(Px(20), Some(Px(60)));
274        axis -= Px(5);
275        assert_eq!(axis, AxisConstraint::new(Px(15), Some(Px(55))));
276        axis += Px(10);
277        assert_eq!(axis, AxisConstraint::new(Px(25), Some(Px(65))));
278    }
279}