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