tessera_ui_basic_components/
spacer.rs

1//! Spacer component for Tessera UI.
2//!
3//! This module provides the [`spacer`] component and its configuration struct [`SpacerArgs`].
4//! A spacer is an invisible, non-interactive UI element used to insert empty space between other components
5//! or to create flexible layouts where certain regions expand to fill available space.
6//!
7//! Typical use cases include aligning content within rows or columns, distributing space between widgets,
8//! or enforcing minimum gaps in layouts. The sizing behavior is controlled via [`DimensionValue`], allowing
9//! both fixed-size and flexible (fill) spacers. This is essential for building responsive and adaptive UIs.
10//!
11//! # Examples
12//! - Add a fixed gap between two buttons in a row.
13//! - Use a fill spacer to push content to the edges of a container.
14//! - Combine multiple spacers for complex layout arrangements.
15//!
16//! See [`SpacerArgs`] and [`spacer`] for usage details.
17
18use derive_builder::Builder;
19use tessera_ui::{ComputedData, Constraint, DimensionValue, Dp, Px, tessera};
20///
21/// Arguments for configuring the [`spacer`] component.
22///
23/// `SpacerArgs` allows you to specify the width and height behavior of a spacer in a layout.
24/// By default, both width and height are fixed to zero pixels. To create a flexible spacer
25/// that expands to fill available space, use [`DimensionValue::Fill`] for the desired axis.
26///
27/// # Example
28/// ```
29/// use tessera_ui_basic_components::spacer::{spacer, SpacerArgs};
30/// use tessera_ui::{DimensionValue, Px};
31///
32/// // Fixed-size spacer (default)
33/// spacer(SpacerArgs::default());
34///
35/// // Expanding spacer (fills available width)
36/// spacer(SpacerArgs {
37///     width: DimensionValue::Fill { min: None, max: None },
38///     height: DimensionValue::Fixed(Px(0)),
39/// });
40/// ```
41#[derive(Default, Clone, Copy, Builder)]
42#[builder(pattern = "owned")]
43pub struct SpacerArgs {
44    /// The desired width behavior of the spacer.
45    ///
46    /// Defaults to `Fixed(Px(0))`.
47    #[builder(default = "DimensionValue::Fixed(Px(0))", setter(into))]
48    pub width: DimensionValue,
49    /// The desired height behavior of the spacer.
50    ///
51    /// Defaults to `Fixed(Px(0))`.
52    #[builder(default = "DimensionValue::Fixed(Px(0))", setter(into))]
53    pub height: DimensionValue,
54}
55
56impl SpacerArgs {
57    /// Creates a spacer that tries to fill available space in both width and height.
58    ///
59    /// # Example
60    /// ```
61    /// use tessera_ui_basic_components::spacer::SpacerArgs;
62    /// let args = SpacerArgs::fill_both();
63    /// ```
64    pub fn fill_both() -> Self {
65        SpacerArgsBuilder::default()
66            .width(DimensionValue::Fill {
67                min: None,
68                max: None,
69            })
70            .height(DimensionValue::Fill {
71                min: None,
72                max: None,
73            })
74            .build()
75            .unwrap() // build() should not fail with these defaults
76    }
77
78    /// Creates a spacer that tries to fill available width.
79    ///
80    /// # Example
81    /// ```
82    /// use tessera_ui_basic_components::spacer::SpacerArgs;
83    /// let args = SpacerArgs::fill_width();
84    /// ```
85    pub fn fill_width() -> Self {
86        SpacerArgsBuilder::default()
87            .width(DimensionValue::Fill {
88                min: None,
89                max: None,
90            })
91            .height(DimensionValue::Fixed(Px(0))) // Default height if only filling width
92            .build()
93            .unwrap()
94    }
95
96    /// Creates a spacer that tries to fill available height.
97    ///
98    /// # Example
99    /// ```
100    /// use tessera_ui_basic_components::spacer::SpacerArgs;
101    /// let args = SpacerArgs::fill_height();
102    /// ```
103    pub fn fill_height() -> Self {
104        SpacerArgsBuilder::default()
105            .width(DimensionValue::Fixed(Px(0))) // Default width if only filling height
106            .height(DimensionValue::Fill {
107                min: None,
108                max: None,
109            })
110            .build()
111            .unwrap()
112    }
113}
114
115impl From<Dp> for SpacerArgs {
116    /// Creates a fixed-size spacer from a [`Dp`] value for both width and height.
117    ///
118    /// # Example
119    /// ```
120    /// use tessera_ui_basic_components::spacer::SpacerArgs;
121    /// use tessera_ui::Dp;
122    /// let args = SpacerArgs::from(Dp(8.0));
123    /// ```
124    fn from(value: Dp) -> Self {
125        SpacerArgsBuilder::default()
126            .width(DimensionValue::Fixed(value.to_px()))
127            .height(DimensionValue::Fixed(value.to_px()))
128            .build()
129            .unwrap()
130    }
131}
132
133impl From<Px> for SpacerArgs {
134    /// Creates a fixed-size spacer from a [`Px`] value for both width and height.
135    ///
136    /// # Example
137    /// ```
138    /// use tessera_ui_basic_components::spacer::SpacerArgs;
139    /// use tessera_ui::Px;
140    /// let args = SpacerArgs::from(Px(16));
141    /// ```
142    fn from(value: Px) -> Self {
143        SpacerArgsBuilder::default()
144            .width(DimensionValue::Fixed(value))
145            .height(DimensionValue::Fixed(value))
146            .build()
147            .unwrap()
148    }
149}
150
151///
152/// A component that inserts an empty, flexible space into a layout.
153///
154/// The `spacer` component is commonly used to add gaps between other UI elements,
155/// or to create flexible layouts where certain areas expand to fill available space.
156/// The behavior of the spacer is controlled by the [`SpacerArgs`] parameter, which
157/// allows you to specify fixed or flexible sizing for width and height using [`DimensionValue`].
158///
159/// - Use `DimensionValue::Fixed` for a fixed-size spacer.
160/// - Use `DimensionValue::Fill` to make the spacer expand to fill available space in its parent container.
161///
162/// # Example
163/// ```
164/// use tessera_ui_basic_components::{
165///     row::{row, RowArgs},
166///     spacer::{spacer, SpacerArgs},
167///     text::text,
168/// };
169///
170/// row(
171///     RowArgs::default(),
172///     |scope| {
173///         scope.child(|| text("Left".to_string()));
174///         // This spacer will fill the available width, pushing "Right" to the end.
175///         scope.child(|| spacer(SpacerArgs::fill_width()));
176///         scope.child(|| text("Right".to_string()));
177///     }
178/// );
179/// ```
180///
181/// You can also use [`SpacerArgs::fill_both`] or [`SpacerArgs::fill_height`] for other layout scenarios.
182///
183/// # Parameters
184/// - `args`: Configuration for the spacer's width and height. Accepts any type convertible to [`SpacerArgs`].
185#[tessera]
186pub fn spacer(args: impl Into<SpacerArgs>) {
187    let args: SpacerArgs = args.into();
188
189    measure(Box::new(move |input| {
190        let spacer_intrinsic_constraint = Constraint::new(args.width, args.height);
191        let effective_spacer_constraint =
192            spacer_intrinsic_constraint.merge(input.parent_constraint);
193
194        let final_spacer_width = match effective_spacer_constraint.width {
195            DimensionValue::Fixed(w) => w,
196            DimensionValue::Wrap { min, .. } => min.unwrap_or(Px(0)), // Spacer has no content, so it's its min or 0.
197            DimensionValue::Fill { min, max: _ } => {
198                // If the effective constraint is Fill, it means the parent allows filling.
199                // However, a simple spacer has no content to expand beyond its minimum.
200                // The actual size it gets if parent is Fill and allocates space
201                // would be determined by the parent's layout logic (e.g. row/column giving it a Fixed size).
202                // Here, based purely on `effective_spacer_constraint` being Fill,
203                // it should take at least its `min` value.
204                // If parent constraint was Fixed(v), merge would result in Fixed(v.clamp(min, max)).
205                // If parent was Wrap, merge would result in Fill{min,max} (if spacer was Fill).
206                // If parent was Fill{p_min, p_max}, merge would result in Fill{combined_min, combined_max}.
207                // In all Fill cases, the spacer itself doesn't "push" for more than its min.
208                min.unwrap_or(Px(0))
209            }
210        };
211
212        let final_spacer_height = match effective_spacer_constraint.height {
213            DimensionValue::Fixed(h) => h,
214            DimensionValue::Wrap { min, .. } => min.unwrap_or(Px(0)),
215            DimensionValue::Fill { min, max: _ } => min.unwrap_or(Px(0)),
216        };
217
218        Ok(ComputedData {
219            width: final_spacer_width,
220            height: final_spacer_height,
221        })
222    }));
223}