Files
ltk/docs/widgets.md
Pedro M. de Echanove Pasquin 530a5696b9
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
event_loop, slider, list_item: overlay exclusive zones follow the surface, on_release for sliders, list labels elide against the trailing slot
Exclusive zones (event_loop/overlays_reconcile.rs, event_loop/surface.rs). `OverlaySpec::size` is documented as physical pixels and converted to logical for `layer_surface.set_size` by dividing by the parent's integer scale; `exclusive_zone` sat right beside it in the same `LayerConfig` and was passed through raw, so at scale 2 the reserved band was expressed in a unit twice as coarse as the surface it is meant to match. Worse, `set_exclusive_zone` appeared exactly once in the whole crate — at materialize time — while the reconcile loop propagated later size changes through `last_requested_size`, so an overlay whose size kept being recomputed carried a reservation frozen at whatever the first frame produced. Crustace's dock is the visible case: it derives both numbers from the same `desktop_pill_height`, and with the zone stuck at the density-1 value (icon at its 40 px floor, 40 × 1.70 = 68 logical) while the surface grew to 87, a maximized window overlapped the top fifth of the dock — measured on a 1.75 output as 151 physical px of painted dock against a 120 px reserved band. The zone now goes through the same divisor as the size, with `-1` (ignore other zones) and `0` (reserve nothing) passing through untouched as the sentinels they are, and `SurfaceState` tracks `last_requested_zone` so the reconcile loop re-sends it whenever the spec moves, committing once for both.
Slider::on_release / VSlider::on_release (widget/slider, widget/vslider, widget/handlers.rs, widget/element.rs, input/gesture). Sliders only had `on_change`, which fires on every motion event, so an app whose commit is expensive — a subprocess, a D-Bus round trip, a compositor reconfigure — paid for it per pixel of travel: dragging the text-scale slider in Eydos Settings wrote `gsettings` once per motion and swept the whole desktop through a repaint each time. The new builder fires once with the final value when the drag ends, leaving `on_change` to move the thumb and nothing else; `on_change` alone behaves exactly as before, and the mapped variant propagates through `map_msg` like its sibling. The handler snapshot carries the callback next to `on_change` and exposes `slider_release_msg`; the gesture machine's slider branch of `on_release`, which previously returned an empty event list, now resolves the widget through `find_widget`, recomputes the value from the release position with the same `slider_value_from_pos` the drag path uses, and pushes `ReleaseEvent::PushMsg` — the variant whose documentation already described "button press or final slider value on release". A unit test covers the new emission; the existing one asserting an empty release still holds, because it passes an empty widget list and the lookup finds nothing.
ListItem elision (widget/list_item/mod.rs). The label and subtitle were painted with `draw_text` and no width budget, so a row title longer than its width ran under the trailing text or the disclosure icon instead of truncating. The trailing slots are now measured and positioned before the text is painted — their extent is what decides how much room the label has — and both lines are elided with an ellipsis against the space left over, accumulating per-character widths the way `Text` already did, with one icon gap kept between the text and whatever follows it. `Text` keeps its own inline copy of that algorithm for now; folding the two into a single crate-internal helper is the natural follow-up, and a precondition for teaching `Button` to elide once `Row` learns to distribute a width deficit instead of only leftover space.
2026-08-04 18:44:21 +02:00

35 KiB

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 and docs/architecture.md first. For copy-pasteable patterns built from these widgets, see docs/cookbook.md.

Table of contents


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.

# 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 for activation on a richer custom-shaped surface, icon_button for image-only buttons.

icon_button

A button whose visual is an RGBA bitmap instead of text. Same dispatch shape as button.

When: toolbar icons, system tray glyphs, anywhere the visual is icon-first.

# 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, window_button (specialised icon button for window decorations).

pressable

Wraps any Element 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.

# 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 when a plain text button is enough, 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.

# 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.

# 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 for free-form tappable rows, 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").

# 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 for less prominent opt-ins, 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.

# 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, 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.

# 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, 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.

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

on_release( f ) fires once with the final value when the drag ends. Pair it with on_change — which keeps moving the thumb — when the commit is expensive (a subprocess, a D-Bus round trip, a compositor reconfigure): a sweep would otherwise pay for it on every motion event. vslider carries the same builder.

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 for the vertical axis, progress_bar for read-only progress display.

vslider

A vertical slider. Same value model as slider0.0 at the bottom, 1.0 at the top.

When: column-shaped equalisers, compact volume / brightness picks inside narrow side panels.

# 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.

progress_bar

A read-only linear progress indicator.

When: determinate operations with a known fraction (downloads, file copies, install steps).

# use ltk::{ progress_bar, ProgressBar };
# struct App { download_fraction: f32 }
# impl App { fn _ex( &self ) -> ProgressBar {
progress_bar( self.download_fraction )
# }}

See also: 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.

# 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 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.

# 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.

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.

# 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.

# 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's max_width, which caps the content width while the column still claims the full available rect (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 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.

# 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.

# 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 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.

# 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.


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.

# 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 for passive clipping without gestures, 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).

# 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 Lengths inside resolve exactly as they would outside the clip — the right behaviour for scroll-like clipping. local_viewport() opts out: the child resolves those Lengths 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.

flex

A filler wrapper for both flow layouts. Treats its non-spacer child like a spacer for leftover-space distribution but draws the child inside the allocated rect: leftover width inside a row, leftover height inside a 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).

# 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 for invisible fillers, column and row.

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.

# 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 shows Prev / Next buttons and arrow-key navigation against an external offset state.

See also: scroll for free vertical / horizontal panning of arbitrary content; 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").

# 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 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.

# 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 for hover-anchored hints.

tooltip

Anchored hint rendered below a target widget. Like 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.

# 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 for the full overlay wiring.

See also: 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, 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 buttons (autocomplete, country list, theme picker).

# 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 for small mutually-exclusive sets, 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 children.

# 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 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 bar over hand-managed pages would be more code than benefit.

# 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 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.

# 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 for non-blocking transient notifications, 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.

# 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.

# 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.

# 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.

# 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 on the X axis — except its default padding is 0, not column's 16 px.

# 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.

# 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 and row for flow layout.

grid

Fixed-column-count grid that wraps its children into rows.

When: icon launchers, photo galleries, app drawers.

# 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 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.

# 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 for a non-empty filler.