tessera_foundation/modifier/
layout.rs1use 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#[derive(Clone, PartialEq, Copy, Debug)]
21pub struct MinimumInteractiveComponentEnforcement {
22 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 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
211fn 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)]
255pub struct WeightParentData {
257 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)]
284pub struct AlignmentParentData {
286 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}