1use tessera_ui::{
7 __private::configure_layout_node,
8 ComputedData, LayoutResult, MeasurementError, Modifier, NoopDrawModifier, Px, PxPosition,
9 RenderSlot,
10 layout::{MeasureScope, PlacementResult, PlacementScope},
11 tessera,
12};
13
14use crate::alignment::Alignment;
15
16fn center_axis(container: Px, child: Px) -> f32 {
17 (container.0 - child.0) as f32 / 2.0
18}
19
20fn compute_child_offset(
21 alignment: Alignment,
22 container_w: Px,
23 container_h: Px,
24 child_w: Px,
25 child_h: Px,
26) -> (f32, f32) {
27 match alignment {
28 Alignment::TopStart => (0.0, 0.0),
29 Alignment::TopCenter => (center_axis(container_w, child_w), 0.0),
30 Alignment::TopEnd => ((container_w.0 - child_w.0) as f32, 0.0),
31 Alignment::CenterStart => (0.0, center_axis(container_h, child_h)),
32 Alignment::Center => (
33 center_axis(container_w, child_w),
34 center_axis(container_h, child_h),
35 ),
36 Alignment::CenterEnd => (
37 (container_w.0 - child_w.0) as f32,
38 center_axis(container_h, child_h),
39 ),
40 Alignment::BottomStart => (0.0, (container_h.0 - child_h.0) as f32),
41 Alignment::BottomCenter => (
42 center_axis(container_w, child_w),
43 (container_h.0 - child_h.0) as f32,
44 ),
45 Alignment::BottomEnd => (
46 (container_w.0 - child_w.0) as f32,
47 (container_h.0 - child_h.0) as f32,
48 ),
49 }
50}
51
52#[tessera(tessera_ui, layout)]
88pub fn boxed(
89 alignment: Option<Alignment>,
90 modifier: Option<Modifier>,
91 children: Option<RenderSlot>,
92) {
93 let alignment = alignment.unwrap_or_default();
94 let modifier = modifier.unwrap_or_default();
95 configure_layout_node(BoxedLayout { alignment }, NoopDrawModifier, modifier);
96 children.unwrap_or_else(RenderSlot::empty).render();
97}
98
99#[derive(Clone, PartialEq)]
100struct BoxedLayout {
101 alignment: Alignment,
102}
103
104impl tessera_ui::LayoutModifierNode for BoxedLayout {
105 fn place_children(
106 &self,
107 scope: &PlacementScope<'_>,
108 _inner: &PlacementResult,
109 ) -> Option<PlacementResult> {
110 let size = scope.size();
111 let mut result = LayoutResult::new(size);
112 for child in scope.children() {
113 let alignment = child
114 .parent_data::<crate::modifier::AlignmentParentData>()
115 .map_or(self.alignment, |data| data.alignment);
116 let child_size = child.size();
117 let (x, y) = compute_child_offset(
118 alignment,
119 size.width,
120 size.height,
121 child_size.width,
122 child_size.height,
123 );
124 result.place_child(child, PxPosition::from_f32_xy(x, y));
125 }
126 Some(result.into_placement_result())
127 }
128
129 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
130 let mut result = LayoutResult::default();
131 let children = input.children();
132 let child_alignments = collect_child_alignments(input, &children);
133 let n = child_alignments.len();
134 debug_assert_eq!(
135 children.len(),
136 n,
137 "Mismatch between children defined in scope and runtime children count"
138 );
139
140 let parent_constraint = *input.parent_constraint().as_ref();
141 let child_constraint = input.parent_constraint().without_min();
142
143 let mut max_child_width = Px(0);
144 let mut max_child_height = Px(0);
145 let mut children_sizes = vec![None; n];
146
147 for (i, child) in children.iter().enumerate().take(n) {
148 let child_result = child.measure(input, &child_constraint)?;
149 max_child_width = max_child_width.max(child_result.width);
150 max_child_height = max_child_height.max(child_result.height);
151 children_sizes[i] = Some(child_result);
152 }
153
154 let final_width = parent_constraint.width.clamp(max_child_width);
155 let final_height = parent_constraint.height.clamp(max_child_height);
156
157 for (i, child_size_opt) in children_sizes.iter().enumerate() {
158 if let Some(child_size) = child_size_opt {
159 let child_id = children[i];
160 let child_alignment = child_alignments[i].unwrap_or(self.alignment);
161 let (x, y) = compute_child_offset(
162 child_alignment,
163 final_width,
164 final_height,
165 child_size.width,
166 child_size.height,
167 );
168 result.place_child(child_id, PxPosition::from_f32_xy(x, y));
169 }
170 }
171
172 Ok(result.with_size(ComputedData {
173 width: final_width,
174 height: final_height,
175 }))
176 }
177}
178
179fn collect_child_alignments(
180 scope: &MeasureScope<'_>,
181 children: &[tessera_ui::layout::LayoutChild],
182) -> Vec<Option<Alignment>> {
183 children
184 .iter()
185 .map(|child| {
186 child
187 .parent_data::<crate::modifier::AlignmentParentData>(scope)
188 .map(|data| data.alignment)
189 })
190 .collect()
191}
192
193#[cfg(test)]
194mod tests {
195 use tessera_ui::{
196 AxisConstraint, ComputedData, LayoutResult, MeasurementError, Modifier, NoopDrawModifier,
197 Px,
198 layout::{MeasureScope, layout},
199 tessera,
200 };
201
202 use crate::{
203 alignment::Alignment,
204 modifier::{ModifierExt as _, SemanticsArgs},
205 };
206
207 use super::boxed;
208
209 #[derive(Clone, PartialEq)]
210 struct FixedTestLayout {
211 width: i32,
212 height: i32,
213 }
214
215 impl tessera_ui::LayoutModifierNode for FixedTestLayout {
216 fn measure(&self, _input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
217 Ok(LayoutResult::new(ComputedData {
218 width: Px::new(self.width),
219 height: Px::new(self.height),
220 }))
221 }
222 }
223
224 #[tessera]
225 fn fixed_test_box(
226 modifier: Option<Modifier>,
227 tag: Option<String>,
228 width: Option<i32>,
229 height: Option<i32>,
230 ) {
231 let modifier = modifier.unwrap_or_default();
232 let tag = tag.unwrap_or_default();
233 let width = width.unwrap_or_default();
234 let height = height.unwrap_or_default();
235
236 layout()
237 .policy(FixedTestLayout { width, height })
238 .draw(NoopDrawModifier)
239 .modifier(modifier.then(Modifier::new().semantics(SemanticsArgs {
240 test_tag: Some(tag),
241 ..Default::default()
242 })));
243 }
244
245 #[tessera]
246 fn boxed_layout_case() {
247 boxed()
248 .alignment(Alignment::TopStart)
249 .modifier(Modifier::new().constrain(
250 Some(AxisConstraint::exact(Px::new(100))),
251 Some(AxisConstraint::exact(Px::new(80))),
252 ))
253 .children(|| {
254 boxed_start_box();
255 layout()
256 .modifier(Modifier::new().align(Alignment::BottomEnd))
257 .child(|| {
258 boxed_end_box();
259 });
260 });
261 }
262
263 #[tessera]
264 fn boxed_start_box() {
265 fixed_test_box()
266 .tag("boxed_start".to_string())
267 .width(20)
268 .height(10);
269 }
270
271 #[tessera]
272 fn boxed_end_box() {
273 fixed_test_box()
274 .tag("boxed_end".to_string())
275 .width(30)
276 .height(15);
277 }
278
279 #[tessera]
280 fn boxed_center_case() {
281 boxed()
282 .alignment(Alignment::Center)
283 .modifier(Modifier::new().constrain(
284 Some(AxisConstraint::exact(Px::new(100))),
285 Some(AxisConstraint::exact(Px::new(80))),
286 ))
287 .children(|| {
288 fixed_test_box()
289 .tag("boxed_center".to_string())
290 .width(20)
291 .height(10);
292 });
293 }
294
295 #[tessera]
296 fn boxed_forwarded_parent_data_case() {
297 boxed()
298 .alignment(Alignment::TopStart)
299 .modifier(Modifier::new().constrain(
300 Some(AxisConstraint::exact(Px::new(100))),
301 Some(AxisConstraint::exact(Px::new(80))),
302 ))
303 .children(|| {
304 fixed_test_box()
305 .modifier(Modifier::new().align(Alignment::Center))
306 .tag("boxed_forwarded".to_string())
307 .width(20)
308 .height(10);
309 });
310 }
311
312 #[test]
313 fn boxed_honors_child_alignment_override() {
314 tessera_ui::assert_layout! {
315 viewport: (120, 100),
316 content: {
317 boxed_layout_case();
318 },
319 expect: {
320 node("boxed_start").position(0, 0).size(20, 10);
321 node("boxed_end").position(70, 65).size(30, 15);
322 }
323 }
324 }
325
326 #[test]
327 fn boxed_uses_default_alignment_for_children_without_parent_data() {
328 tessera_ui::assert_layout! {
329 viewport: (120, 100),
330 content: {
331 boxed_center_case();
332 },
333 expect: {
334 node("boxed_center").position(40, 35).size(20, 10);
335 }
336 }
337 }
338
339 #[test]
340 fn boxed_honors_alignment_on_wrapped_children() {
341 tessera_ui::assert_layout! {
342 viewport: (120, 100),
343 content: {
344 boxed_forwarded_parent_data_case();
345 },
346 expect: {
347 node("boxed_forwarded").position(40, 35).size(20, 10);
348 }
349 }
350 }
351}