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`](crate::alignment::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.
10//!
11//! This module also provides supporting types and a macro for ergonomic usage.
12use derive_builder::Builder;
13use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, PxPosition};
14use tessera_ui_macros::tessera;
15
16use crate::alignment::Alignment;
17
18pub use crate::boxed_ui;
19
20/// Arguments for the `Boxed` component.
21#[derive(Clone, Debug, Builder)]
22#[builder(pattern = "owned")]
23pub struct BoxedArgs {
24    /// The alignment of children within the `Boxed` container.
25    #[builder(default)]
26    pub alignment: Alignment,
27    /// Width behavior for the boxed container.
28    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
29    pub width: DimensionValue,
30    /// Height behavior for the boxed container.
31    #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
32    pub height: DimensionValue,
33}
34
35impl Default for BoxedArgs {
36    fn default() -> Self {
37        BoxedArgsBuilder::default().build().unwrap()
38    }
39}
40
41/// `BoxedItem` represents a stackable child component.
42pub struct BoxedItem {
43    pub child: Box<dyn FnOnce() + Send + Sync>,
44}
45
46impl BoxedItem {
47    pub fn new(child: Box<dyn FnOnce() + Send + Sync>) -> Self {
48        BoxedItem { child }
49    }
50}
51
52/// A trait for converting various types into a `BoxedItem`.
53pub trait AsBoxedItem {
54    fn into_boxed_item(self) -> BoxedItem;
55}
56
57impl AsBoxedItem for BoxedItem {
58    fn into_boxed_item(self) -> BoxedItem {
59        self
60    }
61}
62
63impl<F: FnOnce() + Send + Sync + 'static> AsBoxedItem for F {
64    fn into_boxed_item(self) -> BoxedItem {
65        BoxedItem {
66            child: Box::new(self),
67        }
68    }
69}
70
71/// A component that overlays its children on top of each other.
72///
73/// The `boxed` component acts as a container that stacks all its child components.
74/// The size of the container is determined by the dimensions of the largest child,
75/// and the alignment of the children within the container can be customized.
76///
77/// It's useful for creating layered UIs where components need to be placed
78/// relative to a common parent.
79///
80/// # Arguments
81///
82/// * `args`: A `BoxedArgs` struct that specifies the configuration for the container.
83///   - `alignment`: Controls how children are positioned within the box.
84///     See [`Alignment`](crate::alignment::Alignment) for available options.
85///   - `width`: The width of the container. Can be fixed, fill the parent, or wrap the content.
86///     See [`DimensionValue`](tessera_ui::DimensionValue) for details.
87///   - `height`: The height of the container. Can be fixed, fill the parent, or wrap the content.
88///     See [`DimensionValue`](tessera_ui::DimensionValue) for details.
89///
90/// * `children_items_input`: An array of child components to be rendered inside the box.
91///   Any component that implements the `AsBoxedItem` trait can be a child.
92///
93/// # Example
94///
95/// ```
96/// use tessera_ui_basic_components::boxed::{boxed, BoxedArgs};
97/// use tessera_ui_basic_components::text::text;
98///
99/// boxed(BoxedArgs::default(), [|| text("Hello".to_string())]);
100/// ```
101#[tessera(render_fn=boxed_ui)]
102pub fn boxed<const N: usize>(args: BoxedArgs, children_items_input: [impl AsBoxedItem; N]) {
103    let children_items: [BoxedItem; N] =
104        children_items_input.map(|item_input| item_input.into_boxed_item());
105
106    let mut child_closures = Vec::with_capacity(N);
107
108    for child_item in children_items {
109        child_closures.push(child_item.child);
110    }
111
112    measure(Box::new(move |input| {
113        let boxed_intrinsic_constraint = Constraint::new(args.width, args.height);
114        let effective_constraint = boxed_intrinsic_constraint.merge(input.parent_constraint);
115
116        let mut max_child_width = Px(0);
117        let mut max_child_height = Px(0);
118        let mut children_sizes = vec![None; N];
119
120        for i in 0..N {
121            let Some(child_id) = input.children_ids.get(i).copied() else {
122                continue; // Skip if no child ID is available
123            };
124            let child_result = input.measure_child(child_id, &effective_constraint)?;
125            max_child_width = max_child_width.max(child_result.width);
126            max_child_height = max_child_height.max(child_result.height);
127            children_sizes[i] = Some(child_result);
128        }
129
130        let final_width = match effective_constraint.width {
131            DimensionValue::Fixed(w) => w,
132            DimensionValue::Fill { min, max } => {
133                let mut w = max.unwrap_or(max_child_width);
134                if let Some(min_w) = min {
135                    w = w.max(min_w);
136                }
137                w
138            }
139            DimensionValue::Wrap { min, max } => {
140                let mut w = max_child_width;
141                if let Some(min_w) = min {
142                    w = w.max(min_w);
143                }
144                if let Some(max_w) = max {
145                    w = w.min(max_w);
146                }
147                w
148            }
149        };
150
151        let final_height = match effective_constraint.height {
152            DimensionValue::Fixed(h) => h,
153            DimensionValue::Fill { min, max } => {
154                let mut h = max.unwrap_or(max_child_height);
155                if let Some(min_h) = min {
156                    h = h.max(min_h);
157                }
158                h
159            }
160            DimensionValue::Wrap { min, max } => {
161                let mut h = max_child_height;
162                if let Some(min_h) = min {
163                    h = h.max(min_h);
164                }
165                if let Some(max_h) = max {
166                    h = h.min(max_h);
167                }
168                h
169            }
170        };
171
172        for (i, child_size_opt) in children_sizes.iter().enumerate() {
173            if let Some(child_size) = child_size_opt {
174                let child_id = input.children_ids[i];
175
176                let (x, y) = match args.alignment {
177                    Alignment::TopStart => (Px(0), Px(0)),
178                    Alignment::TopCenter => ((final_width - child_size.width) / 2, Px(0)),
179                    Alignment::TopEnd => (final_width - child_size.width, Px(0)),
180                    Alignment::CenterStart => (Px(0), (final_height - child_size.height) / 2),
181                    Alignment::Center => (
182                        (final_width - child_size.width) / 2,
183                        (final_height - child_size.height) / 2,
184                    ),
185                    Alignment::CenterEnd => (
186                        final_width - child_size.width,
187                        (final_height - child_size.height) / 2,
188                    ),
189                    Alignment::BottomStart => (Px(0), final_height - child_size.height),
190                    Alignment::BottomCenter => (
191                        (final_width - child_size.width) / 2,
192                        final_height - child_size.height,
193                    ),
194                    Alignment::BottomEnd => (
195                        final_width - child_size.width,
196                        final_height - child_size.height,
197                    ),
198                };
199                input.place_child(child_id, PxPosition::new(x, y));
200            }
201        }
202
203        Ok(ComputedData {
204            width: final_width,
205            height: final_height,
206        })
207    }));
208
209    for child_closure in child_closures {
210        child_closure();
211    }
212}
213
214/// A macro for simplifying `Boxed` component declarations.
215#[macro_export]
216macro_rules! boxed_ui {
217    ($args:expr $(, $child:expr)* $(,)?) => {
218        {
219            use $crate::boxed::AsBoxedItem;
220            $crate::boxed::boxed($args, [
221                $(
222                    $child.into_boxed_item()
223                ),*
224            ])
225        }
226    };
227}