tessera_ui_basic_components/
boxed.rs

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