ltk/layout/
spacer.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use crate::render::Canvas;
use crate::types::Length;
use crate::widget::Element;

/// A flexible, invisible spacer that expands to fill available space.
///
/// The optional `weight` controls how much of the remaining space this spacer
/// claims relative to other spacers in the same layout. A spacer with `weight = 2`
/// takes twice as much space as one with `weight = 1`.
///
/// Place a `Spacer` between two widgets inside a [`Column`](crate::layout::column::Column)
/// or [`Row`](crate::layout::row::Row) to push them apart:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "Top" ) )
///     .push( spacer() )    // pushes "Bottom" to the bottom
///     .push( text( "Bottom" ) )
/// .into()
/// # }
/// ```
///
/// Use [`.weight(n)`](Spacer::weight) to replace several consecutive spacers:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, Column };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() {
/// // These two are equivalent:
/// let _: Column<Msg> = column().push( spacer().weight( 3 ) );
/// let _: Column<Msg> = column().push( spacer() ).push( spacer() ).push( spacer() );
/// # }
/// ```
///
/// Use [`.height(px)`](Spacer::height) to create a fixed-size vertical spacer:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "Header" ) )
///     .push( spacer().height( 20.0 ) )  // Exactly 20 px gap
///     .push( text( "Content" ) )
/// .into()
/// # }
/// ```
///
/// `.height(...)` and `.width(...)` also accept any [`crate::Length`], so the
/// gap can scale with the surface instead of being frozen at a px constant:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Length, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "Header" ) )
///     // 6 % of the surface's smaller side, never below 16 px or above 64 px.
///     .push( spacer().height( Length::vmin( 6.0 ).clamp( 16.0, 64.0 ) ) )
///     .push( text( "Content" ) )
/// .into()
/// # }
/// ```
pub struct Spacer
{
	/// Relative weight of this spacer (default 1).
	pub weight: u32,
	/// Fixed height (overrides flexible behavior in a column). Accepts any
	/// [`Length`] — pass an `f32`/`i32`/`u32` for the px case (kept for
	/// backward compatibility with existing call sites), or
	/// `Length::vmin( … )` etc. for viewport-relative gaps.
	pub fixed_height: Option<Length>,
	/// Fixed width (overrides flexible behavior in a row). Same length-type
	/// semantics as [`Self::fixed_height`].
	pub fixed_width:  Option<Length>,
}

impl Spacer
{
	/// Set the relative weight of this spacer (default 1).
	pub fn weight( mut self, w: u32 ) -> Self
	{
		self.weight = w;
		self
	}

	/// Set a fixed height for this spacer. Accepts any [`Length`]: a bare
	/// `24.0_f32` is treated as `Length::px( 24.0 )` for source-level
	/// backwards compatibility; a `Length::vmin( 6.0 )` makes the gap
	/// scale with the surface's smaller dimension.
	pub fn height( mut self, h: impl Into<Length> ) -> Self
	{
		self.fixed_height = Some( h.into() );
		self
	}

	/// Set a fixed width for this spacer. Mirrors [`Self::height`] for the
	/// horizontal axis.
	pub fn width( mut self, w: impl Into<Length> ) -> Self
	{
		self.fixed_width = Some( w.into() );
		self
	}

	/// Returns `( fixed_width, fixed_height )` resolved against the
	/// current canvas viewport, falling back to `0.0` on axes that were
	/// not pinned. The parent layout distributes leftover along its main
	/// axis among the still-flexible spacers and `Flex` wrappers,
	/// weighted by `weight`.
	pub fn preferred_size( &self, canvas: &Canvas ) -> ( f32, f32 )
	{
		let vp = canvas.viewport_layout();
		let em = Length::EM_BASE_DEFAULT;
		(
			self.fixed_width .map( |l| l.resolve( vp, em ) ).unwrap_or( 0.0 ),
			self.fixed_height.map( |l| l.resolve( vp, em ) ).unwrap_or( 0.0 ),
		)
	}

	/// Resolved fixed height in logical pixels, or `None` when the
	/// spacer is flex. Cheaper than [`Self::preferred_size`] when the
	/// layout only needs the main-axis size for one orientation.
	pub fn resolved_height( &self, canvas: &Canvas ) -> Option<f32>
	{
		self.fixed_height.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
	}

	pub fn resolved_width( &self, canvas: &Canvas ) -> Option<f32>
	{
		self.fixed_width.map( |l| l.resolve( canvas.viewport_layout(), Length::EM_BASE_DEFAULT ) )
	}

	/// No-op — spacers are invisible.
	pub fn draw( &self ) {}
}

impl<Msg: Clone + 'static> From<Spacer> for Element<Msg>
{
	fn from( s: Spacer ) -> Self
	{
		Element::Spacer( s )
	}
}

/// Create a flexible spacer with weight 1.
///
/// Call [`.weight(n)`](Spacer::weight) to set a relative weight greater than 1:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, Column };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() {
/// // These two are equivalent:
/// let _: Column<Msg> = column().push( spacer().weight( 3 ) );
/// let _: Column<Msg> = column().push( spacer() ).push( spacer() ).push( spacer() );
/// # }
/// ```
///
/// Call [`.height(px)`](Spacer::height) to create a fixed-size vertical gap:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "First" ) )
///     .push( spacer().height( 24.0 ) )  // Fixed 24px gap
///     .push( text( "Second" ) )
/// .into()
/// # }
/// ```
pub fn spacer() -> Spacer
{
	Spacer { weight: 1, fixed_height: None, fixed_width: None }
}

#[ cfg( test ) ]
mod tests
{
	use super::*;
	use crate::render::Canvas;

	fn make_canvas() -> Canvas { Canvas::new( 800, 600 ) }

	#[ test ]
	fn height_accepts_f32_as_pixels()
	{
		let s = spacer().height( 24.0 );
		let canvas = make_canvas();
		assert_eq!( s.resolved_height( &canvas ), Some( 24.0 ) );
	}

	#[ test ]
	fn height_accepts_length_and_resolves_against_viewport()
	{
		// 10 % of the smaller side (= 600) = 60 px.
		let s = spacer().height( Length::vmin( 10.0 ) );
		let canvas = make_canvas();
		assert_eq!( s.resolved_height( &canvas ), Some( 60.0 ) );
	}

	#[ test ]
	fn width_accepts_length_and_resolves_against_viewport()
	{
		let s = spacer().width( Length::vw( 25.0 ) );
		let canvas = make_canvas();
		// 25 % of 800 = 200.
		assert_eq!( s.resolved_width( &canvas ), Some( 200.0 ) );
	}

	#[ test ]
	fn flex_spacer_has_no_resolved_dimensions()
	{
		let s = spacer().weight( 3 );
		let canvas = make_canvas();
		assert_eq!( s.resolved_height( &canvas ), None );
		assert_eq!( s.resolved_width(  &canvas ), None );
	}
}