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