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}