tessera_ui_basic_components/
boxed.rs

1//! Provides the `Boxed` component for overlaying multiple child components in a single container.
2//!
3//! The `Boxed` module enables stacking and aligning several UI elements on top of each other,
4//! making it ideal for building layered interfaces, overlays, decorations, or custom backgrounds.
5//! Children are positioned according to the specified [`Alignment`],
6//! and the container size adapts to the largest child or can be customized via [`DimensionValue`].
7//!
8//! Typical use cases include tooltips, badges, composite controls, or any scenario where
9//! multiple widgets need to share the same space with flexible alignment.
10use derive_builder::Builder;
11use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, PxPosition, place_node, tessera};
12
13use crate::alignment::Alignment;
14
15/// Arguments for the `Boxed` component.
16#[derive(Clone, Debug, Builder)]
17#[builder(pattern = "owned")]
18pub struct BoxedArgs {
19    /// The alignment of children within the `Boxed` container.
20    #[builder(default)]
21    pub alignment: Alignment,
22    /// Width behavior for the boxed container.
23    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
24    pub width: DimensionValue,
25    /// Height behavior for the boxed container.
26    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
27    pub height: DimensionValue,
28}
29
30impl Default for BoxedArgs {
31    fn default() -> Self {
32        BoxedArgsBuilder::default().build().unwrap()
33    }
34}
35
36/// A scope for declaratively adding children to a `boxed` component.
37pub struct BoxedScope<'a> {
38    child_closures: &'a mut Vec<Box<dyn FnOnce() + Send + Sync>>,
39    child_alignments: &'a mut Vec<Option<Alignment>>,
40}
41
42impl<'a> BoxedScope<'a> {
43    /// Adds a child component to the box.
44    pub fn child<F>(&mut self, child_closure: F)
45    where
46        F: FnOnce() + Send + Sync + 'static,
47    {
48        self.child_closures.push(Box::new(child_closure));
49        self.child_alignments.push(None);
50    }
51
52    /// Adds a child component with a custom alignment overriding the container default.
53    pub fn child_with_alignment<F>(&mut self, alignment: Alignment, child_closure: F)
54    where
55        F: FnOnce() + Send + Sync + 'static,
56    {
57        self.child_closures.push(Box::new(child_closure));
58        self.child_alignments.push(Some(alignment));
59    }
60}
61
62/// Helper: resolve an effective final dimension from a DimensionValue and the largest child size.
63/// Keeps logic concise and documented in one place.
64fn resolve_final_dimension(dv: DimensionValue, largest_child: Px) -> Px {
65    match dv {
66        DimensionValue::Fixed(v) => v,
67        DimensionValue::Fill { min, max } => {
68            let mut v = max.unwrap_or(largest_child);
69            if let Some(min_v) = min {
70                v = v.max(min_v);
71            }
72            v
73        }
74        DimensionValue::Wrap { min, max } => {
75            let mut v = largest_child;
76            if let Some(min_v) = min {
77                v = v.max(min_v);
78            }
79            if let Some(max_v) = max {
80                v = v.min(max_v);
81            }
82            v
83        }
84    }
85}
86
87/// Helper: compute centered offset along one axis.
88fn center_axis(container: Px, child: Px) -> Px {
89    (container - child) / 2
90}
91
92/// Helper: compute child placement (x, y) inside the container according to alignment.
93fn compute_child_offset(
94    alignment: Alignment,
95    container_w: Px,
96    container_h: Px,
97    child_w: Px,
98    child_h: Px,
99) -> (Px, Px) {
100    match alignment {
101        Alignment::TopStart => (Px(0), Px(0)),
102        Alignment::TopCenter => (center_axis(container_w, child_w), Px(0)),
103        Alignment::TopEnd => (container_w - child_w, Px(0)),
104        Alignment::CenterStart => (Px(0), center_axis(container_h, child_h)),
105        Alignment::Center => (
106            center_axis(container_w, child_w),
107            center_axis(container_h, child_h),
108        ),
109        Alignment::CenterEnd => (container_w - child_w, center_axis(container_h, child_h)),
110        Alignment::BottomStart => (Px(0), container_h - child_h),
111        Alignment::BottomCenter => (center_axis(container_w, child_w), container_h - child_h),
112        Alignment::BottomEnd => (container_w - child_w, container_h - child_h),
113    }
114}
115
116/// A component that overlays its children on top of each other.
117///
118/// The `boxed` component acts as a container that stacks all its child components.
119/// The size of the container is determined by the dimensions of the largest child,
120/// and the alignment of the children within the container can be customized.
121///
122/// Children are added via the `scope` closure, which provides a `BoxedScope`
123/// to add children declaratively.
124#[tessera]
125pub fn boxed<F>(args: BoxedArgs, scope_config: F)
126where
127    F: FnOnce(&mut BoxedScope),
128{
129    let mut child_closures: Vec<Box<dyn FnOnce() + Send + Sync>> = Vec::new();
130    let mut child_alignments: Vec<Option<Alignment>> = Vec::new();
131
132    {
133        let mut scope = BoxedScope {
134            child_closures: &mut child_closures,
135            child_alignments: &mut child_alignments,
136        };
137        scope_config(&mut scope);
138    }
139
140    let n = child_closures.len();
141    let child_alignments = child_alignments;
142
143    // Measurement closure: measure all present children and compute container size.
144    measure(Box::new(move |input| {
145        debug_assert_eq!(
146            input.children_ids.len(),
147            n,
148            "Mismatch between children defined in scope and runtime children count"
149        );
150
151        let boxed_intrinsic_constraint = Constraint::new(args.width, args.height);
152        let effective_constraint = boxed_intrinsic_constraint.merge(input.parent_constraint);
153
154        // Track largest child sizes
155        let mut max_child_width = Px(0);
156        let mut max_child_height = Px(0);
157        let mut children_sizes = vec![None; n];
158
159        let children_to_measure: Vec<_> = input
160            .children_ids
161            .iter()
162            .map(|&child_id| (child_id, effective_constraint))
163            .collect();
164
165        let children_results = input.measure_children(children_to_measure)?;
166
167        for (i, &child_id) in input.children_ids.iter().enumerate().take(n) {
168            if let Some(child_result) = children_results.get(&child_id) {
169                max_child_width = max_child_width.max(child_result.width);
170                max_child_height = max_child_height.max(child_result.height);
171                children_sizes[i] = Some(*child_result);
172            }
173        }
174
175        // Resolve final container dimensions using helpers.
176        let final_width = resolve_final_dimension(effective_constraint.width, max_child_width);
177        let final_height = resolve_final_dimension(effective_constraint.height, max_child_height);
178
179        // Place each measured child according to alignment.
180        for (i, child_size_opt) in children_sizes.iter().enumerate() {
181            if let Some(child_size) = child_size_opt {
182                let child_id = input.children_ids[i];
183                let child_alignment = child_alignments[i].unwrap_or(args.alignment);
184                let (x, y) = compute_child_offset(
185                    child_alignment,
186                    final_width,
187                    final_height,
188                    child_size.width,
189                    child_size.height,
190                );
191                place_node(child_id, PxPosition::new(x, y), input.metadatas);
192            }
193        }
194
195        Ok(ComputedData {
196            width: final_width,
197            height: final_height,
198        })
199    }));
200
201    // Render child closures after measurement.
202    for child_closure in child_closures {
203        child_closure();
204    }
205}