tessera_ui_basic_components/
row.rs

1//! A horizontal layout component.
2//!
3//! ## Usage
4//!
5//! Use to stack children horizontally.
6use derive_builder::Builder;
7use tessera_ui::{
8    ComputedData, Constraint, DimensionValue, MeasureInput, MeasurementError, NodeId, Px,
9    PxPosition, tessera,
10};
11
12use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
13
14/// Arguments for the `row` component.
15#[derive(Builder, Clone, Debug)]
16#[builder(pattern = "owned")]
17pub struct RowArgs {
18    /// Width behavior for the row.
19    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
20    pub width: DimensionValue,
21    /// Height behavior for the row.
22    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
23    pub height: DimensionValue,
24    /// Main axis alignment (horizontal alignment).
25    #[builder(default = "MainAxisAlignment::Start")]
26    pub main_axis_alignment: MainAxisAlignment,
27    /// Cross axis alignment (vertical alignment).
28    #[builder(default = "CrossAxisAlignment::Start")]
29    pub cross_axis_alignment: CrossAxisAlignment,
30}
31
32impl Default for RowArgs {
33    fn default() -> Self {
34        RowArgsBuilder::default()
35            .build()
36            .expect("builder construction failed")
37    }
38}
39
40/// A scope for declaratively adding children to a `row` component.
41pub struct RowScope<'a> {
42    child_closures: &'a mut Vec<Box<dyn FnOnce() + Send + Sync>>,
43    child_weights: &'a mut Vec<Option<f32>>,
44}
45
46impl<'a> RowScope<'a> {
47    /// Adds a child component to the row.
48    pub fn child<F>(&mut self, child_closure: F)
49    where
50        F: FnOnce() + Send + Sync + 'static,
51    {
52        self.child_closures.push(Box::new(child_closure));
53        self.child_weights.push(None);
54    }
55
56    /// Adds a child component to the row with a specified weight for flexible space distribution.
57    pub fn child_weighted<F>(&mut self, child_closure: F, weight: f32)
58    where
59        F: FnOnce() + Send + Sync + 'static,
60    {
61        self.child_closures.push(Box::new(child_closure));
62        self.child_weights.push(Some(weight));
63    }
64}
65
66struct PlaceChildrenArgs<'a> {
67    children_sizes: &'a [Option<ComputedData>],
68    children_ids: &'a [NodeId],
69    input: &'a MeasureInput<'a>,
70    final_row_width: Px,
71    final_row_height: Px,
72    total_children_width: Px,
73    main_axis_alignment: MainAxisAlignment,
74    cross_axis_alignment: CrossAxisAlignment,
75    child_count: usize,
76}
77
78struct MeasureWeightedChildrenArgs<'a> {
79    input: &'a MeasureInput<'a>,
80    weighted_indices: &'a [usize],
81    children_sizes: &'a mut [Option<ComputedData>],
82    max_child_height: &'a mut Px,
83    remaining_width: Px,
84    total_weight: f32,
85    row_effective_constraint: &'a Constraint,
86    child_weights: &'a [Option<f32>],
87}
88
89/// # row
90///
91/// A layout component that arranges its children in a horizontal row.
92///
93/// ## Usage
94///
95/// Stack components horizontally, with options for alignment and flexible spacing.
96///
97/// ## Parameters
98///
99/// - `args` — configures the row's dimensions and alignment; see [`RowArgs`].
100/// - `scope_config` — a closure that receives a [`RowScope`] for adding children.
101///
102/// ## Examples
103///
104/// ```
105/// use tessera_ui_basic_components::{
106///     row::{row, RowArgs},
107///     text::{text, TextArgsBuilder},
108///     spacer::{spacer, SpacerArgs},
109/// };
110///
111/// row(RowArgs::default(), |scope| {
112///     scope.child(|| text(TextArgsBuilder::default().text("First".to_string()).build().expect("builder construction failed")));
113///     scope.child_weighted(|| spacer(SpacerArgs::default()), 1.0); // Flexible space
114///     scope.child(|| text(TextArgsBuilder::default().text("Last".to_string()).build().expect("builder construction failed")));
115/// });
116/// ```
117#[tessera]
118pub fn row<F>(args: RowArgs, scope_config: F)
119where
120    F: FnOnce(&mut RowScope),
121{
122    let mut child_closures: Vec<Box<dyn FnOnce() + Send + Sync>> = Vec::new();
123    let mut child_weights: Vec<Option<f32>> = Vec::new();
124
125    {
126        let mut scope = RowScope {
127            child_closures: &mut child_closures,
128            child_weights: &mut child_weights,
129        };
130        scope_config(&mut scope);
131    }
132
133    let n = child_closures.len();
134
135    measure(Box::new(
136        move |input| -> Result<ComputedData, MeasurementError> {
137            assert_eq!(
138                input.children_ids.len(),
139                n,
140                "Mismatch between children defined in scope and runtime children count"
141            );
142
143            let row_intrinsic_constraint = Constraint::new(args.width, args.height);
144            let row_effective_constraint = row_intrinsic_constraint.merge(input.parent_constraint);
145
146            let has_weighted_children = child_weights.iter().any(|w| w.unwrap_or(0.0) > 0.0);
147            let should_use_weight_for_width = has_weighted_children
148                && matches!(
149                    row_effective_constraint.width,
150                    DimensionValue::Fixed(_)
151                        | DimensionValue::Fill { max: Some(_), .. }
152                        | DimensionValue::Wrap { max: Some(_), .. }
153                );
154
155            if should_use_weight_for_width {
156                measure_weighted_row(input, &args, &child_weights, &row_effective_constraint, n)
157            } else {
158                measure_unweighted_row(input, &args, &row_effective_constraint, n)
159            }
160        },
161    ));
162
163    for child_closure in child_closures {
164        child_closure();
165    }
166}
167
168fn measure_weighted_row(
169    input: &MeasureInput,
170    args: &RowArgs,
171    child_weights: &[Option<f32>],
172    row_effective_constraint: &Constraint,
173    n: usize,
174) -> Result<ComputedData, MeasurementError> {
175    // Prepare buffers and metadata for measurement:
176    // - `children_sizes` stores each child's measurement result (width, height).
177    // - `max_child_height` tracks the maximum height among children to compute the row's final height.
178    // - `available_width_for_children` is the total width available to allocate to children under the current constraint (present only for Fill/Fixed/Wrap(max)).
179    let mut children_sizes = vec![None; n];
180    let mut max_child_height = Px(0);
181    let available_width_for_children = row_effective_constraint
182        .width
183        .get_max()
184        .expect("Row width Fill expected with finite max constraint");
185
186    // Classify children into weighted and unweighted and compute the total weight.
187    let (weighted_indices, unweighted_indices, total_weight) = classify_children(child_weights);
188
189    let total_width_of_unweighted_children = measure_unweighted_children(
190        input,
191        &unweighted_indices,
192        &mut children_sizes,
193        &mut max_child_height,
194        row_effective_constraint,
195    )?;
196
197    measure_weighted_children(&mut MeasureWeightedChildrenArgs {
198        input,
199        weighted_indices: &weighted_indices,
200        children_sizes: &mut children_sizes,
201        max_child_height: &mut max_child_height,
202        remaining_width: available_width_for_children - total_width_of_unweighted_children,
203        total_weight,
204        row_effective_constraint,
205        child_weights,
206    })?;
207
208    let final_row_width = available_width_for_children;
209    let final_row_height = calculate_final_row_height(row_effective_constraint, max_child_height);
210
211    let total_measured_children_width: Px = children_sizes
212        .iter()
213        .filter_map(|s| s.map(|s| s.width))
214        .fold(Px(0), |acc, w| acc + w);
215
216    place_children_with_alignment(&PlaceChildrenArgs {
217        children_sizes: &children_sizes,
218        children_ids: input.children_ids,
219        input,
220        final_row_width,
221        final_row_height,
222        total_children_width: total_measured_children_width,
223        main_axis_alignment: args.main_axis_alignment,
224        cross_axis_alignment: args.cross_axis_alignment,
225        child_count: n,
226    });
227
228    Ok(ComputedData {
229        width: final_row_width,
230        height: final_row_height,
231    })
232}
233
234fn measure_unweighted_row(
235    input: &MeasureInput,
236    args: &RowArgs,
237    row_effective_constraint: &Constraint,
238    n: usize,
239) -> Result<ComputedData, MeasurementError> {
240    let mut children_sizes = vec![None; n];
241    let mut total_children_measured_width = Px(0);
242    let mut max_child_height = Px(0);
243
244    let parent_offered_constraint_for_child = Constraint::new(
245        match row_effective_constraint.width {
246            DimensionValue::Fixed(v) => DimensionValue::Wrap {
247                min: None,
248                max: Some(v),
249            },
250            DimensionValue::Fill { max, .. } => DimensionValue::Wrap { min: None, max },
251            DimensionValue::Wrap { max, .. } => DimensionValue::Wrap { min: None, max },
252        },
253        row_effective_constraint.height,
254    );
255
256    let children_to_measure: Vec<_> = input
257        .children_ids
258        .iter()
259        .map(|&child_id| (child_id, parent_offered_constraint_for_child))
260        .collect();
261
262    let children_results = input.measure_children(children_to_measure)?;
263
264    for (i, &child_id) in input.children_ids.iter().enumerate().take(n) {
265        if let Some(child_result) = children_results.get(&child_id) {
266            children_sizes[i] = Some(*child_result);
267            total_children_measured_width += child_result.width;
268            max_child_height = max_child_height.max(child_result.height);
269        }
270    }
271
272    let final_row_width =
273        calculate_final_row_width(row_effective_constraint, total_children_measured_width);
274    let final_row_height = calculate_final_row_height(row_effective_constraint, max_child_height);
275
276    place_children_with_alignment(&PlaceChildrenArgs {
277        children_sizes: &children_sizes,
278        children_ids: input.children_ids,
279        input,
280        final_row_width,
281        final_row_height,
282        total_children_width: total_children_measured_width,
283        main_axis_alignment: args.main_axis_alignment,
284        cross_axis_alignment: args.cross_axis_alignment,
285        child_count: n,
286    });
287
288    Ok(ComputedData {
289        width: final_row_width,
290        height: final_row_height,
291    })
292}
293
294fn classify_children(child_weights: &[Option<f32>]) -> (Vec<usize>, Vec<usize>, f32) {
295    // Split children into weighted and unweighted categories and compute the total weight of weighted children.
296    // Returns: (weighted_indices, unweighted_indices, total_weight)
297    let mut weighted_indices = Vec::new();
298    let mut unweighted_indices = Vec::new();
299    let mut total_weight = 0.0;
300
301    for (i, weight) in child_weights.iter().enumerate() {
302        if let Some(w) = weight {
303            if *w > 0.0 {
304                weighted_indices.push(i);
305                total_weight += w;
306            } else {
307                // weight == 0.0 is treated as an unweighted item (it won't participate in remaining-space allocation)
308                unweighted_indices.push(i);
309            }
310        } else {
311            unweighted_indices.push(i);
312        }
313    }
314    (weighted_indices, unweighted_indices, total_weight)
315}
316
317fn measure_unweighted_children(
318    input: &MeasureInput,
319    unweighted_indices: &[usize],
320    children_sizes: &mut [Option<ComputedData>],
321    max_child_height: &mut Px,
322    row_effective_constraint: &Constraint,
323) -> Result<Px, MeasurementError> {
324    let mut total_width = Px(0);
325
326    let parent_offered_constraint_for_child = Constraint::new(
327        DimensionValue::Wrap {
328            min: None,
329            max: row_effective_constraint.width.get_max(),
330        },
331        row_effective_constraint.height,
332    );
333
334    let children_to_measure: Vec<_> = unweighted_indices
335        .iter()
336        .map(|&child_idx| {
337            (
338                input.children_ids[child_idx],
339                parent_offered_constraint_for_child,
340            )
341        })
342        .collect();
343
344    let children_results = input.measure_children(children_to_measure)?;
345
346    for &child_idx in unweighted_indices {
347        let child_id = input.children_ids[child_idx];
348        if let Some(child_result) = children_results.get(&child_id) {
349            children_sizes[child_idx] = Some(*child_result);
350            total_width += child_result.width;
351            *max_child_height = (*max_child_height).max(child_result.height);
352        }
353    }
354
355    Ok(total_width)
356}
357
358fn measure_weighted_children(
359    args: &mut MeasureWeightedChildrenArgs,
360) -> Result<(), MeasurementError> {
361    if args.total_weight <= 0.0 {
362        return Ok(());
363    }
364
365    let children_to_measure: Vec<_> = args
366        .weighted_indices
367        .iter()
368        .map(|&child_idx| {
369            let child_weight = args.child_weights[child_idx].unwrap_or(0.0);
370            let allocated_width =
371                Px((args.remaining_width.0 as f32 * (child_weight / args.total_weight)) as i32);
372            let child_id = args.input.children_ids[child_idx];
373            let parent_offered_constraint_for_child = Constraint::new(
374                DimensionValue::Fixed(allocated_width),
375                args.row_effective_constraint.height,
376            );
377            (child_id, parent_offered_constraint_for_child)
378        })
379        .collect();
380
381    let children_results = args.input.measure_children(children_to_measure)?;
382
383    for &child_idx in args.weighted_indices {
384        let child_id = args.input.children_ids[child_idx];
385        if let Some(child_result) = children_results.get(&child_id) {
386            args.children_sizes[child_idx] = Some(*child_result);
387            *args.max_child_height = (*args.max_child_height).max(child_result.height);
388        }
389    }
390
391    Ok(())
392}
393
394fn calculate_final_row_width(
395    row_effective_constraint: &Constraint,
396    total_children_measured_width: Px,
397) -> Px {
398    // Decide the final width based on the row's width constraint type:
399    // - Fixed: use the fixed width
400    // - Fill: try to occupy the parent's available maximum width (limited by min)
401    // - Wrap: use the total width of children, limited by min/max constraints
402    match row_effective_constraint.width {
403        DimensionValue::Fixed(w) => w,
404        DimensionValue::Fill { min, max } => {
405            if let Some(max) = max {
406                let w = max;
407                if let Some(min) = min { w.max(min) } else { w }
408            } else {
409                panic!(
410                    "Seem that you are using Fill without max constraint, which is not supported in Row width."
411                );
412            }
413        }
414        DimensionValue::Wrap { min, max } => {
415            let mut w = total_children_measured_width;
416            if let Some(min_w) = min {
417                w = w.max(min_w);
418            }
419            if let Some(max_w) = max {
420                w = w.min(max_w);
421            }
422            w
423        }
424    }
425}
426
427fn calculate_final_row_height(row_effective_constraint: &Constraint, max_child_height: Px) -> Px {
428    // Calculate the final height based on the height constraint type:
429    // - Fixed: use the fixed height
430    // - Fill: use the maximum height available from the parent (limited by min)
431    // - Wrap: use the maximum child height, limited by min/max
432    match row_effective_constraint.height {
433        DimensionValue::Fixed(h) => h,
434        DimensionValue::Fill { min, max } => {
435            if let Some(max_h) = max {
436                let h = max_h;
437                if let Some(min_h) = min {
438                    h.max(min_h)
439                } else {
440                    h
441                }
442            } else {
443                panic!(
444                    "Seem that you are using Fill without max constraint, which is not supported in Row height."
445                );
446            }
447        }
448        DimensionValue::Wrap { min, max } => {
449            let mut h = max_child_height;
450            if let Some(min_h) = min {
451                h = h.max(min_h);
452            }
453            if let Some(max_h) = max {
454                h = h.min(max_h);
455            }
456            h
457        }
458    }
459}
460
461fn place_children_with_alignment(args: &PlaceChildrenArgs) {
462    // Compute the initial x and spacing between children according to the main axis (horizontal),
463    // then iterate measured children:
464    // - use calculate_cross_axis_offset to compute each child's offset on the cross axis (vertical)
465    // - place each child with place_node at the computed coordinates
466    let (mut current_x, spacing) = calculate_main_axis_layout(args);
467
468    for (i, child_size_opt) in args.children_sizes.iter().enumerate() {
469        if let Some(child_actual_size) = child_size_opt {
470            let child_id = args.children_ids[i];
471            let y_offset = calculate_cross_axis_offset(
472                child_actual_size,
473                args.final_row_height,
474                args.cross_axis_alignment,
475            );
476
477            args.input
478                .place_child(child_id, PxPosition::new(current_x, y_offset));
479            current_x += child_actual_size.width;
480            if i < args.child_count - 1 {
481                current_x += spacing;
482            }
483        }
484    }
485}
486
487fn calculate_main_axis_layout(args: &PlaceChildrenArgs) -> (Px, Px) {
488    // Calculate the start position on the main axis and the spacing between children:
489    // Returns (start_x, spacing_between_children)
490    let available_space = (args.final_row_width - args.total_children_width).max(Px(0));
491    match args.main_axis_alignment {
492        MainAxisAlignment::Start => (Px(0), Px(0)),
493        MainAxisAlignment::Center => (available_space / 2, Px(0)),
494        MainAxisAlignment::End => (available_space, Px(0)),
495        MainAxisAlignment::SpaceEvenly => calculate_space_evenly(available_space, args.child_count),
496        MainAxisAlignment::SpaceBetween => {
497            calculate_space_between(available_space, args.child_count)
498        }
499        MainAxisAlignment::SpaceAround => calculate_space_around(available_space, args.child_count),
500    }
501}
502
503fn calculate_space_evenly(available_space: Px, child_count: usize) -> (Px, Px) {
504    if child_count > 0 {
505        let s = available_space / (child_count as i32 + 1);
506        (s, s)
507    } else {
508        (Px(0), Px(0))
509    }
510}
511
512fn calculate_space_between(available_space: Px, child_count: usize) -> (Px, Px) {
513    if child_count > 1 {
514        (Px(0), available_space / (child_count as i32 - 1))
515    } else if child_count == 1 {
516        (available_space / 2, Px(0))
517    } else {
518        (Px(0), Px(0))
519    }
520}
521
522fn calculate_space_around(available_space: Px, child_count: usize) -> (Px, Px) {
523    if child_count > 0 {
524        let s = available_space / (child_count as i32);
525        (s / 2, s)
526    } else {
527        (Px(0), Px(0))
528    }
529}
530
531fn calculate_cross_axis_offset(
532    child_actual_size: &ComputedData,
533    final_row_height: Px,
534    cross_axis_alignment: CrossAxisAlignment,
535) -> Px {
536    // Compute child's offset on the cross axis (vertical):
537    // - Start: align to top (0)
538    // - Center: center (remaining_height / 2)
539    // - End: align to bottom (remaining_height)
540    // - Stretch: no offset (the child will be stretched to fill height; stretching handled in measurement)
541    match cross_axis_alignment {
542        CrossAxisAlignment::Start => Px(0),
543        CrossAxisAlignment::Center => (final_row_height - child_actual_size.height).max(Px(0)) / 2,
544        CrossAxisAlignment::End => (final_row_height - child_actual_size.height).max(Px(0)),
545        CrossAxisAlignment::Stretch => Px(0),
546    }
547}