tessera_ui_basic_components/
column.rs

1//! A vertical layout component.
2//!
3//! ## Usage
4//!
5//! Use to stack children vertically.
6use derive_builder::Builder;
7use tessera_ui::{
8    ComponentNodeMetaDatas, ComputedData, Constraint, DimensionValue, MeasureInput,
9    MeasurementError, NodeId, Px, PxPosition, place_node, tessera,
10};
11
12use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
13
14/// Arguments for the `column` component.
15#[derive(Builder, Clone, Debug)]
16#[builder(pattern = "owned")]
17pub struct ColumnArgs {
18    /// Width behavior for the column.
19    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
20    pub width: DimensionValue,
21    /// Height behavior for the column.
22    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
23    pub height: DimensionValue,
24    /// Main axis alignment (vertical alignment).
25    #[builder(default = "MainAxisAlignment::Start")]
26    pub main_axis_alignment: MainAxisAlignment,
27    /// Cross axis alignment (horizontal alignment).
28    #[builder(default = "CrossAxisAlignment::Start")]
29    pub cross_axis_alignment: CrossAxisAlignment,
30}
31
32impl Default for ColumnArgs {
33    fn default() -> Self {
34        ColumnArgsBuilder::default().build().unwrap()
35    }
36}
37
38/// A scope for declaratively adding children to a `column` component.
39pub struct ColumnScope<'a> {
40    child_closures: &'a mut Vec<Box<dyn FnOnce() + Send + Sync>>,
41    child_weights: &'a mut Vec<Option<f32>>,
42}
43
44impl<'a> ColumnScope<'a> {
45    /// Adds a child component to the column.
46    pub fn child<F>(&mut self, child_closure: F)
47    where
48        F: FnOnce() + Send + Sync + 'static,
49    {
50        self.child_closures.push(Box::new(child_closure));
51        self.child_weights.push(None);
52    }
53
54    /// Adds a child component to the column with a specified weight for flexible space distribution.
55    pub fn child_weighted<F>(&mut self, child_closure: F, weight: f32)
56    where
57        F: FnOnce() + Send + Sync + 'static,
58    {
59        self.child_closures.push(Box::new(child_closure));
60        self.child_weights.push(Some(weight));
61    }
62}
63
64/// # column
65///
66/// A layout component that arranges its children in a vertical column.
67///
68/// ## Usage
69///
70/// Stack components vertically, with options for alignment and flexible spacing.
71///
72/// ## Parameters
73///
74/// - `args` — configures the column's dimensions and alignment; see [`ColumnArgs`].
75/// - `scope_config` — a closure that receives a [`ColumnScope`] for adding children.
76///
77/// ## Examples
78///
79/// ```
80/// use tessera_ui_basic_components::column::{column, ColumnArgs};
81/// use tessera_ui_basic_components::text::{text, TextArgsBuilder};
82/// use tessera_ui_basic_components::spacer::{spacer, SpacerArgs};
83///
84/// column(ColumnArgs::default(), |scope| {
85///     scope.child(|| text(TextArgsBuilder::default().text("First item".to_string()).build().unwrap()));
86///     scope.child_weighted(|| spacer(SpacerArgs::default()), 1.0); // This spacer will be flexible
87///     scope.child(|| text(TextArgsBuilder::default().text("Last item".to_string()).build().unwrap()));
88/// });
89/// ```
90#[tessera]
91pub fn column<F>(args: ColumnArgs, scope_config: F)
92where
93    F: FnOnce(&mut ColumnScope),
94{
95    let mut child_closures: Vec<Box<dyn FnOnce() + Send + Sync>> = Vec::new();
96    let mut child_weights: Vec<Option<f32>> = Vec::new();
97
98    {
99        let mut scope = ColumnScope {
100            child_closures: &mut child_closures,
101            child_weights: &mut child_weights,
102        };
103        scope_config(&mut scope);
104    }
105
106    let n = child_closures.len();
107
108    measure(Box::new(
109        move |input| -> Result<ComputedData, MeasurementError> {
110            assert_eq!(
111                input.children_ids.len(),
112                n,
113                "Mismatch between children defined in scope and runtime children count"
114            );
115
116            let column_intrinsic_constraint = Constraint::new(args.width, args.height);
117            let column_effective_constraint =
118                column_intrinsic_constraint.merge(input.parent_constraint);
119
120            let mut children_sizes = vec![None; n];
121            let mut max_child_width = Px(0);
122
123            let should_use_weight_for_height = matches!(
124                column_effective_constraint.height,
125                DimensionValue::Fixed(_)
126                    | DimensionValue::Fill { max: Some(_), .. }
127                    | DimensionValue::Wrap { max: Some(_), .. }
128            );
129
130            let (final_column_width, final_column_height, total_measured_children_height) =
131                if should_use_weight_for_height {
132                    measure_weighted_column(
133                        input,
134                        &args,
135                        &child_weights,
136                        &column_effective_constraint,
137                        &mut children_sizes,
138                        &mut max_child_width,
139                    )?
140                } else {
141                    measure_unweighted_column(
142                        input,
143                        &args,
144                        &column_effective_constraint,
145                        &mut children_sizes,
146                        &mut max_child_width,
147                    )?
148                };
149
150            place_children_with_alignment(&PlaceChildrenArgs {
151                children_sizes: &children_sizes,
152                children_ids: input.children_ids,
153                metadatas: input.metadatas,
154                final_column_width,
155                final_column_height,
156                total_children_height: total_measured_children_height,
157                main_axis_alignment: args.main_axis_alignment,
158                cross_axis_alignment: args.cross_axis_alignment,
159                child_count: n,
160            });
161
162            Ok(ComputedData {
163                width: final_column_width,
164                height: final_column_height,
165            })
166        },
167    ));
168
169    for child_closure in child_closures {
170        child_closure();
171    }
172}
173
174/// Helper struct used to place children with alignment. Local to this module.
175struct PlaceChildrenArgs<'a> {
176    children_sizes: &'a [Option<ComputedData>],
177    children_ids: &'a [NodeId],
178    metadatas: &'a ComponentNodeMetaDatas,
179    final_column_width: Px,
180    final_column_height: Px,
181    total_children_height: Px,
182    main_axis_alignment: MainAxisAlignment,
183    cross_axis_alignment: CrossAxisAlignment,
184    child_count: usize,
185}
186
187/// Helper: classify children into weighted / unweighted and compute total weight.
188fn classify_children(child_weights: &[Option<f32>]) -> (Vec<usize>, Vec<usize>, f32) {
189    let mut weighted_indices = Vec::new();
190    let mut unweighted_indices = Vec::new();
191    let mut total_weight = 0.0;
192    for (i, weight_opt) in child_weights.iter().enumerate() {
193        if let Some(w) = weight_opt {
194            if *w > 0.0 {
195                weighted_indices.push(i);
196                total_weight += w;
197            } else {
198                unweighted_indices.push(i);
199            }
200        } else {
201            unweighted_indices.push(i);
202        }
203    }
204    (weighted_indices, unweighted_indices, total_weight)
205}
206
207/// Measure all non-weighted children (vertical variant).
208/// Returns the accumulated total height of those children.
209fn measure_unweighted_children_for_column(
210    input: &MeasureInput,
211    indices: &[usize],
212    children_sizes: &mut [Option<ComputedData>],
213    max_child_width: &mut Px,
214    column_effective_constraint: &Constraint,
215) -> Result<Px, MeasurementError> {
216    let mut total = Px(0);
217
218    let parent_offered_constraint_for_child = Constraint::new(
219        column_effective_constraint.width,
220        DimensionValue::Wrap {
221            min: None,
222            max: column_effective_constraint.height.get_max(),
223        },
224    );
225
226    let children_to_measure: Vec<_> = indices
227        .iter()
228        .map(|&child_idx| {
229            (
230                input.children_ids[child_idx],
231                parent_offered_constraint_for_child,
232            )
233        })
234        .collect();
235
236    let children_results = input.measure_children(children_to_measure)?;
237
238    for &child_idx in indices {
239        let child_id = input.children_ids[child_idx];
240        if let Some(child_result) = children_results.get(&child_id) {
241            children_sizes[child_idx] = Some(*child_result);
242            total += child_result.height;
243            *max_child_width = (*max_child_width).max(child_result.width);
244        }
245    }
246
247    Ok(total)
248}
249
250/// Measure weighted children by distributing the remaining height proportionally.
251fn measure_weighted_children_for_column(
252    input: &MeasureInput,
253    weighted_indices: &[usize],
254    children_sizes: &mut [Option<ComputedData>],
255    max_child_width: &mut Px,
256    remaining_height: Px,
257    total_weight: f32,
258    column_effective_constraint: &Constraint,
259    child_weights: &[Option<f32>],
260) -> Result<(), MeasurementError> {
261    if total_weight <= 0.0 {
262        return Ok(());
263    }
264
265    let children_to_measure: Vec<_> = weighted_indices
266        .iter()
267        .map(|&child_idx| {
268            let child_weight = child_weights[child_idx].unwrap_or(0.0);
269            let allocated_height =
270                Px((remaining_height.0 as f32 * (child_weight / total_weight)) as i32);
271            let child_id = input.children_ids[child_idx];
272            let parent_offered_constraint_for_child = Constraint::new(
273                column_effective_constraint.width,
274                DimensionValue::Fixed(allocated_height),
275            );
276            (child_id, parent_offered_constraint_for_child)
277        })
278        .collect();
279
280    let children_results = input.measure_children(children_to_measure)?;
281
282    for &child_idx in weighted_indices {
283        let child_id = input.children_ids[child_idx];
284        if let Some(child_result) = children_results.get(&child_id) {
285            children_sizes[child_idx] = Some(*child_result);
286            *max_child_width = (*max_child_width).max(child_result.width);
287        }
288    }
289
290    Ok(())
291}
292
293fn calculate_final_column_height(
294    column_effective_constraint: &Constraint,
295    measured_children_height: Px,
296) -> Px {
297    match column_effective_constraint.height {
298        DimensionValue::Fixed(h) => h,
299        DimensionValue::Fill { min, max } => {
300            if let Some(max) = max {
301                if let Some(min) = min {
302                    max.max(min)
303                } else {
304                    max
305                }
306            } else {
307                panic!(
308                    "Seems that you are trying to use Fill without max in a non-infinite parent constraint. This is not supported. Parent constraint: {column_effective_constraint:?}"
309                );
310            }
311        }
312        DimensionValue::Wrap { min, max } => {
313            let mut h = measured_children_height;
314            if let Some(min_h) = min {
315                h = h.max(min_h);
316            }
317            if let Some(max_h) = max {
318                h = h.min(max_h);
319            }
320            h
321        }
322    }
323}
324
325fn calculate_final_column_width(
326    column_effective_constraint: &Constraint,
327    max_child_width: Px,
328    parent_constraint: &Constraint,
329) -> Px {
330    match column_effective_constraint.width {
331        DimensionValue::Fixed(w) => w,
332        DimensionValue::Fill { min, max } => {
333            if let Some(max) = max {
334                if let Some(min) = min {
335                    max.max(min)
336                } else {
337                    max
338                }
339            } else {
340                panic!(
341                    "Seems that you are trying to use Fill without max in a non-infinite parent constraint. This is not supported. Parent constraint: {parent_constraint:?}"
342                );
343            }
344        }
345        DimensionValue::Wrap { min, max } => {
346            let mut w = max_child_width;
347            if let Some(min_w) = min {
348                w = w.max(min_w);
349            }
350            if let Some(max_w) = max {
351                w = w.min(max_w);
352            }
353            w
354        }
355    }
356}
357
358/// Measure column when height uses weighted allocation.
359/// Returns (final_width, final_height, total_measured_children_height)
360fn measure_weighted_column(
361    input: &MeasureInput,
362    _args: &ColumnArgs,
363    child_weights: &[Option<f32>],
364    column_effective_constraint: &Constraint,
365    children_sizes: &mut [Option<ComputedData>],
366    max_child_width: &mut Px,
367) -> Result<(Px, Px, Px), MeasurementError> {
368    let available_height_for_children = column_effective_constraint.height.get_max().unwrap();
369
370    let (weighted_children_indices, unweighted_children_indices, total_weight_sum) =
371        classify_children(child_weights);
372
373    let total_height_of_unweighted_children = measure_unweighted_children_for_column(
374        input,
375        &unweighted_children_indices,
376        children_sizes,
377        max_child_width,
378        column_effective_constraint,
379    )?;
380
381    let remaining_height_for_weighted_children =
382        (available_height_for_children - total_height_of_unweighted_children).max(Px(0));
383
384    measure_weighted_children_for_column(
385        input,
386        &weighted_children_indices,
387        children_sizes,
388        max_child_width,
389        remaining_height_for_weighted_children,
390        total_weight_sum,
391        column_effective_constraint,
392        child_weights,
393    )?;
394
395    let total_measured_children_height: Px = children_sizes
396        .iter()
397        .filter_map(|s| s.as_ref().map(|s| s.height))
398        .fold(Px(0), |acc, h| acc + h);
399
400    let final_column_height =
401        calculate_final_column_height(column_effective_constraint, total_measured_children_height);
402    let final_column_width = calculate_final_column_width(
403        column_effective_constraint,
404        *max_child_width,
405        input.parent_constraint,
406    );
407
408    Ok((
409        final_column_width,
410        final_column_height,
411        total_measured_children_height,
412    ))
413}
414
415fn measure_unweighted_column(
416    input: &MeasureInput,
417    _args: &ColumnArgs,
418    column_effective_constraint: &Constraint,
419    children_sizes: &mut [Option<ComputedData>],
420    max_child_width: &mut Px,
421) -> Result<(Px, Px, Px), MeasurementError> {
422    let n = children_sizes.len();
423    let mut total_children_measured_height = Px(0);
424
425    let parent_offered_constraint_for_child = Constraint::new(
426        column_effective_constraint.width,
427        DimensionValue::Wrap {
428            min: None,
429            max: column_effective_constraint.height.get_max(),
430        },
431    );
432
433    let children_to_measure: Vec<_> = input
434        .children_ids
435        .iter()
436        .map(|&child_id| (child_id, parent_offered_constraint_for_child))
437        .collect();
438
439    let children_results = input.measure_children(children_to_measure)?;
440
441    for (i, &child_id) in input.children_ids.iter().enumerate().take(n) {
442        if let Some(child_result) = children_results.get(&child_id) {
443            children_sizes[i] = Some(*child_result);
444            total_children_measured_height += child_result.height;
445            *max_child_width = (*max_child_width).max(child_result.width);
446        }
447    }
448
449    let final_column_height =
450        calculate_final_column_height(column_effective_constraint, total_children_measured_height);
451    let final_column_width = calculate_final_column_width(
452        column_effective_constraint,
453        *max_child_width,
454        input.parent_constraint,
455    );
456    Ok((
457        final_column_width,
458        final_column_height,
459        total_children_measured_height,
460    ))
461}
462
463/// Place measured children into the column according to main and cross axis alignment.
464///
465/// This helper computes the starting y position and spacing between children based on
466/// `MainAxisAlignment` variants (Start, Center, End, SpaceEvenly, SpaceBetween, SpaceAround)
467/// and aligns each child horizontally using `CrossAxisAlignment`. It calls `place_node` to
468/// record each child's layout position.
469///
470/// `args` contains measured child sizes, node ids, component metadata and final column size.
471fn place_children_with_alignment(args: &PlaceChildrenArgs) {
472    let (mut current_y, spacing_between_children) = calculate_main_axis_layout_for_column(
473        args.final_column_height,
474        args.total_children_height,
475        args.main_axis_alignment,
476        args.child_count,
477    );
478
479    for (i, child_size_opt) in args.children_sizes.iter().enumerate() {
480        if let Some(child_actual_size) = child_size_opt {
481            let child_id = args.children_ids[i];
482            let x_offset = calculate_cross_axis_offset_for_column(
483                child_actual_size,
484                args.final_column_width,
485                args.cross_axis_alignment,
486            );
487            place_node(
488                child_id,
489                PxPosition::new(x_offset, current_y),
490                args.metadatas,
491            );
492            current_y += child_actual_size.height;
493            if i < args.child_count - 1 {
494                current_y += spacing_between_children;
495            }
496        }
497    }
498}
499
500fn calculate_main_axis_layout_for_column(
501    final_column_height: Px,
502    total_children_height: Px,
503    main_axis_alignment: MainAxisAlignment,
504    child_count: usize,
505) -> (Px, Px) {
506    let available_space = (final_column_height - total_children_height).max(Px(0));
507    match main_axis_alignment {
508        MainAxisAlignment::Start => (Px(0), Px(0)),
509        MainAxisAlignment::Center => (available_space / 2, Px(0)),
510        MainAxisAlignment::End => (available_space, Px(0)),
511        MainAxisAlignment::SpaceEvenly => {
512            if child_count > 0 {
513                let s = available_space / (child_count as i32 + 1);
514                (s, s)
515            } else {
516                (Px(0), Px(0))
517            }
518        }
519        MainAxisAlignment::SpaceBetween => {
520            if child_count > 1 {
521                (Px(0), available_space / (child_count as i32 - 1))
522            } else if child_count == 1 {
523                (available_space / 2, Px(0))
524            } else {
525                (Px(0), Px(0))
526            }
527        }
528        MainAxisAlignment::SpaceAround => {
529            if child_count > 0 {
530                let s = available_space / (child_count as i32);
531                (s / 2, s)
532            } else {
533                (Px(0), Px(0))
534            }
535        }
536    }
537}
538
539fn calculate_cross_axis_offset_for_column(
540    child_actual_size: &ComputedData,
541    final_column_width: Px,
542    cross_axis_alignment: CrossAxisAlignment,
543) -> Px {
544    match cross_axis_alignment {
545        CrossAxisAlignment::Start => Px(0),
546        CrossAxisAlignment::Center => (final_column_width - child_actual_size.width).max(Px(0)) / 2,
547        CrossAxisAlignment::End => (final_column_width - child_actual_size.width).max(Px(0)),
548        CrossAxisAlignment::Stretch => Px(0),
549    }
550}