Files
ltk/docs/widgets.md
Pedro M. de Echanove Pasquin e343142347
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
viewport: local_viewport() opt-out of root-viewport inheritance; list_item: height/font_size builders
`Viewport::local_viewport()` resolves the child's viewport-relative (vw / vh / vmin) and fluid Lengths against the viewport's own rect instead of the root layout viewport the sub-canvas inherits since the fluid-resolution inheritance change. That inheritance is right for scroll-like clips (content renders the same size inside and outside), but it is exactly wrong for a fixed-size floating mini-UI — crustace's phone-shaped quick-settings pill pinned to a corner of a desktop-wide surface resolved its vw text and fluid stock geometry against the whole monitor, inflating the content past the pill's fixed clip and cutting off the bottom stripe. The flag pins the sub-canvas's layout viewport to its own size via the new crate-internal Canvas::set_local_layout_viewport, nested sub-canvases keep propagating the pinned value, and a unit test covers the override plus propagation.
`ListItem::height( impl Into<Length> )` and `ListItem::font_size( impl Into<Length> )` mirror the Toggle / Radio height() builders: override the theme row height (floored at the label's rendered height so the text never clips) and the primary-label font size (subtitle and trailing keep their theme sizes), letting dense context menus trade the stock touch-target generosity for row density.
Docs: widgets.md gains both builder sets, architecture.md documents the layout-viewport inheritance model next to the per-canvas density it parallels, the cookbook slide-in panel recipe notes when the pill needs local_viewport(), and the toggle / radio height() rustdoc demotes its private theme::HEIGHT link to a code span so cargo doc is warning-free again. CHANGELOG entries added.
2026-08-02 13:38:16 +02:00

1037 lines
35 KiB
Markdown

# ltk widget catalogue
A flat reference of every widget and layout exported at the crate root.
Each entry has the same four sections: **what it is**, **when to use it**,
**minimal example**, **see also**. The full builder API and per-method
docs live on `cargo doc`; this file is the at-a-glance index.
For mental model and architecture, read [`docs/onboarding.md`](./onboarding.md)
and [`docs/architecture.md`](./architecture.md) first. For copy-pasteable
patterns built from these widgets, see [`docs/cookbook.md`](./cookbook.md).
## Table of contents
- [Buttons and activations](#buttons-and-activations)
- [`button`](#button) · [`icon_button`](#icon_button) · [`pressable`](#pressable) · [`window_button`](#window_button) · [`list_item`](#list_item)
- [Stateful binary controls](#stateful-binary-controls)
- [`toggle`](#toggle) · [`checkbox`](#checkbox) · [`radio`](#radio)
- [Continuous controls](#continuous-controls)
- [`slider`](#slider) · [`vslider`](#vslider) · [`progress_bar`](#progress_bar)
- [Text input and display](#text-input-and-display)
- [`text`](#text) · [`text_edit`](#text_edit) · [`rich_text`](#rich_text)
- [Decoration and chrome](#decoration-and-chrome)
- [`container`](#container) · [`separator`](#separator) · [`img_widget`](#img_widget) · [`external`](#external)
- [Clipping wrappers](#clipping-wrappers)
- [`scroll`](#scroll) · [`viewport`](#viewport) · [`flex`](#flex) · [`carousel`](#carousel)
- [Overlays and feedback](#overlays-and-feedback)
- [`spinner`](#spinner) · [`toast`](#toast) · [`tooltip`](#tooltip) · [`combo`](#combo) · [`tabs`](#tabs) · [`notebook`](#notebook) · [`dialog`](#dialog)
- [Pickers](#pickers)
- [`date_picker`](#date_picker) · [`time_picker`](#time_picker) · [`color_picker`](#color_picker)
- [Layouts](#layouts)
- [`column`](#column) · [`row`](#row) · [`stack`](#stack) · [`grid`](#grid) · [`spacer`](#spacer)
---
## Buttons and activations
### `button`
A standard text button. Activates on tap, Enter, or Space when focused.
`font_size`, `height` and `width` all take an `impl Into<Length>`, so the
box can scale with the surface (e.g. a full-width form button) instead of
sizing to its label.
**When**: any place a normal app would have a "Save" / "Cancel" / "Send"
control.
```rust,no_run
# use ltk::{ button, Element, Length };
# #[ derive( Clone ) ] enum Msg { Save }
# fn _ex() -> Element<Msg> {
button( "Save" )
.height( Length::vmin( 9.0 ).clamp( 44.0, 72.0 ) )
.width( Length::orient( 95.0, 25.0 ) )
.on_press( Msg::Save )
.into()
# }
```
**See also**: [`pressable`](#pressable) for activation on a richer
custom-shaped surface, [`icon_button`](#icon_button) for image-only
buttons.
### `icon_button`
A button whose visual is an RGBA bitmap instead of text. Same dispatch
shape as [`button`](#button).
**When**: toolbar icons, system tray glyphs, anywhere the visual is
icon-first.
```rust,no_run
# use std::sync::Arc;
# use ltk::{ icon_button, Element };
# #[ derive( Clone ) ] enum Msg { OpenSearch }
# fn _ex( rgba_bytes: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
icon_button( rgba_bytes, w, h ).on_press( Msg::OpenSearch )
.into()
# }
```
**See also**: [`button`](#button), [`window_button`](#window_button)
(specialised icon button for window decorations).
### `pressable`
Wraps any [`Element`](../src/widget/mod.rs) so it dispatches a press
message. Invisible to drawing — the wrapped child paints itself.
**When**: a card, a custom-styled list row, or any non-trivial visual
that should behave like a button. Inner widgets that are themselves
interactive (a button nested inside) keep priority.
```rust,no_run
# use ltk::{ column, container, pressable, row, Element, Pressable };
# #[ derive( Clone ) ] enum Msg { OpenWifiPicker }
# fn _ex(
# icon: Element<Msg>,
# title: Element<Msg>,
# subtitle: Element<Msg>,
# ) -> Pressable<Msg> {
pressable(
container( row()
.push( icon )
.push( column().push( title ).push( subtitle ) ) )
.surface( "surface-card" )
)
.on_press( Msg::OpenWifiPicker )
# }
```
**See also**: [`button`](#button) when a plain text button is enough,
[`list_item`](#list_item) for the standard "label + subtitle + trailing"
pattern.
### `window_button`
A title-bar control button (minimize / maximize / restore / close).
Comes with a special hover-tint for `Close`.
**When**: building custom server-side window decorations.
```rust,no_run
# use ltk::{ window_button, window_controls, Element, WindowButton, WindowButtonKind };
# #[ derive( Clone ) ] enum Msg { CloseWindow, Minimize, Maximize, Close }
# fn _ex() -> ( WindowButton<Msg>, Element<Msg> ) {
let close = window_button( WindowButtonKind::Close ).on_press( Msg::CloseWindow );
// Or the full standard set:
let bar = window_controls(
Some( Msg::Minimize ),
WindowButtonKind::Maximize,
Some( Msg::Maximize ),
Some( Msg::Close ),
);
# ( close, bar.into() )
# }
```
`focusable( true )` opts the button into the Tab cycle (off by default
to match desktop convention).
**See also**: `theme_window_controls` for the colour tokens each mode
supplies.
### `list_item`
A row with a primary label, optional subtitle, optional leading icon,
optional right-aligned trailing text and/or trailing icon, and a
tappable surface.
**When**: settings menus, navigation lists, contact rows. Grows taller when a subtitle is set.
```rust,no_run
# use ltk::{ list_item, ListItem };
# #[ derive( Clone ) ] enum Msg { OpenWifi }
# fn _ex() -> ListItem<Msg> {
let mut it = list_item( "Wi-Fi" )
.subtitle( "Eduroam" )
.on_press( Msg::OpenWifi );
// Disclosure arrow from the active theme, tinted to match the
// trailing-text colour.
if let Some( ( rgba, w, h ) ) = ltk::theme_icon_rgba( "general/right", 21 )
{
let tinted = std::sync::Arc::new( ltk::tint_symbolic( &rgba, ltk::theme_palette().text_secondary ) );
it = it.trailing_icon( tinted, w, h );
}
it
# }
```
`trailing( text )` right-aligns a text value (current setting, badge
count); `trailing_icon( rgba, w, h )` draws an icon at the right edge.
When both are set the text sits to the left of the icon.
`pad_h( px )` overrides the horizontal inset between the row edge and
its content (theme default 16 px) — lower it when the enclosing view's
own padding already provides the margin.
`height( len )` overrides the theme row height (56 design px, 68 with a subtitle) and `font_size( len )` the primary-label size (16 design px; subtitle and trailing keep their theme sizes), both `impl Into<Length>`. Use them together where a dense list — a context menu, a compact picker — should trade the touch-target generosity of a settings list for row density; the resolved height is floored at the label's rendered height so the text never clips however aggressive the override.
**See also**: [`pressable`](#pressable) for free-form tappable rows,
[`scroll`](#scroll) to wrap a list of items in a scrollable container.
---
## Stateful binary controls
### `toggle`
A two-state on / off switch. Renders as a horizontal pill with a sliding
thumb.
**When**: prominent settings toggles ("Wi-Fi", "Do not disturb").
```rust,no_run
# use ltk::{ toggle, Toggle };
# #[ derive( Clone ) ] enum Msg { ToggleWifi }
# struct App { wifi_enabled: bool }
# impl App { fn _ex( &self ) -> Toggle<Msg> {
toggle( self.wifi_enabled )
.label( "Wi-Fi" )
.on_toggle( Msg::ToggleWifi )
# }}
```
**See also**: [`checkbox`](#checkbox) for less prominent opt-ins,
[`radio`](#radio) for mutually-exclusive groups.
### `checkbox`
A two-state opt-in with a square box and a check glyph.
**When**: form fields, terms acceptance, multi-select lists.
```rust,no_run
# use ltk::{ checkbox, Checkbox };
# #[ derive( Clone ) ] enum Msg { ToggleTerms }
# struct App { accept_terms: bool }
# impl App { fn _ex( &self ) -> Checkbox<Msg> {
checkbox( self.accept_terms )
.label( "I accept the terms" )
.on_toggle( Msg::ToggleTerms )
# }}
```
**See also**: [`toggle`](#toggle), [`radio`](#radio).
### `radio`
A single option inside a mutually-exclusive group. Build one per
variant; the application owns "which is selected".
**When**: priority pickers, layout choices, anywhere exactly one of N
is selected.
```rust,no_run
# use ltk::{ column, radio, Element };
# #[ derive( Clone, PartialEq ) ] enum Priority { Low, Medium, High }
# #[ derive( Clone ) ] enum Msg { SetPriority( Priority ) }
# struct App { priority: Priority }
# impl App { fn _ex( &self ) -> Element<Msg> {
column()
.push( radio( self.priority == Priority::Low ).label( "Low" ).on_select( Msg::SetPriority( Priority::Low ) ) )
.push( radio( self.priority == Priority::Medium ).label( "Medium" ).on_select( Msg::SetPriority( Priority::Medium ) ) )
.push( radio( self.priority == Priority::High ).label( "High" ).on_select( Msg::SetPriority( Priority::High ) ) )
.into()
# }}
```
**See also**: [`checkbox`](#checkbox), [`toggle`](#toggle).
---
## Continuous controls
### `slider`
A horizontal slider for selecting a value in `[0.0, 1.0]`.
**When**: brightness / volume / scrub bars. Drag the thumb or tap a
position; the change message fires continuously during drag.
```rust,no_run
# use ltk::{ slider, Slider };
# #[ derive( Clone ) ] enum Msg { SetBrightness( f32 ) }
# struct App { brightness: f32 }
# impl App { fn _ex( &self ) -> Slider<Msg> {
slider( self.brightness )
.on_change( |v| Msg::SetBrightness( v ) )
# }}
```
`accent_thumb( true )` swaps the default thumb for the two-circle
brand-coloured variant. `track_surface( id )` and `fill_surface( id )`
override the default theme slots.
**See also**: [`vslider`](#vslider) for the vertical axis,
[`progress_bar`](#progress_bar) for read-only progress display.
### `vslider`
A vertical slider. Same value model as [`slider`](#slider) — `0.0` at
the bottom, `1.0` at the top.
**When**: column-shaped equalisers, compact volume / brightness picks
inside narrow side panels.
```rust,no_run
# use ltk::{ vslider, VSlider };
# #[ derive( Clone ) ] enum Msg { SetVolume( f32 ) }
# struct App { volume: f32 }
# impl App { fn _ex( &self ) -> VSlider<Msg> {
vslider( self.volume ).on_change( |v| Msg::SetVolume( v ) )
# }}
```
**See also**: [`slider`](#slider).
### `progress_bar`
A read-only linear progress indicator.
**When**: determinate operations with a known fraction
(downloads, file copies, install steps).
```rust,no_run
# use ltk::{ progress_bar, ProgressBar };
# struct App { download_fraction: f32 }
# impl App { fn _ex( &self ) -> ProgressBar {
progress_bar( self.download_fraction )
# }}
```
**See also**: [`spinner`](#spinner) for indeterminate operations
without a known fraction.
---
## Text input and display
### `text`
A text label. By default it stays on one line and truncates with an
ellipsis when wider than its rect; `wrap( true )` instead word-wraps to
the layout width, and `line_height( mult )` scales the gap between the
wrapped lines (`1.0` = the font's natural leading).
**When**: titles, captions, multi-line hints, anything non-interactive.
```rust,no_run
# use ltk::{ text, Color, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex() -> ( Element<Msg>, Element<Msg>, Element<Msg> ) {
let title = text( "Title" ).size( 24.0 ).color( Color::WHITE );
let centred = text( "Centred" ).align_center();
let hint = text( "Swipe up or press Enter to unlock" )
.align_center()
.wrap( true )
.line_height( 1.4 );
# ( title.into(), centred.into(), hint.into() )
# }
```
**See also**: [`text_edit`](#text_edit) for editable text.
### `text_edit`
A text input with cursor, Backspace, Enter / Submit, clipboard
support, a `secure( true )` password mode that masks the visible
characters and zeroizes the buffer on drop, and a
`password_toggle( visible, on_toggle )` builder that pins a show /
hide-password eye icon to the right edge of the field. Single-line by default; `multiline( true )` switches to a multi-row editor whose visible height follows `rows( n )`.
**When**: login fields, chat inputs, search boxes.
```rust,no_run
# use ltk::{ text_edit, Element };
# #[ derive( Clone ) ] enum Msg {
# UsernameChanged( String ), PasswordChanged( String ),
# TogglePassword, Submit,
# }
# struct App { username: String, password: String, show_password: bool }
# impl App { fn _ex( &self ) -> ( Element<Msg>, Element<Msg> ) {
let user = text_edit( "Username", &self.username )
.on_change( |s| Msg::UsernameChanged( s ) )
.on_submit( Msg::Submit );
// Password field with the built-in show / hide eye. The widget
// owns the icon hit-testing — taps inside the eye zone fire
// `TogglePassword` instead of moving the caret. Flip the bool in
// your `update`; the widget reads it back on the next render.
let pw = text_edit( "Password", &self.password )
.on_change( |s| Msg::PasswordChanged( s ) )
.password_toggle( self.show_password, Msg::TogglePassword );
# ( user.into(), pw.into() )
# }}
```
`password_toggle` keeps the wipe-on-drop and IME-bypass guarantees
of `secure( true )` regardless of the current visibility, so
flipping the eye does not weaken the field's threat model at
runtime — only what the user sees on screen changes.
**See also**: the password recipe in
[`docs/cookbook.md`](./cookbook.md#password-field-with-pam-submit).
### `rich_text`
A wrapped paragraph that carries a `Msg` per clickable link range — the
ltk counterpart of an Android `Spanned` with `URLSpan` / `ClickableSpan`.
Unlike `text`, the layout pass emits one hit rect per link *line*, so a
link that wraps across lines is hit-tested on each of its lines and taps
land on the link rather than on the whole paragraph.
**When**: body copy with inline links — a terms-and-conditions blurb, a
chat message with a URL, an "about" screen crediting a project.
```rust,no_run
# use ltk::{ rich_text, Element };
# #[ derive( Clone ) ] enum Msg { OpenTerms, OpenPrivacy }
# fn _ex() -> Element<Msg> {
let body = "By continuing you accept the Terms and the Privacy Policy.";
rich_text( body )
.size( 14.0 )
.link( 30, 35, Msg::OpenTerms ) // byte range of "Terms"
.link( 44, 58, Msg::OpenPrivacy ) // byte range of "Privacy Policy"
.into()
# }
```
Link ranges are **byte** offsets into the content `[start, end)`, drawn
underlined in `link_color`. `color` sets the non-link text colour, `size`
the font size (any `Length`), and `font( family, weight, style )` the
typeface resolved through the active theme on draw.
---
## Decoration and chrome
### `container`
A wrapper that adds padding, a background colour or themed surface, a
border radius, and (via theme slots) shadows / inset shadows / backdrop
blur.
**When**: cards, panels, callouts, anywhere a child needs a backdrop or
elevation.
```rust,no_run
# use ltk::{ column, container, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex( title: Element<Msg>, subtitle: Element<Msg> ) -> Element<Msg> {
container( column().push( title ).push( subtitle ) )
.surface( "surface-card" )
.padding( 16.0 )
.radius( 24.0 )
.into()
# }
```
Per-edge padding is available with `padding_top` / `padding_right` /
`padding_bottom` / `padding_left`. Per-corner radius takes a `Corners`
struct or a tuple.
`max_width(px)` caps the container's outer width — when the parent
offers a wider rect, the container reports `min( offered, px )` as its
preferred width instead of stretching to fill. Note the semantics differ from [`column`](#column)'s `max_width`, which caps the *content* width while the column still claims the full available rect ([`row`](#row) has no such flag); the container cap shrinks the widget itself, so a single decorated child gets a real width cap without extra wrapping.
**See also**: [`pressable`](#pressable) wrapping a `container` makes a
card interactive.
### `separator`
A horizontal divider line with a theme-default colour and a mode-scaled
thickness / vertical padding. `thickness` and `pad_v` take an `impl
Into<Length>`; both default to the process widget-scaling mode, and
passing `pad_v( 0.0 )` gives a flush, padding-less divider (distinct from
the mode default).
**When**: visual breaks between settings groups, list categories,
content blocks.
```rust,no_run
# use ltk::{ column, separator, text, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex() -> Element<Msg> {
column()
.push( text( "General" ) )
.push( separator() )
.push( text( "Network" ) )
.into()
# }
```
### `img_widget`
A static image rendered from RGBA pixel data shared via `Arc`.
**When**: wallpapers, icons, illustrations. The shared `Arc` makes
re-using the same buffer across frames a pointer copy.
```rust,no_run
# use std::sync::Arc;
# use ltk::{ img_widget, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex( rgba_bytes: Arc<Vec<u8>>, width: u32, height: u32 ) -> Element<Msg> {
img_widget( rgba_bytes, width, height )
.opacity( 0.8 )
.into()
# }
```
`cover()` scales to fill the rect preserving aspect; `size( w, h )` sets
explicit display dimensions.
**See also**: [`Image::from_path`](../src/widget/image.rs) helper for
disk-loaded files (PNG, JPEG via the `image` crate).
### `external`
An escape hatch that reserves layout space and defers its pixels to a
caller-provided producer, composited in-line with the rest of the tree.
Two sources:
- `External::cpu( w, h, |canvas, rect| … )` — an immediate-mode CPU
drawing closure invoked once per frame with the `Canvas` and the
widget's laid-out `rect` (physical pixels). Works on **both** backends
and is the way to host a custom `onDraw`-style routine — paths, clips,
text — straight onto the canvas with no GL round-trip.
- `External::new( w, h, ExternalSource::Texture( … ) )` — samples a
caller-owned GL texture each frame (a web engine, a video decoder).
**GLES only**; the producer keeps the texture and ltk only composites.
**When**: a `VectorDrawable` / Lottie frame, a custom-painted gauge, or
embedding another renderer's output.
```rust,no_run
# use ltk::{ External, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex() -> Element<Msg> {
External::cpu( 120.0, 120.0, |canvas, rect|
{
canvas.fill_rect( rect, ltk::Color::rgb( 0.1, 0.1, 0.12 ), 8.0 );
// any Canvas primitive: fill_path, set_clip_path, draw_text, …
} ).into()
# }
```
**See also**: the CPU-drawing and path-clip recipe in
[`docs/cookbook.md`](./cookbook.md#custom-cpu-drawing-and-path-clipping).
---
## Clipping wrappers
### `scroll`
A scrollable viewport — vertical by default, with `.horizontal()` and `.both()` builders switching the axis (`ScrollAxis::{ Vertical, Horizontal, Both }`). Drag the content to scroll; clipping is automatic.
**When**: lists or grids that may overflow the available height.
```rust,no_run
# use ltk::{ column, list_item, scroll, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex() -> Element<Msg> {
scroll(
column()
.push( list_item::<Msg>( "Item 1" ) )
.push( list_item( "Item 2" ) )
.push( list_item( "Item 3" ) )
)
.into()
# }
```
The scroll widget owns its own gesture handling — drags inside it do
not trigger the app-level `on_swipe_*` callbacks.
**See also**: [`viewport`](#viewport) for passive clipping without
gestures, [`grid`](#grid) inside a `scroll` for app drawers.
### `viewport`
A passive clipping wrapper. Clips its child to the assigned rect; no
scrolling, no gesture handling.
**When**: panels that animate in via a parent translation, fade-in
content, anywhere you want a hard clip without scrolling. The
`fade_bottom( px )` builder feathers the bottom edge to transparent
during slide-in animations (GLES backend; software backend renders a
hard edge).
```rust,no_run
# use ltk::{ viewport, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex( panel_view: Element<Msg>, panel_height: f32 ) -> Element<Msg> {
viewport( panel_view )
.height( panel_height )
.fade_bottom( 16.0 )
.into()
# }
```
By default the clip's sub-canvas inherits the root layout viewport, so viewport-relative (`vw` / `vh` / `vmin`) and fluid `Length`s inside resolve exactly as they would outside the clip — the right behaviour for scroll-like clipping. `local_viewport()` opts out: the child resolves those `Length`s against the viewport's own rect instead. Use it for a fixed-size floating mini-UI — a phone-shaped panel pinned to a corner of a desktop-wide surface — whose content is calibrated against the panel rect and must not scale with the surface hosting it.
**See also**: the slide-in panel recipe in
[`docs/cookbook.md`](./cookbook.md#slide-in-panel).
### `flex`
A filler wrapper for both flow layouts. Treats its non-spacer child
like a [`spacer`](#spacer) for leftover-space distribution but draws
the child inside the allocated rect: leftover width inside a
[`row`](#row), leftover height inside a [`column`](#column). Weights
split the leftover proportionally between flex / spacer siblings.
**When**: a row where one non-trivial child should fill the remaining
width (a card next to a fixed-size icon), or a column where a child
should absorb the remaining height (a log pane under fixed toolbars).
```rust,no_run
# use ltk::{ column, flex, row, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex(
# icon: Element<Msg>,
# title: Element<Msg>,
# subtitle: Element<Msg>,
# ) -> Element<Msg> {
row()
.push( icon )
.push( flex( column().push( title ).push( subtitle ) ) )
.into()
# }
```
`weight( n )` sets the relative share when there are several flex /
spacer siblings.
**See also**: [`spacer`](#spacer) for invisible fillers,
[`column`](#column) and [`row`](#row).
### `carousel`
Horizontal carousel: the focused child sits centred in the viewport at
`focused_width_frac` of the viewport width and its neighbours peek out
on the left / right at `gap` separation. The widget is a pure layout
primitive — the `offset` (positive shifts content right) is owned by
the caller, so drag / inertia / snap-ease live in the host (compositor,
gesture recogniser, etc.).
**When**: mobile-style app switchers, image / story strips, any
single-focus horizontal navigation where neighbours hint at the next /
previous tile.
```rust,no_run
# use ltk::{ button, carousel, container, Element, Color, Corners };
# #[ derive( Clone ) ] enum Msg { Open( usize ) }
# fn _ex( offset: f32 ) -> Element<Msg> {
carousel()
.focused_width_frac( 0.8 )
.gap( 16.0 )
.offset( offset )
.push( container( button::<Msg>( "Tile 1" ).on_press( Msg::Open( 0 ) ) )
.background( Color::rgb( 0.95, 0.4, 0.4 ) )
.radius( Corners::all( 12.0 ) ) )
.push( container( button::<Msg>( "Tile 2" ).on_press( Msg::Open( 1 ) ) )
.background( Color::rgb( 0.95, 0.85, 0.3 ) )
.radius( Corners::all( 12.0 ) ) )
.into()
# }
```
Helpers on the widget translate between offsets and indices:
`snap_offset( viewport_w, idx )` returns the offset that centres tile
`idx`; `focused_index( viewport_w )` rounds the current offset to the
nearest tile. The runnable demo at
[`examples/carousel.rs`](../examples/carousel.rs) shows Prev / Next
buttons and arrow-key navigation against an external `offset` state.
**See also**: [`scroll`](#scroll) for free vertical / horizontal panning
of arbitrary content; [`tabs`](#tabs) for a non-touch alternative.
---
## Overlays and feedback
### `spinner`
Indeterminate progress indicator. The widget is stateless: the application owns the rotation phase and advances it each frame — any monotonically increasing value works, only the fractional part is used. Pair with `App::is_animating` so the run loop keeps requesting redraws while the spinner is on screen.
**When**: long-running operations with no known fraction
(network calls, indexing, "waiting for compositor").
```rust,no_run
# use ltk::{ spinner, Spinner };
# struct App { spinner_phase: f32 }
# impl App { fn _ex( &self ) -> Spinner {
spinner().phase( self.spinner_phase ).size( 24.0 )
# }}
```
**See also**: [`progress_bar`](#progress_bar) for determinate progress.
### `toast`
Transient notification pill anchored near the bottom of the surface. Not an `Element` — build it in `App::overlays` and return its `.overlay()` while a toast is pending. Auto-dismissal is the application's responsibility: `duration( secs )` only stores a value read back via `duration_value()`, so the app schedules its own "toast expired" timer and clears the state when it fires.
**When**: confirmation snackbars ("Saved"), non-blocking errors,
status flashes.
```rust,no_run
# use ltk::{ toast, OverlaySpec };
# #[ derive( Clone ) ] enum Msg {}
# struct App { toast_message: Option<String> }
# impl App {
fn overlays( &self ) -> Vec<OverlaySpec<Msg>>
{
match &self.toast_message
{
Some( m ) => vec![ toast( m ).duration( 3.0 ).overlay() ],
None => vec![],
}
}
# }
```
**See also**: [`tooltip`](#tooltip) for hover-anchored hints.
### `tooltip`
Anchored hint rendered below a target widget. Like [`toast`](#toast) it is not an `Element` — return its `.overlay()` from `App::overlays` while the hint should be visible. Hover detection and the show / hide delay are the application's responsibility; the automatic variant is `Button::tooltip( text )`, which shows the hint by itself after a pointer dwell on the button.
**When**: discoverable affordances on icon-only controls, keyboard
shortcut reminders, helper text that should not occupy permanent
layout space.
```rust,no_run
# use ltk::{ tooltip, Tooltip, WidgetId };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex() -> Tooltip<Msg> {
tooltip( "Ctrl+S", WidgetId( "btn/save" ) ).max_width( 240 )
# }
```
The overlay anchors below the widget built with the matching
`.id( WidgetId )`. See [`docs/cookbook.md`](./cookbook.md) for the full
overlay wiring.
**See also**: [`toast`](#toast) for transient self-dismissing
notifications.
### `combo`
Select / dropdown with a popup list — single- or multi-select (`multi_select( true )` adds selection chips), with optional type-to-filter (`searchable( true )`). The widget is a stateless projection over an app-owned `ComboState`, and the app places **two** pieces: the trigger (`Combo::trigger()`) goes in the view tree like any widget, and the open popup is either layered into the same surface via `Combo::popup()` inside a [`stack`](#stack), or returned as a real xdg-popup from `App::overlays` via `Combo::overlay()`.
**When**: pick-one (or pick-several) fields with too many options for
a row of [`radio`](#radio) buttons (autocomplete, country list, theme
picker).
```rust,no_run
# use ltk::{ column, combo, Combo, ComboState, Element, OverlaySpec };
# #[ derive( Clone ) ] enum Msg { Pick( usize ), ToggleOpen, Dismiss }
# struct App { fruits: ComboState }
# impl App {
# fn build_combo( &self ) -> Combo<Msg> {
# let items = vec![ "One".to_string(), "Two".to_string(), "Three".to_string() ];
combo( self.fruits.clone(), items )
.on_toggle_open( Msg::ToggleOpen )
.on_select_idx( Msg::Pick )
.on_dismiss( Msg::Dismiss )
# }
fn view( &self ) -> Element<Msg>
{
column().push( self.build_combo().trigger() ).into()
}
fn overlays( &self ) -> Vec<OverlaySpec<Msg>>
{
self.build_combo().overlay().into_iter().collect()
}
# }
```
`update()` then flips `is_open` on `ToggleOpen` / `Dismiss` and writes
the selection into the `ComboState` on `Pick`.
**See also**: [`radio`](#radio) for small mutually-exclusive sets,
[`notebook`](#notebook) for tabbed mode switches.
### `tabs`
A bare tab bar — visual selection without owning the page content.
Use this when you want full control over what each tab shows.
**When**: paged settings screens where the panes are large and you
want to keep them as siblings in the tree, not as
[`notebook`](#notebook) children.
```rust,no_run
# use ltk::{ tabs, TabBar };
# #[ derive( Clone ) ] enum Msg { TabChanged( usize ) }
# fn _ex( active: usize ) -> TabBar<Msg> {
tabs::<Msg, _, _>( [ "Profile", "Network", "Privacy" ] )
.selected( active )
.on_select( Msg::TabChanged )
# }
```
**See also**: [`notebook`](#notebook) for tabs that own their pages.
### `notebook`
Tabbed container. Owns the pages and shows only the active one.
**When**: settings dialogs, multi-step forms, anywhere a flat
[`tabs`](#tabs) bar over hand-managed pages would be more code than
benefit.
```rust,no_run
# use ltk::{ notebook, text, Notebook };
# #[ derive( Clone ) ] enum Msg { TabChanged( usize ) }
# fn _ex( active: usize ) -> Notebook<Msg> {
notebook::<Msg>()
.page( "General", text( "general body" ) )
.page( "Advanced", text( "advanced body" ) )
.selected( active )
.on_select( Msg::TabChanged )
# }
```
**See also**: [`tabs`](#tabs) for a bare tab bar without page
ownership.
### `dialog`
Modal or non-modal centered confirmation card with a built-in scrim,
optional title, subtitle, custom body, and a right-aligned action
row. Pressing `Esc` fires the configured `cancel` message.
**When**: destructive confirmations ("Delete file?"), guided pickers
that block other interaction until resolved, "are you sure" gates
before a long-running operation.
```rust,no_run
# use ltk::{ button, dialog, ButtonVariant, Element };
# #[ derive( Clone ) ] enum Msg { Cancel, Confirm }
# fn _ex() -> Element<Msg> {
dialog()
.title( "Delete partition?" )
.subtitle( "This will erase every file on /dev/sda2." )
.cancel( Msg::Cancel )
.action( button::<Msg>( "Cancel" ).variant( ButtonVariant::Tertiary ).on_press( Msg::Cancel ) )
.action( button::<Msg>( "Delete" ).variant( ButtonVariant::Primary ).on_press( Msg::Confirm ) )
.into()
# }
```
`modal( false ) + dismiss_on_scrim( msg )` makes a tap on the dim
background fire `msg`; combining `dismiss_on_scrim` with `modal( true )`
panics when the dialog is converted to an `Element` — the `.into()` asserts with "dialog: dismiss_on_scrim is not valid when modal=true", since the two contracts contradict. `body( elem )`
swaps a custom element in between the subtitle and the action row —
the example app at `examples/dialog.rs` uses this for an in-dialog
slider. `max_width( … )` caps the card width; it accepts any `Length`
and defaults to `Length::fluid( 480.0 )` so the card scales with the
same curve as the stock buttons inside it.
**See also**: [`toast`](#toast) for non-blocking transient
notifications, [`combo`](#combo) for a single-pick popup that does
not require modal blocking.
---
## Pickers
### `date_picker`
Calendar-grid date selector with month / year navigation.
**When**: birthdays, deadlines, any single-date input.
```rust,no_run
# use ltk::{ date_picker, Date, DatePicker };
# #[ derive( Clone ) ] enum Msg { Picked( Date ) }
# fn _ex() -> DatePicker<Msg> {
date_picker( Date::new( 2026, 5, 7 ) ).on_change( Msg::Picked )
# }
```
### `time_picker`
Hour / minute selector. Wheel-style scrubbing on touch.
**When**: alarms, scheduling, time-of-day input.
```rust,no_run
# use ltk::{ time_picker, Time, TimePicker };
# #[ derive( Clone ) ] enum Msg { Picked( Time ) }
# fn _ex( now: Time ) -> TimePicker<Msg> {
time_picker( now ).on_change( Msg::Picked )
# }
```
### `color_picker`
Hue + saturation/value picker with a hex/RGB readout.
**When**: theming UIs, paint tools, accent customisation.
```rust,no_run
# use ltk::{ color_picker, Color, ColorPicker };
# #[ derive( Clone ) ] enum Msg { Picked( Color ) }
# fn _ex( current: Color ) -> ColorPicker<Msg> {
color_picker( current ).on_change( Msg::Picked )
# }
```
---
## Layouts
### `column`
Vertical flow. Children stack top-to-bottom with optional padding,
spacing, alignment.
```rust,no_run
# use ltk::{ button, column, spacer, text, Element };
# #[ derive( Clone ) ] enum Msg { Ok }
# fn _ex() -> Element<Msg> {
column()
.padding( 24.0 )
.spacing( 12.0 )
.push( text( "Title" ) )
.push( spacer() )
.push( button( "OK" ).on_press( Msg::Ok ) )
.into()
# }
```
`max_width(px)` caps the inner content width; `fit_content()` reports
the natural content width to the parent (otherwise the column claims
the full available width). `center_y( true )` centres the content
block vertically when there are no spacers.
Watch the defaults: `column()` starts with **16 px padding** on every
side (and 8 px spacing). A column nested inside an already-padded
container silently doubles the inset — pass `.padding( 0.0 )` when the
parent owns the margin.
### `row`
Horizontal flow. Mirror of [`column`](#column) on the X axis — except
its default padding is `0`, not `column`'s 16 px.
```rust,no_run
# use ltk::{ flex, row, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex(
# label: Element<Msg>,
# field: Element<Msg>,
# submit_button: Element<Msg>,
# ) -> Element<Msg> {
row()
.spacing( 8.0 )
.push( label )
.push( flex( field ) )
.push( submit_button )
.into()
# }
```
### `stack`
Z-order overlay. Each child gets explicit horizontal and vertical
alignment (`HAlign` / `VAlign`) plus an optional margin and pixel
translation. Children are drawn in declaration order — the last child
sits on top.
```rust,no_run
# use std::sync::Arc;
# use ltk::{ button, column, img_widget, stack, text, Element, HAlign, VAlign };
# #[ derive( Clone ) ] enum Msg { Add }
# fn _ex( background: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
stack()
.push( img_widget( background, w, h ) )
.push_aligned(
column().push( text( "Heading" ) ),
HAlign::Center, VAlign::Center,
)
.push_aligned_margin(
button( "+" ).on_press( Msg::Add ),
HAlign::End, VAlign::Bottom,
16.0,
)
.into()
# }
```
**See also**: [`column`](#column) and [`row`](#row) for flow layout.
### `grid`
Fixed-column-count grid that wraps its children into rows.
**When**: icon launchers, photo galleries, app drawers.
```rust,no_run
# use std::sync::Arc;
# use ltk::{ grid, icon_button, Element };
# #[ derive( Clone ) ] enum Msg { OpenApp1, OpenApp2 }
# fn _ex( app1_rgba: Arc<Vec<u8>>, app2_rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
grid( 4 )
.padding( 16.0 )
.spacing( 12.0 )
.push( icon_button( app1_rgba, w, h ).on_press( Msg::OpenApp1 ) )
.push( icon_button( app2_rgba, w, h ).on_press( Msg::OpenApp2 ) )
// ...
.into()
# }
```
Wrap inside [`scroll`](#scroll) when the grid may overflow.
`centre_last_row( true )` shifts a partial last row so its tiles sit
centred under the full rows above instead of left-aligned — useful for
app switchers and gallery layouts where a 7-of-9 leftover band reads
better balanced.
`grid_min_cell( width )` is the adaptive variant: instead of a fixed
column count, it fits as many columns as the available width allows
while keeping every cell at least `width` wide (never fewer than one),
re-deriving the count on every layout — so the same grid shows more
columns on a wide window and fewer on a phone. `width` accepts any
`Length`; `max_columns( n )` caps the derived count so cells grow
instead of multiplying on very wide surfaces.
### `spacer`
An invisible flexible filler. Inside a column / row, absorbs leftover
space along the parent's main axis.
```rust,no_run
# use ltk::{ column, spacer, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex( header: Element<Msg>, footer: Element<Msg> ) -> Element<Msg> {
column()
.push( header )
.push( spacer() ) // pushes footer to the bottom
.push( footer )
.into()
# }
```
`weight( n )` sets the relative share; `height( px )` / `width( px )`
pin the spacer to a fixed size on its respective axis.
**See also**: [`flex`](#flex) for a non-empty filler.