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