1use tessera_ui::{
7 __private::configure_layout_node,
8 AxisConstraint, ComputedData, Constraint, LayoutResult, MeasurementError, Modifier,
9 NoopDrawModifier, Px, PxPosition, RenderSlot,
10 layout::{LayoutChild, LayoutPlacementTarget, MeasureScope, PlacementResult, PlacementScope},
11 tessera,
12};
13
14use crate::{
15 alignment::{CrossAxisAlignment, MainAxisAlignment},
16 layout_flex::{classify_children, cross_axis_child_constraint},
17};
18
19#[tessera(tessera_ui, layout)]
55pub fn column(
56 modifier: Option<Modifier>,
57 main_axis_alignment: Option<MainAxisAlignment>,
58 cross_axis_alignment: Option<CrossAxisAlignment>,
59 children: Option<RenderSlot>,
60) {
61 let modifier = modifier.unwrap_or_default();
62 let main_axis_alignment = main_axis_alignment.unwrap_or_default();
63 let cross_axis_alignment = cross_axis_alignment.unwrap_or_default();
64 configure_layout_node(
65 ColumnLayout {
66 main_axis_alignment,
67 cross_axis_alignment,
68 },
69 NoopDrawModifier,
70 modifier,
71 );
72 children.unwrap_or_else(RenderSlot::empty).render();
73}
74
75#[derive(Clone, PartialEq)]
76struct ColumnLayout {
77 main_axis_alignment: MainAxisAlignment,
78 cross_axis_alignment: CrossAxisAlignment,
79}
80
81impl tessera_ui::LayoutModifierNode for ColumnLayout {
82 fn measure_eq(&self, other: &Self) -> bool {
83 (self.cross_axis_alignment == CrossAxisAlignment::Stretch)
84 == (other.cross_axis_alignment == CrossAxisAlignment::Stretch)
85 }
86
87 fn place_children(
88 &self,
89 scope: &PlacementScope<'_>,
90 _inner: &PlacementResult,
91 ) -> Option<PlacementResult> {
92 let children = scope.children();
93 let children_sizes = children
94 .iter()
95 .map(|child| Some(child.size()))
96 .collect::<Vec<_>>();
97 let total_children_height = children
98 .iter()
99 .map(|child| child.size().height)
100 .fold(Px::ZERO, |total, size| total + size);
101 let mut result = LayoutResult::new(scope.size());
102 place_children_with_alignment(
103 &PlaceChildrenArgs {
104 children_sizes: &children_sizes,
105 children: &children,
106 final_column_width: scope.size().width,
107 final_column_height: scope.size().height,
108 total_children_height,
109 main_axis_alignment: self.main_axis_alignment,
110 cross_axis_alignment: self.cross_axis_alignment,
111 child_count: children.len(),
112 },
113 &mut result,
114 );
115 Some(result.into_placement_result())
116 }
117
118 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
119 let mut result = LayoutResult::default();
120 let children = input.children();
121 let child_weights = collect_child_weights(input, &children);
122 let n = child_weights.len();
123 assert_eq!(
124 children.len(),
125 n,
126 "Mismatch between children defined in scope and runtime children count"
127 );
128
129 let column_parent_constraint = *input.parent_constraint().as_ref();
130
131 let mut children_sizes = vec![None; n];
132 let mut max_child_width = Px(0);
133
134 let has_weighted_children = child_weights.iter().any(|&weight| weight > 0.0);
135 let should_use_weight_for_height =
136 has_weighted_children && column_parent_constraint.height.resolve_max().is_some();
137
138 let (final_column_width, final_column_height, total_measured_children_height) =
139 if should_use_weight_for_height {
140 measure_weighted_column(
141 &children,
142 input,
143 &child_weights,
144 &column_parent_constraint,
145 &mut children_sizes,
146 &mut max_child_width,
147 self.cross_axis_alignment,
148 )?
149 } else {
150 measure_unweighted_column(
151 &children,
152 input,
153 &column_parent_constraint,
154 &mut children_sizes,
155 &mut max_child_width,
156 self.cross_axis_alignment,
157 )?
158 };
159
160 place_children_with_alignment(
161 &PlaceChildrenArgs {
162 children_sizes: &children_sizes,
163 children: &children,
164 final_column_width,
165 final_column_height,
166 total_children_height: total_measured_children_height,
167 main_axis_alignment: self.main_axis_alignment,
168 cross_axis_alignment: self.cross_axis_alignment,
169 child_count: n,
170 },
171 &mut result,
172 );
173
174 Ok(result.with_size(ComputedData {
175 width: final_column_width,
176 height: final_column_height,
177 }))
178 }
179}
180
181struct PlaceChildrenArgs<'a, T> {
182 children_sizes: &'a [Option<ComputedData>],
183 children: &'a [T],
184 final_column_width: Px,
185 final_column_height: Px,
186 total_children_height: Px,
187 main_axis_alignment: MainAxisAlignment,
188 cross_axis_alignment: CrossAxisAlignment,
189 child_count: usize,
190}
191
192fn measure_unweighted_children_for_column(
193 children: &[LayoutChild],
194 input: &mut MeasureScope<'_>,
195 indices: &[usize],
196 children_sizes: &mut [Option<ComputedData>],
197 max_child_width: &mut Px,
198 column_parent_constraint: &Constraint,
199 cross_axis_alignment: CrossAxisAlignment,
200) -> Result<Px, MeasurementError> {
201 let mut total = Px(0);
202
203 let parent_offered_constraint_for_child = Constraint::new(
204 cross_axis_child_constraint(column_parent_constraint.width, cross_axis_alignment),
205 column_parent_constraint.height.without_min(),
206 );
207
208 for &child_idx in indices {
209 let child_id = children[child_idx];
210 let child_result = child_id.measure(input, &parent_offered_constraint_for_child)?;
211 children_sizes[child_idx] = Some(child_result.size());
212 total += child_result.height;
213 *max_child_width = (*max_child_width).max(child_result.width);
214 }
215
216 Ok(total)
217}
218
219struct WeightedColumnMeasureContext<'a> {
220 children_sizes: &'a mut [Option<ComputedData>],
221 max_child_width: &'a mut Px,
222 column_parent_constraint: &'a Constraint,
223 child_weights: &'a [f32],
224 cross_axis_alignment: CrossAxisAlignment,
225}
226
227fn measure_weighted_children_for_column(
228 children: &[LayoutChild],
229 scope: &mut MeasureScope<'_>,
230 ctx: WeightedColumnMeasureContext<'_>,
231 weighted_indices: &[usize],
232 remaining_height: Px,
233 total_weight: f32,
234) -> Result<(), MeasurementError> {
235 if total_weight <= 0.0 {
236 return Ok(());
237 }
238
239 for &child_idx in weighted_indices {
240 let child_weight = ctx.child_weights[child_idx];
241 let allocated_height =
242 Px((remaining_height.0 as f32 * (child_weight / total_weight)) as i32);
243 let child_id = children[child_idx];
244 let parent_offered_constraint_for_child = Constraint::new(
245 cross_axis_child_constraint(
246 ctx.column_parent_constraint.width,
247 ctx.cross_axis_alignment,
248 ),
249 AxisConstraint::exact(allocated_height),
250 );
251 let child_result = child_id.measure(scope, &parent_offered_constraint_for_child)?;
252 ctx.children_sizes[child_idx] = Some(child_result.size());
253 *ctx.max_child_width = (*ctx.max_child_width).max(child_result.width);
254 }
255
256 Ok(())
257}
258
259fn measure_weighted_column(
260 children: &[LayoutChild],
261 input: &mut MeasureScope<'_>,
262 child_weights: &[f32],
263 column_parent_constraint: &Constraint,
264 children_sizes: &mut [Option<ComputedData>],
265 max_child_width: &mut Px,
266 cross_axis_alignment: CrossAxisAlignment,
267) -> Result<(Px, Px, Px), MeasurementError> {
268 let available_height_for_children = column_parent_constraint
269 .height
270 .resolve_max()
271 .expect("Column height Fill expected with finite max constraint");
272
273 let (weighted_children_indices, unweighted_children_indices, total_weight_sum) =
274 classify_children(child_weights);
275
276 let total_height_of_unweighted_children = measure_unweighted_children_for_column(
277 children,
278 input,
279 &unweighted_children_indices,
280 children_sizes,
281 max_child_width,
282 column_parent_constraint,
283 cross_axis_alignment,
284 )?;
285
286 let remaining_height_for_weighted_children =
287 (available_height_for_children - total_height_of_unweighted_children).max(Px(0));
288
289 measure_weighted_children_for_column(
290 children,
291 input,
292 WeightedColumnMeasureContext {
293 children_sizes,
294 max_child_width,
295 column_parent_constraint,
296 child_weights,
297 cross_axis_alignment,
298 },
299 &weighted_children_indices,
300 remaining_height_for_weighted_children,
301 total_weight_sum,
302 )?;
303
304 let total_measured_children_height: Px = children_sizes
305 .iter()
306 .filter_map(|s| s.as_ref().map(|s| s.height))
307 .fold(Px(0), |acc, h| acc + h);
308
309 let final_column_height = column_parent_constraint
310 .height
311 .clamp(total_measured_children_height);
312 let final_column_width = column_parent_constraint.width.clamp(*max_child_width);
313
314 Ok((
315 final_column_width,
316 final_column_height,
317 total_measured_children_height,
318 ))
319}
320
321fn collect_child_weights(input: &MeasureScope<'_>, children: &[LayoutChild]) -> Vec<f32> {
322 children
323 .iter()
324 .map(|child_id| {
325 child_id
326 .parent_data::<crate::modifier::WeightParentData>(input)
327 .map(|data| data.weight)
328 .unwrap_or(0.0)
329 })
330 .collect()
331}
332
333fn measure_unweighted_column(
334 children: &[LayoutChild],
335 input: &mut MeasureScope<'_>,
336 column_parent_constraint: &Constraint,
337 children_sizes: &mut [Option<ComputedData>],
338 max_child_width: &mut Px,
339 cross_axis_alignment: CrossAxisAlignment,
340) -> Result<(Px, Px, Px), MeasurementError> {
341 let n = children_sizes.len();
342 let mut total_children_measured_height = Px(0);
343
344 let parent_offered_constraint_for_child = Constraint::new(
345 cross_axis_child_constraint(column_parent_constraint.width, cross_axis_alignment),
346 column_parent_constraint.height.without_min(),
347 );
348
349 for (i, child_id) in children.iter().enumerate().take(n) {
350 let child_result = child_id.measure(input, &parent_offered_constraint_for_child)?;
351 children_sizes[i] = Some(child_result.size());
352 total_children_measured_height += child_result.height;
353 *max_child_width = (*max_child_width).max(child_result.width);
354 }
355
356 let final_column_height = column_parent_constraint
357 .height
358 .clamp(total_children_measured_height);
359 let final_column_width = column_parent_constraint.width.clamp(*max_child_width);
360 Ok((
361 final_column_width,
362 final_column_height,
363 total_children_measured_height,
364 ))
365}
366
367fn place_children_with_alignment<T: LayoutPlacementTarget>(
368 args: &PlaceChildrenArgs<'_, T>,
369 result: &mut LayoutResult,
370) {
371 let (mut current_y, spacing_between_children) = crate::layout_flex::main_axis_offsets(
372 args.final_column_height,
373 args.total_children_height,
374 args.main_axis_alignment,
375 args.child_count,
376 );
377
378 for (i, child_size_opt) in args.children_sizes.iter().enumerate() {
379 if let Some(child_actual_size) = child_size_opt {
380 let child_id = args.children[i];
381 let x_offset = crate::layout_flex::cross_axis_offset(
382 child_actual_size.width,
383 args.final_column_width,
384 args.cross_axis_alignment,
385 );
386 result.place_child(child_id, PxPosition::from_f32_xy(x_offset, current_y));
387 current_y += child_actual_size.height.0 as f32;
388 if i < args.child_count - 1 {
389 current_y += spacing_between_children;
390 }
391 }
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use tessera_ui::{
398 AxisConstraint, ComputedData, Dp, LayoutResult, MeasurementError, Modifier,
399 NoopDrawModifier, Px,
400 layout::{MeasureScope, layout},
401 tessera,
402 };
403
404 use crate::{
405 alignment::{CrossAxisAlignment, MainAxisAlignment},
406 modifier::{ModifierExt as _, SemanticsArgs},
407 };
408
409 use super::column;
410
411 #[derive(Clone, PartialEq)]
412 struct FixedTestLayout {
413 width: i32,
414 height: i32,
415 }
416
417 #[derive(Clone, PartialEq)]
418 struct FillHeightTestLayout {
419 width: i32,
420 }
421
422 impl tessera_ui::LayoutModifierNode for FixedTestLayout {
423 fn measure(&self, _input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
424 Ok(LayoutResult::new(ComputedData {
425 width: Px::new(self.width),
426 height: Px::new(self.height),
427 }))
428 }
429 }
430
431 impl tessera_ui::LayoutModifierNode for FillHeightTestLayout {
432 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
433 let height = input
434 .parent_constraint()
435 .height()
436 .resolve_max()
437 .expect("FillHeightTestLayout requires a bounded height constraint");
438
439 Ok(LayoutResult::new(ComputedData {
440 width: Px::new(self.width),
441 height,
442 }))
443 }
444 }
445
446 #[tessera]
447 fn fixed_test_box(tag: Option<String>, width: Option<i32>, height: Option<i32>) {
448 let tag = tag.unwrap_or_default();
449 let width = width.unwrap_or_default();
450 let height = height.unwrap_or_default();
451
452 layout()
453 .policy(FixedTestLayout { width, height })
454 .draw(NoopDrawModifier)
455 .modifier(Modifier::new().semantics(SemanticsArgs {
456 test_tag: Some(tag),
457 ..Default::default()
458 }));
459 }
460
461 #[tessera]
462 fn fill_height_test_box(tag: Option<String>, width: Option<i32>) {
463 let tag = tag.unwrap_or_default();
464 let width = width.unwrap_or_default();
465
466 layout()
467 .policy(FillHeightTestLayout { width })
468 .draw(NoopDrawModifier)
469 .modifier(Modifier::new().semantics(SemanticsArgs {
470 test_tag: Some(tag),
471 ..Default::default()
472 }));
473 }
474
475 #[tessera]
476 fn column_layout_case() {
477 column()
478 .modifier(Modifier::new().constrain(
479 Some(AxisConstraint::exact(Px::new(100))),
480 Some(AxisConstraint::exact(Px::new(60))),
481 ))
482 .main_axis_alignment(MainAxisAlignment::Start)
483 .cross_axis_alignment(CrossAxisAlignment::Center)
484 .children(|| {
485 column_first_box();
486 column_second_box();
487 });
488 }
489
490 #[tessera]
491 fn column_first_box() {
492 fixed_test_box()
493 .tag("column_first".to_string())
494 .width(20)
495 .height(10);
496 }
497
498 #[tessera]
499 fn column_second_box() {
500 fixed_test_box()
501 .tag("column_second".to_string())
502 .width(40)
503 .height(15);
504 }
505
506 #[tessera]
507 fn column_weighted_case() {
508 column()
509 .modifier(Modifier::new().constrain(
510 Some(AxisConstraint::exact(Px::new(60))),
511 Some(AxisConstraint::exact(Px::new(90))),
512 ))
513 .main_axis_alignment(MainAxisAlignment::Start)
514 .cross_axis_alignment(CrossAxisAlignment::Start)
515 .children(|| {
516 fixed_test_box()
517 .tag("column_weighted_fixed".to_string())
518 .width(20)
519 .height(10);
520 layout().modifier(Modifier::new().weight(1.0)).child(|| {
521 fill_height_test_box()
522 .tag("column_weighted_fill".to_string())
523 .width(15);
524 });
525 });
526 }
527
528 #[test]
529 fn column_centers_children_on_cross_axis() {
530 tessera_ui::assert_layout! {
531 viewport: (120, 80),
532 content: {
533 column_layout_case();
534 },
535 expect: {
536 node("column_first").position(40, 0).size(20, 10);
537 node("column_second").position(30, 10).size(40, 15);
538 }
539 }
540 }
541
542 #[test]
543 fn column_allocates_remaining_height_to_weighted_child() {
544 tessera_ui::assert_layout! {
545 viewport: (100, 100),
546 content: {
547 column_weighted_case();
548 },
549 expect: {
550 node("column_weighted_fixed").position(0, 0).size(20, 10);
551 node("column_weighted_fill").position(0, 10).size(15, 80);
552 }
553 }
554 }
555
556 #[tessera]
557 fn sized_test_box(tag: Option<String>, width: Option<Dp>, height: Option<Dp>) {
558 let tag = tag.unwrap_or_default();
559 let width = width.unwrap_or(Dp(0.0));
560 let height = height.unwrap_or(Dp(0.0));
561
562 layout()
563 .draw(NoopDrawModifier)
564 .modifier(
565 Modifier::new()
566 .size(width, height)
567 .semantics(SemanticsArgs {
568 test_tag: Some(tag),
569 ..Default::default()
570 }),
571 );
572 }
573
574 #[tessera]
575 fn column_center_sized_case() {
576 column()
577 .modifier(Modifier::new().fill_max_width())
578 .main_axis_alignment(MainAxisAlignment::Start)
579 .cross_axis_alignment(CrossAxisAlignment::Center)
580 .children(|| {
581 sized_test_box()
582 .tag("column_center_sized".to_string())
583 .width(Dp(32.0))
584 .height(Dp(4.0));
585 });
586 }
587
588 #[tessera]
589 fn column_stretch_sized_case() {
590 column()
591 .modifier(Modifier::new().fill_max_width())
592 .main_axis_alignment(MainAxisAlignment::Start)
593 .cross_axis_alignment(CrossAxisAlignment::Stretch)
594 .children(|| {
595 sized_test_box()
596 .tag("column_stretch_sized".to_string())
597 .width(Dp(32.0))
598 .height(Dp(4.0));
599 });
600 }
601
602 #[test]
603 fn column_measures_center_aligned_sized_child_intrinsically() {
604 tessera_ui::assert_layout! {
605 viewport: (100, 60),
606 content: {
607 column_center_sized_case();
608 },
609 expect: {
610 node("column_center_sized").position(34, 0).size(32, 4);
611 }
612 }
613 }
614
615 #[test]
616 fn column_stretches_sized_child_to_container_width() {
617 tessera_ui::assert_layout! {
618 viewport: (100, 60),
619 content: {
620 column_stretch_sized_case();
621 },
622 expect: {
623 node("column_stretch_sized").position(0, 0).size(100, 4);
624 }
625 }
626 }
627}