Files
ltk/docs/widgets.md
Pedro M. de Echanove Pasquin d4d7ee742e
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes
Rendering parity (software ↔ GLES). The software backend now rounds glyph pen positions and image destinations to the nearest integer pixel, matching what the GLES backend already did; previously it truncated, so text and 1:1 images could land up to half a pixel off between the two backends and the bilinear sample read ~1 px softer than the source. Gradients and shadows are deliberately left unimplemented on the software backend, and the GLES multi-rect `glScissor` clip is left coarse on purpose: making it exact would need stencil bits the EGL config does not carry, or routing the partial-redraw path through the offscreen clip layer, which would break its `fill` / `clear_rects_transparent` scissor semantics. Adds software-backend pixel tests covering the snapping.
Font resolution unification. The system-font candidate chain, `find_font_opt` and `load_default_font_bytes` lived in two copies (`render/helpers` and `gles_render/helpers`) that had already diverged — one resolved through `find_font_opt`, the other inlined the candidate loop — and now live once in `system_fonts`. The two per-backend `OnceLock` default-font caches and `primary_handle` collapse into a single `system_fonts::default_handle`. Module docs and the `font_registry` caller are updated accordingly.
Shared image validation and rect inflation. `draw_image_data`'s dimension check and its one-line warning were byte-duplicated across both backends and are now `render::helpers::validate_rgba_dims`. The six manual symmetric `Rect`-inflate literals in the GLES primitives reuse the existing `Rect::expand`.
FrameState and DrawCtx de-duplication. The eleven `SurfaceState` fields the draw pass owns and threads through `DrawCtx` — `widget_rects`, the cursor / selection maps, the scroll state, `accessible_extras`, `prev_focused` / `prev_hovered` / `prev_pressed` — move into a `FrameState` sub-struct. The per-frame `build_draw_ctx` / `commit_draw_ctx` helpers can then borrow `&mut ss.frame`, disjoint from `ss.canvas` and `ss.pool`, so the four frame paths (software / GLES × full / partial) replace their duplicated `DrawCtx` construction and write-back with a single helper call each. A whole-`SurfaceState` borrow could not express this (partial borrows do not cross function boundaries), which is why the helpers take the sub-struct. `content_dirty` stays on `SurfaceState` — it is an invalidation flag, not frame state — and is reset at the call site.
rich_text tests. Adds the previously-missing `tests.rs` for the `RichText` widget: one hit rect per visual line a link spans (the widget's core invariant), the single-line and no-link cases, preferred-size growth with hard line breaks, and `map_msg` range preservation — all headless against a software `Canvas`, with line counts forced by `\n` so they do not depend on any system font's measured width.
Documentation. Fills the rustdoc gaps on the embedder-facing surface: `core::UiSurface` accessors, the `egl_context` public API, the `GlesCanvas` methods, `theme::typography` and `theme::error`, and the `RichText` / `Text` builders; adds a crate-level "Rendering backends" overview. `CHANGELOG.md` is added (0.2.0 / 0.1.0), and `docs/widgets.md` / `docs/cookbook.md` gain `rich_text`, `external`, and the CPU-draw / path-clip / externally-laid-out-tree recipes. `debian/changelog` gets the 0.2.0-1 entry. Private intra-doc links to `system_fonts` are demoted to code spans so `cargo doc` is warning-free.
Packaging. The `libltk-dev` registry crate shipped a `Cargo.toml` declaring the `lookup` bench while the `Makefile` install copied only `src/`, so Cargo refused to parse the manifest over a missing `benches/lookup.rs`; the install now ships `benches/` as well (the file alone satisfies the parse — criterion is a dev-dependency and is not resolved when the crate is consumed as a library). `Cargo.toml` is bumped to 0.2.0 to match the package version and the `ltk-0.2.0` registry directory.
2026-06-25 12:43:40 +02:00

966 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
**When**: any place a normal app would have a "Save" / "Cancel" / "Send"
control.
```rust,no_run
# use ltk::{ button, Element };
# #[ derive( Clone ) ] enum Msg { Save }
# fn _ex() -> Element<Msg> {
button( "Save" ).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**: [`crate::theme_window_controls`] for the colour tokens
each mode supplies.
### `list_item`
A row with a primary label, optional subtitle, optional right-aligned
trailing text, and a tappable surface.
**When**: settings menus, navigation lists, contact rows. Doubles its
height when a subtitle is set.
```rust,no_run
# use ltk::{ list_item, ListItem };
# #[ derive( Clone ) ] enum Msg { OpenWifi }
# fn _ex() -> ListItem<Msg> {
list_item( "Wi-Fi" )
.subtitle( "Eduroam" )
.trailing( "" )
.on_press( Msg::OpenWifi )
# }
```
**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 single-line label. Truncates with an ellipsis when wider than its
allocated rect.
**When**: titles, captions, anything non-interactive.
```rust,no_run
# use ltk::{ text, Color, Element };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex() -> ( Element<Msg>, Element<Msg> ) {
let title = text( "Title" ).size( 24.0 ).color( Color::WHITE );
let centred = text( "Centred" ).align_center();
# ( title.into(), centred.into() )
# }
```
**See also**: [`text_edit`](#text_edit) for editable text.
### `text_edit`
A single-line 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.
**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. Mirrors the same flag on
[`column`](#column) and [`row`](#row), so a single decorated child no
longer needs a `column`-of-one wrap just to access a width cap.
**See also**: [`pressable`](#pressable) wrapping a `container` makes a
card interactive.
### `separator`
A horizontal divider line with theme-default colour and 1 px thickness.
**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 vertically-scrollable viewport. 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()
# }
```
**See also**: the slide-in panel recipe in
[`docs/cookbook.md`](./cookbook.md#slide-in-panel).
### `flex`
A row-only filler wrapper. Treats its non-spacer child like a
[`spacer`](#spacer) for leftover-width distribution but draws the child
inside the allocated rect.
**When**: a row where one non-trivial child should fill the remaining
width (a card next to a fixed-size icon).
```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. Animates while in the tree.
**When**: long-running operations with no known fraction
(network calls, indexing, "waiting for compositor").
```rust,no_run
# use ltk::{ spinner, Spinner };
# fn _ex() -> Spinner {
spinner().size( 24.0 )
# }
```
**See also**: [`progress_bar`](#progress_bar) for determinate progress.
### `toast`
Transient notification that floats over the surface and dismisses
itself after a timeout.
**When**: confirmation snackbars ("Saved"), non-blocking errors,
status flashes.
```rust,no_run
# use ltk::{ toast, Toast };
# #[ derive( Clone ) ] enum Msg {}
# fn _ex() -> Toast<Msg> {
toast( "Saved" ).duration( 3.0 )
# }
```
**See also**: [`tooltip`](#tooltip) for hover-anchored hints.
### `tooltip`
Anchored hint that appears next to a target widget on hover or focus.
**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 widget paints next to the widget that registered the matching
`WidgetId`. See [`docs/cookbook.md`](./cookbook.md) for the full
overlay wiring.
**See also**: [`toast`](#toast) for transient self-dismissing
notifications.
### `combo`
Editable single-select with a popup list. Supports type-to-filter
and free-text entry.
**When**: pick-one fields with too many options for a row of
[`radio`](#radio) buttons but where a free typed answer is also
valid (autocomplete, country list, theme picker).
```rust,no_run
# use ltk::{ combo, Combo, ComboState };
# #[ derive( Clone ) ] enum Msg { Pick( usize ) }
# fn _ex( state: ComboState ) -> Combo<Msg> {
let items = vec![
"One".to_string(),
"Two".to_string(),
"Three".to_string(),
];
combo( state, items ).on_select_idx( Msg::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 at lower time (the 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.
**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.
### `row`
Horizontal flow. Mirror of [`column`](#column) on the X axis.
```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.
### `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.