tessera_ui_basic_components/
row.rs

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