tessera_ui_basic_components/
row.rs

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