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