ltk/widget/container/mod.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 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
use crate::theme::Paint;
use crate::types::{ Color, Corners, Length };
use crate::render::Canvas;
use super::Element;
#[ cfg( test ) ]
mod tests;
/// A transparent wrapper that adds a background color or a themed
/// surface and padding around any child [`Element`].
///
/// Does not consume a flat index — it is invisible to focus/hit-testing.
///
/// Two background styles. [`Container::background`] paints a flat
/// colour rounded rect. [`Container::surface`] names a theme slot (a
/// `"type": "surface"` entry in the active `ThemeDocument`) which
/// resolves at paint time to a full Glass stack: gradient / solid
/// fill, outer drop shadow, inset shadows, backdrop blur. `surface`
/// takes precedence when both are set, and degrades to `background`
/// (or to no background at all, when neither is set) if the slot is
/// absent from the active theme — third-party themes that do not
/// ship the named surface still render the content, just without
/// the Glass chrome.
///
/// ```rust,no_run
/// # use ltk::{ column, container, row, text, Color, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex(
/// # icon: Element<Msg>,
/// # title: Element<Msg>,
/// # subtitle: Element<Msg>,
/// # ) -> ( Element<Msg>, Element<Msg> ) {
/// // Flat colour
/// let flat = container( text( "Hello" ) )
/// .background( Color::rgb( 0.2, 0.2, 0.25 ) )
/// .padding( 12.0 );
///
/// // Glass card backed by a named theme surface
/// let card = container(
/// row()
/// .push( icon )
/// .push( column().push( title ).push( subtitle ) )
/// )
/// .surface( "surface-card" )
/// .radius( 32.0 )
/// .padding_h( 16.5 )
/// .padding_v( 24.0 );
/// # ( flat.into(), card.into() )
/// # }
/// ```
pub struct Container<Msg: Clone>
{
pub child: Box<Element<Msg>>,
/// Optional background paint — flat colour, linear or radial
/// gradient. Constructed via [`Container::background`], which
/// accepts anything `Into<Paint>` (a plain [`Color`] gets
/// promoted to [`Paint::Solid`] via the trait impl).
pub background: Option<Paint>,
/// Slot id of a themed surface (resolved via
/// [`crate::theme::resolve_surface`]). When set, takes precedence
/// over `background` and paints the full Glass stack instead of a
/// flat colour fill.
pub surface: Option<String>,
/// Per-corner radii applied to every painted layer of the
/// container chrome — flat fill, themed surface (gradient + outer
/// shadows + insets + backdrop blur). Stored as [`Corners`] so
/// callers can pin the rounded shape to one or two corners (a
/// panel pinned to the screen bottom, a side panel pinned to the
/// left edge, …) without hitting the renderer with an offset
/// trick.
pub corners: Corners,
/// Padding on the top edge — gap between the container's top boundary
/// and its child. Stored as a [`Length`] so it can scale with the
/// viewport via [`Length::dp`] / [`Length::vmin`].
pub pad_top: Length,
/// Padding on the right edge.
pub pad_right: Length,
/// Padding on the bottom edge.
pub pad_bottom: Length,
/// Padding on the left edge.
pub pad_left: Length,
pub opacity: f32,
/// Optional `( color, width_px )` border stroke painted around the
/// container's rounded rectangle, after the fill / surface and
/// before the child draws. `None` leaves the chrome flat.
pub border: Option<( Color, f32 )>,
/// Optional hard cap on the container's outer width. When the parent
/// offers more, the container reports its preferred width as
/// `min( offered, max_width )` so it does not stretch to fill.
/// Mirrors the same flag on [`Column`](crate::layout::column::Column)
/// and [`Row`](crate::layout::row::Row).
pub max_width: Option<f32>,
/// When true, the contents of this container are announced by
/// assistive technologies as a `Live::Polite` region — useful for
/// toasts, status banners and OSDs that need to be read on
/// appearance even when the user has not navigated to them.
pub a11y_live: bool,
}
impl<Msg: Clone> Container<Msg>
{
pub fn new( child: impl Into<Element<Msg>> ) -> Self
{
Self
{
child: Box::new( child.into() ),
background: None,
surface: None,
corners: Corners::ZERO,
pad_top: Length::px( 0.0 ),
pad_right: Length::px( 0.0 ),
pad_bottom: Length::px( 0.0 ),
pad_left: Length::px( 0.0 ),
opacity: 1.0,
border: None,
max_width: None,
a11y_live: false,
}
}
pub fn live_region( mut self, live: bool ) -> Self
{
self.a11y_live = live;
self
}
/// Paint a rounded-rect stroke around the container with the given
/// colour and pixel width. Useful for input fields, popovers and
/// any chrome the design system specifies as outlined rather than
/// filled.
pub fn border( mut self, color: Color, width: f32 ) -> Self
{
self.border = Some( ( color, width.max( 0.0 ) ) );
self
}
/// Set the background fill. Accepts anything convertible to
/// [`Paint`] — a plain [`Color`] (auto-wrapped in
/// [`Paint::Solid`]) or an explicit [`crate::theme::LinearGradient`]
/// / [`crate::theme::RadialGradient`]. Ignored at paint time if a
/// themed [`surface`](Self::surface) is set and resolves against
/// the active theme.
pub fn background( mut self, paint: impl Into<Paint> ) -> Self
{
self.background = Some( paint.into() );
self
}
/// Back the container with a themed surface slot. The slot id is
/// resolved against the active `ThemeDocument` at paint time via
/// [`crate::theme::resolve_surface`]; missing slots fall through
/// to [`background`](Self::background) or to no background at all.
///
/// Slot ids are documented by the theme. The default theme ships
/// `surface-card` (generic Glass container) and the slider-specific
/// slots; downstream themes are free to add their own.
pub fn surface( mut self, slot: impl Into<String> ) -> Self
{
self.surface = Some( slot.into() );
self
}
/// Set the corner radii for every painted layer of the container
/// chrome. Accepts a single `f32` (uniform radius — the common
/// case, equivalent to `Corners::all( r )`), a tuple `( tl, tr,
/// br, bl )` (CSS shorthand order), or any explicit
/// [`Corners`] value.
///
/// ```rust,no_run
/// # use ltk::{ container, text, Corners, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> ( Element<Msg>, Element<Msg>, Element<Msg> ) {
/// // Uniform 16 px on all corners (single-value form).
/// let a = container( text( "child" ) ).radius( 16.0 );
///
/// // Rounded top corners only — for a panel pinned flush against
/// // the bottom edge of the screen.
/// let b = container( text( "child" ) ).radius( Corners::top( 16.0 ) );
///
/// // Custom four-corner radii.
/// let c = container( text( "child" ) ).radius( ( 16.0, 16.0, 0.0, 0.0 ) );
/// # ( a.into(), b.into(), c.into() )
/// # }
/// ```
pub fn radius( mut self, corners: impl Into<Corners> ) -> Self
{
self.corners = corners.into();
self
}
/// Set uniform padding on all four sides — equivalent to setting
/// `padding_top`, `padding_right`, `padding_bottom`, and
/// `padding_left` to `p`. Asymmetric variants
/// ([`padding_top`](Self::padding_top), …) override individual
/// edges, so calling this first and then a per-edge setter is the
/// idiomatic way to express "uniform padding except for one
/// edge".
pub fn padding( mut self, p: impl Into<Length> ) -> Self
{
let p = p.into();
self.pad_top = p;
self.pad_right = p;
self.pad_bottom = p;
self.pad_left = p;
self
}
/// Set horizontal padding (left + right each).
pub fn padding_h( mut self, p: impl Into<Length> ) -> Self
{
let p = p.into();
self.pad_left = p;
self.pad_right = p;
self
}
/// Set vertical padding (top + bottom each).
pub fn padding_v( mut self, p: impl Into<Length> ) -> Self
{
let p = p.into();
self.pad_top = p;
self.pad_bottom = p;
self
}
/// Set the top edge padding only. Pairs with
/// [`padding_bottom`](Self::padding_bottom) for asymmetric
/// vertical insets.
pub fn padding_top( mut self, p: impl Into<Length> ) -> Self
{
self.pad_top = p.into();
self
}
/// Set the right edge padding only.
pub fn padding_right( mut self, p: impl Into<Length> ) -> Self
{
self.pad_right = p.into();
self
}
/// Set the bottom edge padding only.
pub fn padding_bottom( mut self, p: impl Into<Length> ) -> Self
{
self.pad_bottom = p.into();
self
}
/// Set the left edge padding only.
pub fn padding_left( mut self, p: impl Into<Length> ) -> Self
{
self.pad_left = p.into();
self
}
/// Set opacity for the entire container and its contents (0.0 = transparent, 1.0 = opaque).
pub fn opacity( mut self, alpha: f32 ) -> Self
{
self.opacity = alpha.clamp( 0.0, 1.0 );
self
}
/// Cap the container's outer width in logical px. When the parent
/// offers a wider rect, the container reports its preferred width as
/// `min( offered, w )` so it does not stretch to fill.
pub fn max_width( mut self, w: f32 ) -> Self
{
self.max_width = Some( w );
self
}
/// Return the preferred `(width, height)` accounting for padding.
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
let vp = canvas.viewport_layout();
let em = Length::EM_BASE_DEFAULT;
let pad_l = self.pad_left.resolve( vp, em );
let pad_r = self.pad_right.resolve( vp, em );
let pad_t = self.pad_top.resolve( vp, em );
let pad_b = self.pad_bottom.resolve( vp, em );
let avail = self.max_width.map( |m| max_width.min( m ) ).unwrap_or( max_width );
let pad_x = pad_l + pad_r;
let pad_y = pad_t + pad_b;
let inner_w = ( avail - pad_x ).max( 0.0 );
let ( cw, ch ) = self.child.preferred_size( inner_w, canvas );
( cw + pad_x, ch + pad_y )
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Container<U>
where
U: Clone + 'static,
Msg: 'static,
{
Container
{
child: Box::new( self.child.map_arc( f ) ),
background: self.background,
surface: self.surface,
corners: self.corners,
pad_top: self.pad_top,
pad_right: self.pad_right,
pad_bottom: self.pad_bottom,
pad_left: self.pad_left,
opacity: self.opacity,
border: self.border,
max_width: self.max_width,
a11y_live: self.a11y_live,
}
}
}
/// Create a [`Container`] that wraps `child`.
pub fn container<Msg: Clone>( child: impl Into<Element<Msg>> ) -> Container<Msg>
{
Container::new( child )
}