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}