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}