tessera_ui_basic_components/
row.rs

1//! # Row Module
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//! - Horizontal arrangement of child components
9//! - Support for main/cross axis alignment and flexible sizing
10//! - Weighted children for proportional space allocation
11//! - Declarative macro [`row_ui!`] for ergonomic usage
12//!
13//! ## Typical Usage
14//! Use the [`row`] component to build horizontal layouts, optionally combining with [`column`](crate::column) for complex grid or responsive designs.
15//!
16//! See the documentation and examples for details on arguments and usage patterns.
17//!
18//! ---
19//!
20//! This module is part of the `tessera-ui-basic-components` crate.
21//!
22//! ## Example
23//! ```
24//! use tessera_ui_basic_components::{row::{row_ui, RowArgs}, text::text};
25//! row_ui!(RowArgs::default(),
26//!     || text("A".to_string()),
27//!     || text("B".to_string()),
28//! );
29//! ```
30use derive_builder::Builder;
31use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, PxPosition, place_node};
32use tessera_ui_macros::tessera;
33
34use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
35
36pub use crate::row_ui;
37
38/// Arguments for the `row` component.
39#[derive(Builder, Clone, Debug)]
40#[builder(pattern = "owned")]
41pub struct RowArgs {
42    /// Width behavior for the row.
43    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
44    pub width: DimensionValue,
45    /// Height behavior for the row.
46    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
47    pub height: DimensionValue,
48    /// Main axis alignment (horizontal alignment).
49    #[builder(default = "MainAxisAlignment::Start")]
50    pub main_axis_alignment: MainAxisAlignment,
51    /// Cross axis alignment (vertical alignment).
52    #[builder(default = "CrossAxisAlignment::Start")]
53    pub cross_axis_alignment: CrossAxisAlignment,
54}
55
56impl Default for RowArgs {
57    fn default() -> Self {
58        RowArgsBuilder::default().build().unwrap()
59    }
60}
61
62/// Represents a child item within a row layout.
63pub struct RowItem {
64    /// Optional weight for flexible space distribution
65    pub weight: Option<f32>,
66    /// The actual child component
67    pub child: Box<dyn FnOnce() + Send + Sync>,
68}
69
70impl RowItem {
71    /// Creates a new `RowItem` with optional weight.
72    pub fn new(child: Box<dyn FnOnce() + Send + Sync>, weight: Option<f32>) -> Self {
73        RowItem { weight, child }
74    }
75
76    /// Creates a weighted row item
77    pub fn weighted(child: Box<dyn FnOnce() + Send + Sync>, weight: f32) -> Self {
78        RowItem {
79            weight: Some(weight),
80            child,
81        }
82    }
83}
84
85/// Trait to allow various types to be converted into a `RowItem`.
86pub trait AsRowItem {
87    fn into_row_item(self) -> RowItem;
88}
89
90impl AsRowItem for RowItem {
91    fn into_row_item(self) -> RowItem {
92        self
93    }
94}
95
96/// Default conversion: a simple function closure becomes a `RowItem` without weight.
97impl<F: FnOnce() + Send + Sync + 'static> AsRowItem for F {
98    fn into_row_item(self) -> RowItem {
99        RowItem {
100            weight: None,
101            child: Box::new(self),
102        }
103    }
104}
105
106/// Allow (FnOnce, weight) to be a RowItem
107impl<F: FnOnce() + Send + Sync + 'static> AsRowItem for (F, f32) {
108    fn into_row_item(self) -> RowItem {
109        RowItem {
110            weight: Some(self.1),
111            child: Box::new(self.0),
112        }
113    }
114}
115
116/// A row component that arranges its children horizontally.
117/// A layout component that arranges its children horizontally.
118///
119/// The `row` component is a fundamental building block for creating horizontal layouts.
120/// It takes a set of child components and arranges them one after another in a single
121/// row. The layout behavior can be extensively customized through the `RowArgs` struct.
122///
123/// # Arguments
124///
125/// * `args`: A `RowArgs` struct that configures the layout properties of the row.
126///   - `width` and `height`: Control the dimensions of the row container. They can be
127///     set to `DimensionValue::Wrap` to fit the content, `DimensionValue::Fixed` for a
128///     specific size, or `DimensionValue::Fill` to occupy available space.
129///   - `main_axis_alignment`: Determines how children are distributed along the horizontal
130///     axis (e.g., `Start`, `Center`, `End`, `SpaceBetween`).
131///   - `cross_axis_alignment`: Determines how children are aligned along the vertical
132///     axis (e.g., `Start`, `Center`, `End`, `Stretch`).
133///
134/// * `children_items_input`: An array of child components to be displayed in the row.
135///   Children can be simple closures, or they can be wrapped in `RowItem` to provide
136///   a `weight` for flexible space distribution. Weighted children will expand to fill
137///   any remaining space in the row according to their weight proportion.
138///
139/// # Example
140///
141/// A simple row with three text components, centered horizontally and vertically.
142///
143/// ```
144/// use tessera_ui_basic_components::{row::{row_ui, RowArgs}, text::text};
145/// use tessera_ui_basic_components::alignment::{MainAxisAlignment, CrossAxisAlignment};
146/// use tessera_ui::{DimensionValue, Dp};
147///
148/// let args = RowArgs {
149///     main_axis_alignment: MainAxisAlignment::Center,
150///     cross_axis_alignment: CrossAxisAlignment::Center,
151///     width: DimensionValue::Fill { min: None, max: None },
152///     height: DimensionValue::Fixed(Dp(50.0).into()),
153/// };
154///
155/// row_ui!(args,
156///     || text("First".to_string()),
157///     || text("Second".to_string()),
158///     || text("Third".to_string()),
159/// );
160/// ```
161#[tessera]
162pub fn row<const N: usize>(args: RowArgs, children_items_input: [impl AsRowItem; N]) {
163    let children_items: [RowItem; N] =
164        children_items_input.map(|item_input| item_input.into_row_item());
165
166    let mut child_closures = Vec::with_capacity(N);
167    let mut child_weights = Vec::with_capacity(N);
168
169    for child_item in children_items {
170        child_closures.push(child_item.child);
171        child_weights.push(child_item.weight);
172    }
173
174    measure(Box::new(move |input| {
175        let row_intrinsic_constraint = Constraint::new(args.width, args.height);
176        // This is the effective constraint for the row itself
177        let row_effective_constraint = row_intrinsic_constraint.merge(input.parent_constraint);
178
179        let mut children_sizes = vec![None; N];
180        let mut max_child_height = Px(0);
181
182        // For row, main axis is horizontal, so check width for weight distribution
183        let should_use_weight_for_width = match row_effective_constraint.width {
184            DimensionValue::Fixed(_) => true,
185            DimensionValue::Fill { max: Some(_), .. } => true,
186            DimensionValue::Wrap { max: Some(_), .. } => true,
187            _ => false,
188        };
189
190        if should_use_weight_for_width {
191            let available_width_for_children = row_effective_constraint.width.get_max().unwrap();
192
193            let mut weighted_children_indices = Vec::new();
194            let mut unweighted_children_indices = Vec::new();
195            let mut total_weight_sum = 0.0f32;
196
197            for (i, weight_opt) in child_weights.iter().enumerate() {
198                if let Some(w) = weight_opt {
199                    if *w > 0.0 {
200                        weighted_children_indices.push(i);
201                        total_weight_sum += w;
202                    } else {
203                        unweighted_children_indices.push(i);
204                    }
205                } else {
206                    unweighted_children_indices.push(i);
207                }
208            }
209
210            let mut total_width_of_unweighted_children = Px(0);
211            for &child_idx in &unweighted_children_indices {
212                let child_id = input.children_ids[child_idx];
213
214                // Parent (row) offers Wrap for width and its own effective height constraint to unweighted children
215                let parent_offered_constraint_for_child = Constraint::new(
216                    DimensionValue::Wrap {
217                        min: None,
218                        max: row_effective_constraint.width.get_max(),
219                    },
220                    row_effective_constraint.height,
221                );
222
223                // measure_node will fetch the child's intrinsic constraint and merge it
224                let child_result =
225                    input.measure_child(child_id, &parent_offered_constraint_for_child)?;
226
227                children_sizes[child_idx] = Some(child_result);
228                total_width_of_unweighted_children += child_result.width;
229                max_child_height = max_child_height.max(child_result.height);
230            }
231
232            let remaining_width_for_weighted_children =
233                (available_width_for_children - total_width_of_unweighted_children).max(Px(0));
234            if total_weight_sum > 0.0 {
235                for &child_idx in &weighted_children_indices {
236                    let child_weight = child_weights[child_idx].unwrap_or(0.0);
237                    let allocated_width_for_child =
238                        Px((remaining_width_for_weighted_children.0 as f32
239                            * (child_weight / total_weight_sum)) as i32);
240                    let child_id = input.children_ids[child_idx];
241
242                    // Parent (row) offers Fixed allocated width and its own effective height constraint to weighted children
243                    let parent_offered_constraint_for_child = Constraint::new(
244                        DimensionValue::Fixed(allocated_width_for_child),
245                        row_effective_constraint.height,
246                    );
247
248                    // measure_node will fetch the child's intrinsic constraint and merge it
249                    let child_result =
250                        input.measure_child(child_id, &parent_offered_constraint_for_child)?;
251
252                    children_sizes[child_idx] = Some(child_result);
253                    max_child_height = max_child_height.max(child_result.height);
254                }
255            }
256
257            let final_row_width = available_width_for_children;
258            // row's height is determined by its own effective constraint, or by wrapping content if no explicit max.
259            let final_row_height = match row_effective_constraint.height {
260                DimensionValue::Fixed(h) => h,
261                DimensionValue::Fill { max: Some(h), .. } => h,
262                DimensionValue::Wrap { min, max } => {
263                    let mut h = max_child_height;
264                    if let Some(min_h) = min {
265                        h = h.max(min_h);
266                    }
267                    if let Some(max_h) = max {
268                        h = h.min(max_h);
269                    }
270                    h
271                }
272                _ => max_child_height, // Fill { max: None } or Wrap { max: None } -> wraps content
273            };
274
275            let total_measured_children_width: Px = children_sizes
276                .iter()
277                .filter_map(|size_opt| size_opt.as_ref().map(|s| s.width))
278                .fold(Px(0), |acc, width| acc + width);
279
280            place_children_with_alignment(
281                &children_sizes,
282                input.children_ids,
283                input.metadatas,
284                final_row_width,
285                final_row_height,
286                total_measured_children_width,
287                args.main_axis_alignment,
288                args.cross_axis_alignment,
289                N,
290            );
291
292            Ok(ComputedData {
293                width: final_row_width,
294                height: final_row_height,
295            })
296        } else {
297            // Not using weight logic for width (row width is Wrap or Fill without max)
298            let mut total_children_measured_width = Px(0);
299
300            for i in 0..N {
301                let child_id = input.children_ids[i];
302
303                // Parent (row) offers Wrap for width and its effective height
304                let parent_offered_constraint_for_child = Constraint::new(
305                    match row_effective_constraint.width {
306                        DimensionValue::Fixed(v) => DimensionValue::Wrap {
307                            min: None,
308                            max: Some(v),
309                        },
310                        DimensionValue::Fill { max, .. } => DimensionValue::Wrap { min: None, max },
311                        DimensionValue::Wrap { max, .. } => DimensionValue::Wrap { min: None, max },
312                    },
313                    row_effective_constraint.height,
314                );
315
316                // measure_node will fetch the child's intrinsic constraint and merge it
317                let child_result =
318                    input.measure_child(child_id, &parent_offered_constraint_for_child)?;
319
320                children_sizes[i] = Some(child_result);
321                total_children_measured_width += child_result.width;
322                max_child_height = max_child_height.max(child_result.height);
323            }
324
325            // Determine row's final size based on its own constraints and content
326            let final_row_width = match row_effective_constraint.width {
327                DimensionValue::Fixed(w) => w,
328                DimensionValue::Fill { min, .. } => {
329                    // Max is None if here. In this case, Fill should take up all available space
330                    // from the parent, not wrap the content.
331                    let mut w = input
332                        .parent_constraint
333                        .width
334                        .get_max()
335                        .unwrap_or(total_children_measured_width);
336                    if let Some(min_w) = min {
337                        w = w.max(min_w);
338                    }
339                    w
340                }
341                DimensionValue::Wrap { min, max } => {
342                    let mut w = total_children_measured_width;
343                    if let Some(min_w) = min {
344                        w = w.max(min_w);
345                    }
346                    if let Some(max_w) = max {
347                        w = w.min(max_w);
348                    }
349                    w
350                }
351            };
352
353            let final_row_height = match row_effective_constraint.height {
354                DimensionValue::Fixed(h) => h,
355                DimensionValue::Fill { min, max } => {
356                    let mut h = max_child_height;
357                    if let Some(min_h) = min {
358                        h = h.max(min_h);
359                    }
360                    if let Some(max_h) = max {
361                        h = h.min(max_h);
362                    } else {
363                        h = max_child_height;
364                    }
365                    h
366                }
367                DimensionValue::Wrap { min, max } => {
368                    let mut h = max_child_height;
369                    if let Some(min_h) = min {
370                        h = h.max(min_h);
371                    }
372                    if let Some(max_h) = max {
373                        h = h.min(max_h);
374                    }
375                    h
376                }
377            };
378
379            place_children_with_alignment(
380                &children_sizes,
381                input.children_ids,
382                input.metadatas,
383                final_row_width,
384                final_row_height,
385                total_children_measured_width,
386                args.main_axis_alignment,
387                args.cross_axis_alignment,
388                N,
389            );
390
391            Ok(ComputedData {
392                width: final_row_width,
393                height: final_row_height,
394            })
395        }
396    }));
397
398    for child_closure in child_closures {
399        child_closure();
400    }
401}
402
403/// A helper function to place children with alignment (horizontal layout).
404fn place_children_with_alignment(
405    children_sizes: &[Option<ComputedData>],
406    children_ids: &[tessera_ui::NodeId],
407    metadatas: &tessera_ui::ComponentNodeMetaDatas,
408    final_row_width: Px,
409    final_row_height: Px,
410    total_children_width: Px,
411    main_axis_alignment: MainAxisAlignment,
412    cross_axis_alignment: CrossAxisAlignment,
413    child_count: usize,
414) {
415    let available_space = (final_row_width - total_children_width).max(Px(0));
416
417    // Calculate start position and spacing on the main axis (horizontal for row)
418    let (mut current_x, spacing_between_children) = match main_axis_alignment {
419        MainAxisAlignment::Start => (Px(0), Px(0)),
420        MainAxisAlignment::Center => (available_space / 2, Px(0)),
421        MainAxisAlignment::End => (available_space, Px(0)),
422        MainAxisAlignment::SpaceEvenly => {
423            if child_count > 0 {
424                let s = available_space / (child_count as i32 + 1);
425                (s, s)
426            } else {
427                (Px(0), Px(0))
428            }
429        }
430        MainAxisAlignment::SpaceBetween => {
431            if child_count > 1 {
432                (Px(0), available_space / (child_count as i32 - 1))
433            } else if child_count == 1 {
434                (available_space / 2, Px(0))
435            } else {
436                (Px(0), Px(0))
437            }
438        }
439        MainAxisAlignment::SpaceAround => {
440            if child_count > 0 {
441                let s = available_space / (child_count as i32);
442                (s / 2, s)
443            } else {
444                (Px(0), Px(0))
445            }
446        }
447    };
448
449    for (i, child_size_opt) in children_sizes.iter().enumerate() {
450        if let Some(child_actual_size) = child_size_opt {
451            let child_id = children_ids[i];
452
453            // Calculate position on the cross axis (vertical for row)
454            let y_offset = match cross_axis_alignment {
455                CrossAxisAlignment::Start => Px(0),
456                CrossAxisAlignment::Center => {
457                    (final_row_height - child_actual_size.height).max(Px(0)) / 2
458                }
459                CrossAxisAlignment::End => (final_row_height - child_actual_size.height).max(Px(0)),
460                CrossAxisAlignment::Stretch => Px(0),
461            };
462
463            place_node(child_id, PxPosition::new(current_x, y_offset), metadatas);
464            current_x += child_actual_size.width;
465            if i < child_count - 1 {
466                current_x += spacing_between_children;
467            }
468        }
469    }
470}
471
472/// A declarative macro to simplify the creation of a [`row`](crate::row::row) component.
473///
474/// The first argument is the `RowArgs` struct, followed by a variable number of
475/// child components. Each child expression will be converted to a `RowItem`
476/// using the `AsRowItem` trait. This allows passing closures, `RowItem` instances,
477/// or `(FnOnce, weight)` tuples.
478///
479/// # Example
480///
481/// ```
482/// use tessera_ui_basic_components::{row::{row_ui, RowArgs, RowItem}, text::text};
483///
484/// row_ui![
485///     RowArgs::default(),
486///     || text("Hello".to_string()), // Closure
487///     (|| text("Weighted".to_string()), 0.5), // Weighted closure
488///     RowItem::new(Box::new(|| text("Item".to_string())), None) // RowItem instance
489/// ];
490/// ```
491#[macro_export]
492macro_rules! row_ui {
493    ($args:expr $(, $child:expr)* $(,)?) => {
494        {
495            use $crate::row::AsRowItem;
496            $crate::row::row($args, [
497                $(
498                    $child.into_row_item()
499                ),*
500            ])
501        }
502    };
503}