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 );
}
}