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};
20use tessera_ui_macros::tessera;
21
22///
23/// Arguments for configuring the [`spacer`] component.
24///
25/// `SpacerArgs` allows you to specify the width and height behavior of a spacer in a layout.
26/// By default, both width and height are fixed to zero pixels. To create a flexible spacer
27/// that expands to fill available space, use [`DimensionValue::Fill`] for the desired axis.
28///
29/// # Example
30/// ```
31/// use tessera_ui_basic_components::spacer::{spacer, SpacerArgs};
32/// use tessera_ui::{DimensionValue, Px};
33///
34/// // Fixed-size spacer (default)
35/// spacer(SpacerArgs::default());
36///
37/// // Expanding spacer (fills available width)
38/// spacer(SpacerArgs {
39///     width: DimensionValue::Fill { min: None, max: None },
40///     height: DimensionValue::Fixed(Px(0)),
41/// });
42/// ```
43#[derive(Default, Clone, Copy, Builder)]
44#[builder(pattern = "owned")]
45pub struct SpacerArgs {
46    /// The desired width behavior of the spacer.
47    ///
48    /// Defaults to `Fixed(Px(0))`. Use `Fill { min: None, max: None }` for an expanding spacer.
49    #[builder(default = "DimensionValue::Fixed(Px(0))")]
50    pub width: DimensionValue,
51    /// The desired height behavior of the spacer.
52    ///
53    /// Defaults to `Fixed(Px(0))`. Use `Fill { min: None, max: None }` for an expanding spacer.
54    #[builder(default = "DimensionValue::Fixed(Px(0))")]
55    pub height: DimensionValue,
56}
57
58impl SpacerArgs {
59    /// Creates a spacer that tries to fill available space in both width and height.
60    ///
61    /// # Example
62    /// ```
63    /// use tessera_ui_basic_components::spacer::SpacerArgs;
64    /// let args = SpacerArgs::fill_both();
65    /// ```
66    pub fn fill_both() -> Self {
67        SpacerArgsBuilder::default()
68            .width(DimensionValue::Fill {
69                min: None,
70                max: None,
71            })
72            .height(DimensionValue::Fill {
73                min: None,
74                max: None,
75            })
76            .build()
77            .unwrap() // build() should not fail with these defaults
78    }
79
80    /// Creates a spacer that tries to fill available width.
81    ///
82    /// # Example
83    /// ```
84    /// use tessera_ui_basic_components::spacer::SpacerArgs;
85    /// let args = SpacerArgs::fill_width();
86    /// ```
87    pub fn fill_width() -> Self {
88        SpacerArgsBuilder::default()
89            .width(DimensionValue::Fill {
90                min: None,
91                max: None,
92            })
93            .height(DimensionValue::Fixed(Px(0))) // Default height if only filling width
94            .build()
95            .unwrap()
96    }
97
98    /// Creates a spacer that tries to fill available height.
99    ///
100    /// # Example
101    /// ```
102    /// use tessera_ui_basic_components::spacer::SpacerArgs;
103    /// let args = SpacerArgs::fill_height();
104    /// ```
105    pub fn fill_height() -> Self {
106        SpacerArgsBuilder::default()
107            .width(DimensionValue::Fixed(Px(0))) // Default width if only filling height
108            .height(DimensionValue::Fill {
109                min: None,
110                max: None,
111            })
112            .build()
113            .unwrap()
114    }
115}
116
117impl From<Dp> for SpacerArgs {
118    /// Creates a fixed-size spacer from a [`Dp`] value for both width and height.
119    ///
120    /// # Example
121    /// ```
122    /// use tessera_ui_basic_components::spacer::SpacerArgs;
123    /// use tessera_ui::Dp;
124    /// let args = SpacerArgs::from(Dp(8.0));
125    /// ```
126    fn from(value: Dp) -> Self {
127        SpacerArgsBuilder::default()
128            .width(DimensionValue::Fixed(value.to_px()))
129            .height(DimensionValue::Fixed(value.to_px()))
130            .build()
131            .unwrap()
132    }
133}
134
135impl From<Px> for SpacerArgs {
136    /// Creates a fixed-size spacer from a [`Px`] value for both width and height.
137    ///
138    /// # Example
139    /// ```
140    /// use tessera_ui_basic_components::spacer::SpacerArgs;
141    /// use tessera_ui::Px;
142    /// let args = SpacerArgs::from(Px(16));
143    /// ```
144    fn from(value: Px) -> Self {
145        SpacerArgsBuilder::default()
146            .width(DimensionValue::Fixed(value))
147            .height(DimensionValue::Fixed(value))
148            .build()
149            .unwrap()
150    }
151}
152
153///
154/// A component that inserts an empty, flexible space into a layout.
155///
156/// The `spacer` component is commonly used to add gaps between other UI elements,
157/// or to create flexible layouts where certain areas expand to fill available space.
158/// The behavior of the spacer is controlled by the [`SpacerArgs`] parameter, which
159/// allows you to specify fixed or flexible sizing for width and height using [`DimensionValue`].
160///
161/// - Use `DimensionValue::Fixed` for a fixed-size spacer.
162/// - Use `DimensionValue::Fill` to make the spacer expand to fill available space in its parent container.
163///
164/// # Example
165/// ```
166/// use tessera_ui_basic_components::{
167///     row::{row_ui, RowArgs},
168///     spacer::{spacer, SpacerArgs},
169///     text::text,
170/// };
171///
172/// row_ui!(
173///     RowArgs::default(),
174///     || text("Left".to_string()),
175///     // This spacer will fill the available width, pushing "Right" to the end.
176///     || spacer(SpacerArgs::fill_width()),
177///     || text("Right".to_string()),
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}