Skip to main content

tessera_foundation/modifier/
layout.rs

1//! Layout modifiers for sizing, padding, and constraints.
2//!
3//! ## Usage
4//!
5//! Apply padding, sizing, or minimum touch target adjustments to component
6//! subtrees.
7
8use std::{any::TypeId, sync::Arc};
9
10use tessera_ui::{
11    AxisConstraint, ComputedData, Constraint, Dp, LayoutModifierChild, LayoutModifierInput,
12    LayoutModifierNode, LayoutResult, MeasurementError, ParentDataMap, ParentDataModifierNode,
13    PlacementModifierNode, Px, PxPosition,
14    layout::{PlacementResult, PlacementScope},
15};
16
17use crate::alignment::Alignment;
18
19/// Controls whether minimum interactive size wrappers are enforced.
20#[derive(Clone, PartialEq, Copy, Debug)]
21pub struct MinimumInteractiveComponentEnforcement {
22    /// When true, `minimum_interactive_component_size` expands to the minimum
23    /// size.
24    pub enabled: bool,
25}
26
27impl Default for MinimumInteractiveComponentEnforcement {
28    fn default() -> Self {
29        Self { enabled: true }
30    }
31}
32
33pub use tessera_ui::Insets as Padding;
34
35fn resolve_axis_constraint(
36    parent: AxisConstraint,
37    override_axis: Option<AxisConstraint>,
38    fill_parent_max: bool,
39    enforce_incoming: bool,
40) -> AxisConstraint {
41    if fill_parent_max {
42        return match parent.max {
43            Some(max) => AxisConstraint::exact(max),
44            None => parent,
45        };
46    }
47
48    match override_axis {
49        None => parent,
50        Some(axis) if enforce_incoming => constrain_axis_to_parent(axis, parent),
51        Some(axis) => AxisConstraint::new(
52            axis.min,
53            match (axis.max, parent.max) {
54                (Some(lhs), Some(rhs)) => Some(lhs.min(rhs)),
55                (Some(lhs), None) => Some(lhs),
56                (None, Some(rhs)) => Some(rhs),
57                (None, None) => None,
58            },
59        ),
60    }
61}
62
63fn constrain_axis_to_parent(axis: AxisConstraint, parent: AxisConstraint) -> AxisConstraint {
64    AxisConstraint::new(
65        parent.clamp(axis.min),
66        axis.max.map(|max| parent.clamp(max)).or(parent.max),
67    )
68}
69
70#[derive(Clone, Copy, PartialEq)]
71pub(crate) struct PaddingModifierNode {
72    pub padding: Padding,
73}
74
75impl LayoutModifierNode for PaddingModifierNode {
76    fn measure_content(
77        &self,
78        input: &LayoutModifierInput<'_>,
79        child: &mut dyn LayoutModifierChild,
80    ) -> Result<LayoutResult, MeasurementError> {
81        let left_px: Px = self.padding.left.into();
82        let top_px: Px = self.padding.top.into();
83        let right_px: Px = self.padding.right.into();
84        let bottom_px: Px = self.padding.bottom.into();
85
86        let parent_constraint = input.layout_input.parent_constraint();
87        let constraint = Constraint::new(
88            parent_constraint.width() - (left_px + right_px),
89            parent_constraint.height() - (top_px + bottom_px),
90        );
91        let child_size = child.measure(&constraint)?;
92        child.place(PxPosition::new(left_px, top_px));
93        Ok(LayoutResult::new(ComputedData {
94            width: child_size.width + left_px + right_px,
95            height: child_size.height + top_px + bottom_px,
96        }))
97    }
98
99    fn place_children(
100        &self,
101        _scope: &PlacementScope<'_>,
102        inner: &PlacementResult,
103    ) -> Option<PlacementResult> {
104        let left_px: Px = self.padding.left.into();
105        let top_px: Px = self.padding.top.into();
106        let offset = PxPosition::new(left_px, top_px);
107        Some(PlacementResult::from_placements(
108            inner
109                .items()
110                .iter()
111                .map(|(instance_key, position)| (*instance_key, *position + offset))
112                .collect(),
113        ))
114    }
115}
116
117#[derive(Clone, Copy, PartialEq)]
118pub(crate) struct OffsetModifierNode {
119    pub x: Dp,
120    pub y: Dp,
121}
122
123impl PlacementModifierNode for OffsetModifierNode {
124    fn transform_position(&self, position: PxPosition) -> PxPosition {
125        position.offset(self.x.into(), self.y.into())
126    }
127}
128
129#[derive(Clone, Copy, PartialEq)]
130pub(crate) struct ConstraintModifierNode {
131    pub width_override: Option<AxisConstraint>,
132    pub height_override: Option<AxisConstraint>,
133    pub fill_width: bool,
134    pub fill_height: bool,
135    pub enforce_incoming: bool,
136}
137
138impl LayoutModifierNode for ConstraintModifierNode {
139    fn measure_content(
140        &self,
141        input: &LayoutModifierInput<'_>,
142        child: &mut dyn LayoutModifierChild,
143    ) -> Result<LayoutResult, MeasurementError> {
144        let parent_width = input.layout_input.parent_constraint().width();
145        let parent_height = input.layout_input.parent_constraint().height();
146        let constraint = Constraint::new(
147            resolve_axis_constraint(
148                parent_width,
149                self.width_override,
150                self.fill_width,
151                self.enforce_incoming,
152            ),
153            resolve_axis_constraint(
154                parent_height,
155                self.height_override,
156                self.fill_height,
157                self.enforce_incoming,
158            ),
159        );
160        let child_size = child.measure(&constraint)?;
161        child.place(PxPosition::ZERO);
162        Ok(LayoutResult::new(child_size))
163    }
164}
165
166#[derive(Clone, Copy, PartialEq)]
167pub(crate) struct RequiredSizeModifierNode {
168    pub width: Option<AxisConstraint>,
169    pub height: Option<AxisConstraint>,
170}
171
172impl LayoutModifierNode for RequiredSizeModifierNode {
173    fn measure_content(
174        &self,
175        input: &LayoutModifierInput<'_>,
176        child: &mut dyn LayoutModifierChild,
177    ) -> Result<LayoutResult, MeasurementError> {
178        // Required size bypasses the incoming bounds: the declared size is
179        // measured as-is, unlike `size` which clamps into the parent interval.
180        let parent = input.layout_input.parent_constraint();
181        let constraint = Constraint::new(
182            self.width.unwrap_or_else(|| parent.width()),
183            self.height.unwrap_or_else(|| parent.height()),
184        );
185        let child_size = child.measure(&constraint)?;
186        child.place(PxPosition::ZERO);
187        Ok(LayoutResult::new(child_size))
188    }
189}
190#[derive(Clone, Copy, PartialEq)]
191pub(crate) struct DefaultMinSizeModifierNode {
192    pub min_width: Dp,
193    pub min_height: Dp,
194}
195
196impl LayoutModifierNode for DefaultMinSizeModifierNode {
197    fn measure_content(
198        &self,
199        input: &LayoutModifierInput<'_>,
200        child: &mut dyn LayoutModifierChild,
201    ) -> Result<LayoutResult, MeasurementError> {
202        let parent = input.layout_input.parent_constraint();
203        let width = default_min_axis(parent.width(), self.min_width);
204        let height = default_min_axis(parent.height(), self.min_height);
205        let child_size = child.measure(&Constraint::new(width, height))?;
206        child.place(PxPosition::ZERO);
207        Ok(LayoutResult::new(child_size))
208    }
209}
210
211/// Applies a default minimum only when the incoming axis leaves the minimum
212/// open, so an explicit size declared further out always wins.
213fn default_min_axis(parent: AxisConstraint, default_min: Dp) -> AxisConstraint {
214    if parent.min != Px::ZERO {
215        return parent;
216    }
217
218    let default_min: Px = default_min.into();
219    let min = parent.max.map_or(default_min, |max| default_min.min(max));
220    AxisConstraint::new(min, parent.max)
221}
222
223#[derive(Clone, Copy, Default, PartialEq)]
224pub(crate) struct MinimumInteractiveModifierNode;
225
226impl LayoutModifierNode for MinimumInteractiveModifierNode {
227    fn measure_content(
228        &self,
229        input: &LayoutModifierInput<'_>,
230        child: &mut dyn LayoutModifierChild,
231    ) -> Result<LayoutResult, MeasurementError> {
232        const MIN_SIZE: Dp = Dp(48.0);
233        let parent_constraint = Constraint::new(
234            input.layout_input.parent_constraint().width(),
235            input.layout_input.parent_constraint().height(),
236        );
237        let child_size = child.measure(&parent_constraint)?;
238        if input.layout_input.is_intrinsic() {
239            child.place(PxPosition::ZERO);
240            return Ok(LayoutResult::new(child_size));
241        }
242        let min_px: Px = MIN_SIZE.into();
243        let width = parent_constraint.width.clamp(child_size.width.max(min_px));
244        let height = parent_constraint
245            .height
246            .clamp(child_size.height.max(min_px));
247        let x = ((width - child_size.width) / 2).max(Px(0));
248        let y = ((height - child_size.height) / 2).max(Px(0));
249        child.place(PxPosition::new(x, y));
250        Ok(LayoutResult::new(ComputedData { width, height }))
251    }
252}
253
254#[derive(Clone, Copy, Debug, PartialEq)]
255/// Parent data carrying relative layout weight for row and column containers.
256pub struct WeightParentData {
257    /// Relative weight used by weighted parent layouts.
258    pub weight: f32,
259}
260
261#[derive(Clone, Copy, PartialEq)]
262pub(crate) struct WeightParentDataModifierNode {
263    pub weight: f32,
264}
265
266impl ParentDataModifierNode for WeightParentDataModifierNode {
267    fn equivalent(&self, other: &dyn ParentDataModifierNode) -> bool {
268        (other as &dyn std::any::Any)
269            .downcast_ref::<Self>()
270            .is_some_and(|other| self == other)
271    }
272
273    fn apply_parent_data(&self, map: &mut ParentDataMap) {
274        map.insert(
275            TypeId::of::<WeightParentData>(),
276            Arc::new(WeightParentData {
277                weight: self.weight,
278            }),
279        );
280    }
281}
282
283#[derive(Clone, Copy, Debug, PartialEq)]
284/// Parent data carrying boxed-layout alignment overrides.
285pub struct AlignmentParentData {
286    /// Alignment requested by the child for layered boxed layouts.
287    pub alignment: Alignment,
288}
289
290#[derive(Clone, Copy, PartialEq)]
291pub(crate) struct AlignmentParentDataModifierNode {
292    pub alignment: Alignment,
293}
294
295impl ParentDataModifierNode for AlignmentParentDataModifierNode {
296    fn equivalent(&self, other: &dyn ParentDataModifierNode) -> bool {
297        (other as &dyn std::any::Any)
298            .downcast_ref::<Self>()
299            .is_some_and(|other| self == other)
300    }
301
302    fn apply_parent_data(&self, map: &mut ParentDataMap) {
303        map.insert(
304            TypeId::of::<AlignmentParentData>(),
305            Arc::new(AlignmentParentData {
306                alignment: self.alignment,
307            }),
308        );
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use tessera_ui::{AxisConstraint, Px};
315
316    use super::constrain_axis_to_parent;
317
318    #[test]
319    fn constrain_clamps_target_into_tight_parent() {
320        let parent = AxisConstraint::exact(Px::new(100));
321
322        assert_eq!(
323            constrain_axis_to_parent(AxisConstraint::exact(Px::new(50)), parent),
324            AxisConstraint::exact(Px::new(100))
325        );
326        assert_eq!(
327            constrain_axis_to_parent(AxisConstraint::exact(Px::new(150)), parent),
328            AxisConstraint::exact(Px::new(100))
329        );
330    }
331
332    #[test]
333    fn constrain_keeps_target_inside_loose_parent() {
334        let parent = AxisConstraint::new(Px::ZERO, Some(Px::new(200)));
335
336        assert_eq!(
337            constrain_axis_to_parent(AxisConstraint::exact(Px::new(50)), parent),
338            AxisConstraint::exact(Px::new(50))
339        );
340    }
341}