tessera_ui_basic_components/
spacer.rs

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