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
19struct PlaceChildrenArgs<'a, T> {
20 children_sizes: &'a [Option<ComputedData>],
21 children: &'a [T],
22 final_row_width: Px,
23 final_row_height: Px,
24 total_children_width: Px,
25 main_axis_alignment: MainAxisAlignment,
26 cross_axis_alignment: CrossAxisAlignment,
27 child_count: usize,
28}
29
30struct MeasureWeightedChildrenArgs<'a> {
31 weighted_indices: &'a [usize],
32 children_sizes: &'a mut [Option<ComputedData>],
33 max_child_height: &'a mut Px,
34 remaining_width: Px,
35 total_weight: f32,
36 row_parent_constraint: &'a Constraint,
37 child_weights: &'a [f32],
38 cross_axis_alignment: CrossAxisAlignment,
39}
40
41#[derive(Clone, PartialEq)]
42struct RowLayout {
43 main_axis_alignment: MainAxisAlignment,
44 cross_axis_alignment: CrossAxisAlignment,
45}
46
47impl tessera_ui::LayoutModifierNode for RowLayout {
48 fn measure_eq(&self, other: &Self) -> bool {
49 (self.cross_axis_alignment == CrossAxisAlignment::Stretch)
50 == (other.cross_axis_alignment == CrossAxisAlignment::Stretch)
51 }
52
53 fn place_children(
54 &self,
55 scope: &PlacementScope<'_>,
56 _inner: &PlacementResult,
57 ) -> Option<PlacementResult> {
58 let children = scope.children();
59 let children_sizes = children
60 .iter()
61 .map(|child| Some(child.size()))
62 .collect::<Vec<_>>();
63 let total_children_width = children
64 .iter()
65 .map(|child| child.size().width)
66 .fold(Px::ZERO, |total, size| total + size);
67 let mut result = LayoutResult::new(scope.size());
68 place_children_with_alignment(
69 &PlaceChildrenArgs {
70 children_sizes: &children_sizes,
71 children: &children,
72 final_row_width: scope.size().width,
73 final_row_height: scope.size().height,
74 total_children_width,
75 main_axis_alignment: self.main_axis_alignment,
76 cross_axis_alignment: self.cross_axis_alignment,
77 child_count: children.len(),
78 },
79 &mut result,
80 );
81 Some(result.into_placement_result())
82 }
83
84 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
85 let mut result = LayoutResult::default();
86 let children = input.children();
87 let child_weights = collect_child_weights(input, &children);
88 let row_parent_constraint = *input.parent_constraint().as_ref();
89
90 let has_weighted_children = child_weights.iter().any(|&weight| weight > 0.0);
91 let should_use_weight_for_width =
92 has_weighted_children && row_parent_constraint.width.resolve_max().is_some();
93
94 if should_use_weight_for_width {
95 measure_weighted_row(
96 &children,
97 input,
98 &mut result,
99 self.main_axis_alignment,
100 self.cross_axis_alignment,
101 &child_weights,
102 &row_parent_constraint,
103 )
104 } else {
105 measure_unweighted_row(
106 &children,
107 input,
108 &mut result,
109 self.main_axis_alignment,
110 self.cross_axis_alignment,
111 &row_parent_constraint,
112 )
113 }
114 }
115}
116
117#[tessera(tessera_ui, layout)]
151pub fn row(
152 modifier: Option<Modifier>,
153 main_axis_alignment: Option<MainAxisAlignment>,
154 cross_axis_alignment: Option<CrossAxisAlignment>,
155 children: Option<RenderSlot>,
156) {
157 let modifier = modifier.unwrap_or_default();
158 let main_axis_alignment = main_axis_alignment.unwrap_or_default();
159 let cross_axis_alignment = cross_axis_alignment.unwrap_or_default();
160 configure_layout_node(
161 RowLayout {
162 main_axis_alignment,
163 cross_axis_alignment,
164 },
165 NoopDrawModifier,
166 modifier,
167 );
168 children.unwrap_or_else(RenderSlot::empty).render();
169}
170
171fn measure_weighted_row(
172 children: &[LayoutChild],
173 input: &mut MeasureScope<'_>,
174 result: &mut LayoutResult,
175 main_axis_alignment: MainAxisAlignment,
176 cross_axis_alignment: CrossAxisAlignment,
177 child_weights: &[f32],
178 row_parent_constraint: &Constraint,
179) -> Result<LayoutResult, MeasurementError> {
180 let mut children_sizes = vec![None; child_weights.len()];
181 let mut max_child_height = Px(0);
182 let available_width_for_children = row_parent_constraint
183 .width
184 .resolve_max()
185 .expect("Row width Fill expected with finite max constraint");
186
187 let (weighted_indices, unweighted_indices, total_weight) = classify_children(child_weights);
188
189 let total_width_of_unweighted_children = measure_unweighted_children(
190 children,
191 input,
192 &unweighted_indices,
193 &mut children_sizes,
194 &mut max_child_height,
195 row_parent_constraint,
196 cross_axis_alignment,
197 )?;
198
199 measure_weighted_children(
200 children,
201 input,
202 &mut MeasureWeightedChildrenArgs {
203 weighted_indices: &weighted_indices,
204 children_sizes: &mut children_sizes,
205 max_child_height: &mut max_child_height,
206 remaining_width: (available_width_for_children - total_width_of_unweighted_children)
207 .max(Px::ZERO),
208 total_weight,
209 row_parent_constraint,
210 child_weights,
211 cross_axis_alignment,
212 },
213 )?;
214
215 let final_row_width = available_width_for_children;
216 let final_row_height = row_parent_constraint.height.clamp(max_child_height);
217
218 let total_measured_children_width: Px = children_sizes
219 .iter()
220 .filter_map(|s| s.map(|s| s.width))
221 .fold(Px(0), |acc, w| acc + w);
222
223 place_children_with_alignment(
224 &PlaceChildrenArgs {
225 children_sizes: &children_sizes,
226 children,
227 final_row_width,
228 final_row_height,
229 total_children_width: total_measured_children_width,
230 main_axis_alignment,
231 cross_axis_alignment,
232 child_count: child_weights.len(),
233 },
234 result,
235 );
236
237 result.size = ComputedData {
238 width: final_row_width,
239 height: final_row_height,
240 };
241 Ok(result.clone())
242}
243
244fn measure_unweighted_row(
245 children: &[LayoutChild],
246 input: &mut MeasureScope<'_>,
247 result: &mut LayoutResult,
248 main_axis_alignment: MainAxisAlignment,
249 cross_axis_alignment: CrossAxisAlignment,
250 row_parent_constraint: &Constraint,
251) -> Result<LayoutResult, MeasurementError> {
252 let mut children_sizes = vec![None; children.len()];
253 let mut total_children_measured_width = Px(0);
254 let mut max_child_height = Px(0);
255
256 for (i, child_id) in children.iter().enumerate() {
257 let parent_offered_constraint_for_child = Constraint::new(
258 AxisConstraint::new(
259 Px::ZERO,
260 row_parent_constraint
261 .width
262 .resolve_max()
263 .map(|max| (max - total_children_measured_width).max(Px::ZERO)),
264 ),
265 cross_axis_child_constraint(row_parent_constraint.height, cross_axis_alignment),
266 );
267 let child_result = child_id.measure(input, &parent_offered_constraint_for_child)?;
268 children_sizes[i] = Some(child_result.size());
269 total_children_measured_width += child_result.width;
270 max_child_height = max_child_height.max(child_result.height);
271 }
272
273 let final_row_width = row_parent_constraint
274 .width
275 .clamp(total_children_measured_width);
276 let final_row_height = row_parent_constraint.height.clamp(max_child_height);
277
278 place_children_with_alignment(
279 &PlaceChildrenArgs {
280 children_sizes: &children_sizes,
281 children,
282 final_row_width,
283 final_row_height,
284 total_children_width: total_children_measured_width,
285 main_axis_alignment,
286 cross_axis_alignment,
287 child_count: children_sizes.len(),
288 },
289 result,
290 );
291
292 result.size = ComputedData {
293 width: final_row_width,
294 height: final_row_height,
295 };
296 Ok(result.clone())
297}
298
299fn measure_unweighted_children(
300 children: &[LayoutChild],
301 input: &mut MeasureScope<'_>,
302 unweighted_indices: &[usize],
303 children_sizes: &mut [Option<ComputedData>],
304 max_child_height: &mut Px,
305 row_parent_constraint: &Constraint,
306 cross_axis_alignment: CrossAxisAlignment,
307) -> Result<Px, MeasurementError> {
308 let mut total_width = Px(0);
309
310 for &child_idx in unweighted_indices {
311 let parent_offered_constraint_for_child = Constraint::new(
312 AxisConstraint::new(
313 Px::ZERO,
314 row_parent_constraint
315 .width
316 .resolve_max()
317 .map(|max| (max - total_width).max(Px::ZERO)),
318 ),
319 cross_axis_child_constraint(row_parent_constraint.height, cross_axis_alignment),
320 );
321 let child_id = children[child_idx];
322 let child_result = child_id.measure(input, &parent_offered_constraint_for_child)?;
323 children_sizes[child_idx] = Some(child_result.size());
324 total_width += child_result.width;
325 *max_child_height = (*max_child_height).max(child_result.height);
326 }
327
328 Ok(total_width)
329}
330
331fn measure_weighted_children(
332 children: &[LayoutChild],
333 scope: &mut MeasureScope<'_>,
334 args: &mut MeasureWeightedChildrenArgs<'_>,
335) -> Result<(), MeasurementError> {
336 if args.total_weight <= 0.0 {
337 return Ok(());
338 }
339
340 for &child_idx in args.weighted_indices {
341 let child_weight = args.child_weights[child_idx];
342 let allocated_width =
343 Px((args.remaining_width.0 as f32 * (child_weight / args.total_weight)) as i32);
344 let child_id = children[child_idx];
345 let parent_offered_constraint_for_child = Constraint::new(
346 AxisConstraint::exact(allocated_width),
347 cross_axis_child_constraint(
348 args.row_parent_constraint.height,
349 args.cross_axis_alignment,
350 ),
351 );
352 let child_result = child_id.measure(scope, &parent_offered_constraint_for_child)?;
353 args.children_sizes[child_idx] = Some(child_result.size());
354 *args.max_child_height = (*args.max_child_height).max(child_result.height);
355 }
356
357 Ok(())
358}
359
360fn collect_child_weights(input: &MeasureScope<'_>, children: &[LayoutChild]) -> Vec<f32> {
361 children
362 .iter()
363 .map(|child_id| {
364 child_id
365 .parent_data::<crate::modifier::WeightParentData>(input)
366 .map(|data| data.weight)
367 .unwrap_or(0.0)
368 })
369 .collect()
370}
371
372fn place_children_with_alignment<T: LayoutPlacementTarget>(
373 args: &PlaceChildrenArgs<'_, T>,
374 result: &mut LayoutResult,
375) {
376 let (mut current_x, spacing) = crate::layout_flex::main_axis_offsets(
377 args.final_row_width,
378 args.total_children_width,
379 args.main_axis_alignment,
380 args.child_count,
381 );
382
383 for (i, child_size_opt) in args.children_sizes.iter().enumerate() {
384 if let Some(child_actual_size) = child_size_opt {
385 let child_id = args.children[i];
386 let y_offset = crate::layout_flex::cross_axis_offset(
387 child_actual_size.height,
388 args.final_row_height,
389 args.cross_axis_alignment,
390 );
391
392 result.place_child(child_id, PxPosition::from_f32_xy(current_x, y_offset));
393 current_x += child_actual_size.width.0 as f32;
394 if i < args.child_count - 1 {
395 current_x += spacing;
396 }
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use tessera_ui::{
404 AxisConstraint, ComputedData, LayoutResult, MeasurementError, Modifier, NoopDrawModifier,
405 Px,
406 layout::{MeasureScope, layout},
407 tessera,
408 };
409
410 use crate::{
411 alignment::{CrossAxisAlignment, MainAxisAlignment},
412 modifier::{ModifierExt as _, SemanticsArgs},
413 };
414
415 use super::row;
416
417 #[derive(Clone, PartialEq)]
418 struct FixedTestLayout {
419 width: i32,
420 height: i32,
421 }
422
423 #[derive(Clone, PartialEq)]
424 struct FillWidthTestLayout {
425 height: i32,
426 }
427
428 impl tessera_ui::LayoutModifierNode for FixedTestLayout {
429 fn measure(&self, _input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
430 Ok(LayoutResult::new(ComputedData {
431 width: Px::new(self.width),
432 height: Px::new(self.height),
433 }))
434 }
435 }
436
437 impl tessera_ui::LayoutModifierNode for FillWidthTestLayout {
438 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
439 let width = input
440 .parent_constraint()
441 .width()
442 .resolve_max()
443 .expect("FillWidthTestLayout requires a bounded width constraint");
444
445 Ok(LayoutResult::new(ComputedData {
446 width,
447 height: Px::new(self.height),
448 }))
449 }
450 }
451
452 #[tessera]
453 fn fixed_test_box(tag: Option<String>, width: Option<i32>, height: Option<i32>) {
454 let tag = tag.unwrap_or_default();
455 let width = width.unwrap_or_default();
456 let height = height.unwrap_or_default();
457
458 layout()
459 .policy(FixedTestLayout { width, height })
460 .draw(NoopDrawModifier)
461 .modifier(Modifier::new().semantics(SemanticsArgs {
462 test_tag: Some(tag),
463 ..Default::default()
464 }));
465 }
466
467 #[tessera]
468 fn fill_width_test_box(tag: Option<String>, height: Option<i32>) {
469 let tag = tag.unwrap_or_default();
470 let height = height.unwrap_or_default();
471
472 layout()
473 .policy(FillWidthTestLayout { height })
474 .draw(NoopDrawModifier)
475 .modifier(Modifier::new().semantics(SemanticsArgs {
476 test_tag: Some(tag),
477 ..Default::default()
478 }));
479 }
480
481 #[tessera]
482 fn row_layout_case() {
483 row()
484 .modifier(Modifier::new().constrain(
485 Some(AxisConstraint::exact(Px::new(100))),
486 Some(AxisConstraint::exact(Px::new(30))),
487 ))
488 .main_axis_alignment(MainAxisAlignment::Start)
489 .cross_axis_alignment(CrossAxisAlignment::Start)
490 .children(|| {
491 row_fixed_box();
492 layout().modifier(Modifier::new().weight(1.0)).child(|| {
493 row_weighted_content_box();
494 });
495 });
496 }
497
498 #[tessera]
499 fn row_fixed_box() {
500 fixed_test_box()
501 .tag("row_fixed".to_string())
502 .width(20)
503 .height(10);
504 }
505
506 #[tessera]
507 fn row_weighted_content_box() {
508 fill_width_test_box()
509 .tag("row_weighted_content".to_string())
510 .height(10);
511 }
512
513 #[tessera]
514 fn row_alignment_case() {
515 row()
516 .modifier(Modifier::new().constrain(
517 Some(AxisConstraint::exact(Px::new(100))),
518 Some(AxisConstraint::exact(Px::new(30))),
519 ))
520 .main_axis_alignment(MainAxisAlignment::Center)
521 .cross_axis_alignment(CrossAxisAlignment::End)
522 .children(|| {
523 fixed_test_box()
524 .tag("row_center_first".to_string())
525 .width(20)
526 .height(10);
527 fixed_test_box()
528 .tag("row_center_second".to_string())
529 .width(10)
530 .height(12);
531 });
532 }
533
534 #[test]
535 fn row_allocates_remaining_width_to_weighted_child() {
536 tessera_ui::assert_layout! {
537 viewport: (120, 60),
538 content: {
539 row_layout_case();
540 },
541 expect: {
542 node("row_fixed").position(0, 0).size(20, 10);
543 node("row_weighted_content").position(20, 0).size(80, 10);
544 }
545 }
546 }
547
548 #[test]
549 fn row_honors_main_and_cross_axis_alignment() {
550 tessera_ui::assert_layout! {
551 viewport: (120, 60),
552 content: {
553 row_alignment_case();
554 },
555 expect: {
556 node("row_center_first").position(35, 20).size(20, 10);
557 node("row_center_second").position(55, 18).size(10, 12);
558 }
559 }
560 }
561
562 #[tessera]
563 fn constrained_unweighted_case(weighted: bool) {
564 row()
565 .modifier(Modifier::new().constrain(Some(AxisConstraint::exact(Px(100))), None))
566 .children(move || {
567 fixed_test_box()
568 .tag("reserved".to_string())
569 .width(20)
570 .height(10);
571 fill_width_test_box()
572 .tag("remaining".to_string())
573 .height(10);
574 if weighted {
575 layout().modifier(Modifier::new().weight(1.0)).child(|| {
576 fill_width_test_box()
577 .tag("exhausted".to_string())
578 .height(10);
579 });
580 } else {
581 fill_width_test_box()
582 .tag("exhausted".to_string())
583 .height(10);
584 }
585 });
586 }
587
588 #[test]
589 fn unweighted_children_receive_only_the_remaining_row_width() {
590 tessera_ui::assert_layout! {
591 viewport: (120, 60),
592 content: { constrained_unweighted_case(false); },
593 expect: {
594 node("reserved").position(0, 0).size(20, 10);
595 node("remaining").position(20, 0).size(80, 10);
596 node("exhausted").position(100, 0).size(0, 10);
597 }
598 }
599 }
600
601 #[test]
602 fn unweighted_children_cannot_overallocate_before_weighted_children() {
603 tessera_ui::assert_layout! {
604 viewport: (120, 60),
605 content: { constrained_unweighted_case(true); },
606 expect: {
607 node("reserved").position(0, 0).size(20, 10);
608 node("remaining").position(20, 0).size(80, 10);
609 node("exhausted").position(100, 0).size(0, 10);
610 }
611 }
612 }
613}