tessera_ui_basic_components/
column.rs

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