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