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