Text entry on a phone comes from `zwp_text_input_v3`: a focused field enables the protocol and the compositor's input method, squeekboard today, draws a keyboard and commits text through it. Eydos wants to drop that process, so the keyboard has to come from somewhere else, and the natural place is the toolkit that already draws everything else and knows which field has the focus and what it is for. This commit puts one in ltk, as data files plus a panel any host can drive: ltk's runtime shows it under a focused text field when no input method is there, and forge will draw the same panel in its chrome and deliver the keys to the focused client, which is the second half and is not in this commit. Along the way the demo that exercises it exposed two gaps in the widgets, fixed here too.
Layouts are data. `keyboards/<language>.json`, one file per layout, searched in `LTK_KEYBOARDS_DIR` when set, then `$XDG_DATA_HOME/ltk/keyboards` or `~/.local/share/ltk/keyboards`, then `/usr/share/ltk/keyboards`; `osk::layout::search_paths()`, `available()` and `load( name )` are the API, and the English layout is compiled in as the last resort. A file has `name`, a `label` for the layout-switch key, `layers` and `variants`. A layer is a list of rows; a row a list of keys; a key is a bare string for the text it types, or an object with an `action` (`shift`, `backspace`, `enter`, `space`, `layer`, `layout`, `hide`), the target `layer`, a `label`, and a `width` in key units, `1` by default, which share the row's width so a row of nine keys comes out centred under one of ten. `base` is mandatory; a missing `shift` layer is derived from it by uppercasing. `variants` maps a key's text to what a long press offers, and the shifted form of a key uses its base's, uppercased. A key may also be a named key, `{ "key": "Escape" }`, `Tab`, the arrows, `Home`, `End`, `Page_Up`, `Page_Down`, `Delete`, `Insert`, `F1` to `F12`, or a sticky modifier, `{ "modifier": "ctrl" }` or `alt`. `en.json` and `es.json` ship, with symbol layers and the accented variants each language needs. The files go into the registry copy through `make install` and into `ltk-theme-default` under `/usr/share/ltk/keyboards`.
The terminal row. A terminal needs Escape, Tab, Ctrl, Alt, the arrows and the shell's symbols, and no other application does, so those are not a layout but a row laid above any language's layout: `keyboards/bar-terminal.json`, made of the named keys and modifiers above plus `-`, `/`, `|`, `\` and `~` with their variants. Files named `bar-*.json` are rows, not layouts, and `available()` leaves them out, so the layout-switch key does not cycle onto one; the first version listed it and a tap on that key made the keyboard vanish behind a duplicated bar. Which applications get the row is decided by what the field says it is for: `TextEdit::purpose( InputPurpose::Terminal )`, snapshotted into the handlers like `secure`, which the built-in panel reads through `set_purpose` and which also goes out as the `terminal` content purpose of `zwp_text_input_v3`, the same one VTE-based terminals declare, so a compositor hosting the panel can pick the row from the protocol; for a client that declares nothing it can decide by `app_id`.
`osk::Panel` is a state machine a host drives. It holds the loaded layouts, the current layer, shift as off, one-shot or locked, the sticky modifiers, the purpose and its extra row, the geometry of all of it inside the rectangle the host gave it with `set_rect( rect, scale )`, and the press in flight. The host feeds it `press_down`, `press_move`, `press_up` and, from its own timer, `tick`, each returning an `Event`: `Text`, `Backspace`, `Enter`, `Hide`, `Key( keysym, modifiers )` for what text cannot carry, an arrow, Escape, Tab, or a character while Ctrl or Alt is held, or `Changed` when only the panel's look changed. A key types on release; a press held 450 ms opens its variants, which the finger slides onto and the release picks; backspace fires on press and repeats every 60 ms after the same delay; shift is one-shot and locks on a double tap within 350 ms; a modifier stays down for the next key and a character typed under it goes out as a `Key`, since text cannot carry Ctrl. `next_deadline()` says when `tick` next has work, `height( scale )` what the panel needs, `press_down` answers `None` for a press outside it so the host treats that as its own. `draw( canvas )` paints it with the active palette; the action keys show theme icons rather than glyphs, `keyboard/shift`, `keyboard/backspace`, `keyboard/enter`, `general/down-simple` for hide, `accessibility/languages` for the layout key, the `general` arrows for the terminal row, rasterised once per size and tint, because the first version drew `⇧` and `🌐` and the fallback chain has no glyph for the latter, which showed as a question mark; a layout key with no label of its own shows the active layout's. Nothing in `Panel` knows about text fields or Wayland, which is what lets a compositor host it. Fifteen tests cover the layout parser, the derived shift layer, variants following case, the bar filtering, and the panel's taps, one-shot and locked shift, variants, repeat, layer and layout switches and the terminal row.
The runtime shows it. `event_loop/osk.rs` puts a `Panel` on a surface when a text field takes the focus and `builtin_osk` holds: the compositor advertised no `zwp_text_input_manager_v3`, or `LTK_BUILTIN_OSK=1` is set, which is how to try it on a desktop that has an input method, or the app returns `true` from the new `App::builtin_keyboard()`. The panel is placed at the bottom of the surface by both renderers, and the widget tree is laid out in what is left above it, so the focused field is never covered. Presses, motion and releases from the pointer and from touch are offered to the panel before the gesture machine, a press on the already focused field brings a hidden panel back, and the run loop's timeout now takes the minimum of the long-press, tooltip and panel wakeups so variants and repeat fire on time. `Text` goes through the same insertion as a physical key, `Key` through the same dispatch, with Ctrl applied for its duration, `Enter` submits or inserts a newline in a multiline field, `Hide` keeps the focus and takes the panel away; losing the focus removes it. The layout is picked from `$LANG`. `ltk::InputPurpose` is exported, and `docs/architecture.md` gains the section that says all of this, with the compositor's part sketched.
The demo. `examples/inputs` becomes a notebook with the login page it had and a Shell page: one multiline field that holds a transcript, prompt included, declared a terminal, so the row appears; Enter runs the line under the last prompt (`echo`, `ls`, `pwd`, `clear`), the arrows move through the transcript, and a Ctrl combination interrupts the line the way a terminal shows it. Escape quits only from the login page, because a key the app answers never reaches the field, which is what first made the arrows dead.
Two widget gaps the demo found. `Notebook` gave its page the page's preferred height, so a page that fills its space, a scrolled list or a flex, got none and showed nothing: the active page is now wrapped in `flex` and gets the height under the strip. And the multiline field had no way to scroll: it chose its first visible line from the cursor alone on every draw, so the transcript ran under the keyboard and the wheel did nothing. `text_edit/scroll.rs` gives it a `TextScroll` the runtime keeps per field beside the cursor and the selection, in the frame state, the draw context and the `core` runtime alike: the first line asked for by hand and the cursor at that moment. The view holds there while the cursor stays put and, once it moves, follows it by the least it has to, as an editor does; without a request the result is exactly the old rule. `Window::resolve` computes the visible lines once for drawing, hit testing and the wheel, so they cannot disagree, and replaces the copy each of the first two had. In `on_pointer_axis` the vertical motion is offered to the multiline field under the pointer first, three lines per notch from `value120` or `discrete`, touchpad pixels accumulated until they add up to a line, and only when the field cannot move that way does it go to a `scroll` around it. `TextScroll` is public but opaque, since `Element::draw` is public and needs it; `Default` is the cursor-following behaviour. Six tests cover the hold, the follow, the clamping, the notch and the accumulation. A read-only field is not interactive and does not take the wheel; touch drags inside a field do not scroll it yet.
Not done. Forge does not host the panel yet, so on the phone squeekboard is still what appears. The panel's look is a first pass, sized in logical units with the theme's palette, to be restyled from mockups. Only `en` and `es` ship.
1116 lines
40 KiB
Markdown
1116 lines
40 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) · [`pattern_grid`](#pattern_grid)
|
||
- [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()
|
||
# }
|
||
```
|
||
|
||
`icon_size( n )` sets the drawn size. A bare number is an absolute pixel
|
||
count; pass `Length::widget( n )` instead to follow the widget-scaling
|
||
mode, which is what stock icons (the [`list_item`](#list_item) disclosure
|
||
arrow) already do — use it when the two sit side by side and should match.
|
||
|
||
Give it a `width( … )` or `height( … )` and it becomes a framed key: it
|
||
claims that size, paints the background and border of its `variant` like
|
||
a text [`button`](#button), and centres the icon inside at `icon_size`.
|
||
That is how a keypad mixes digit keys with an icon key of the same shape:
|
||
|
||
```rust,no_run
|
||
# use std::sync::Arc;
|
||
# use ltk::{ icon_button, ButtonVariant, Element };
|
||
# #[ derive( Clone ) ] enum Msg { Backspace }
|
||
# fn _ex( rgba_bytes: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
|
||
icon_button( rgba_bytes, w, h )
|
||
.variant( ButtonVariant::Secondary )
|
||
.width( 64.0 ).height( 64.0 )
|
||
.icon_size( 28.0 )
|
||
.on_press( Msg::Backspace )
|
||
.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 ) )
|
||
# }}
|
||
```
|
||
|
||
`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`](#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.
|
||
|
||
### `pattern_grid`
|
||
|
||
A 3×3 grid of dots for drawing an unlock pattern with a finger or the
|
||
pointer. The widget is stateless: the app owns the list of visited nodes
|
||
(0 to 8, row-major) and passes it back in for drawing.
|
||
|
||
**When**: lock screens and enrolment forms for a pattern secret.
|
||
|
||
```rust,no_run
|
||
# use ltk::{ pattern_grid, PatternGrid };
|
||
# #[ derive( Clone ) ] enum Msg { Node( u8 ), Released }
|
||
# struct App { path: Vec<u8> }
|
||
# impl App { fn _ex( &self ) -> PatternGrid<Msg> {
|
||
pattern_grid( self.path.clone() )
|
||
.on_node( Msg::Node )
|
||
.on_release( Msg::Released )
|
||
# }}
|
||
```
|
||
|
||
`on_node( f )` fires with a node index when a press or a drag enters a
|
||
node, once per entry rather than once per motion event, including nodes
|
||
already in the path — appending, skipping a repeat, or taking in the node
|
||
a straight stroke crosses is the app's rule, not the widget's. `on_release( msg )` fires when the finger lifts.
|
||
`hide_stroke( true )` draws only the dots, against shoulder surfing;
|
||
`error( true )` paints the current path in the palette's `danger` colour.
|
||
`size( len )` sets the side of the square grid (any `Length`); without it the
|
||
theme's fluid default applies, capped by the width the parent allocates.
|
||
|
||
It sizes itself square to `min( max_width, 264 )` design pixels; wrap it
|
||
in a `container( … ).max_width( … )` to pin it.
|
||
|
||
**See also**: [`slider`](#slider), which owns the pointer during a drag
|
||
the same way.
|
||
|
||
---
|
||
|
||
## 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 )`. A multiline field whose text overflows scrolls with the wheel or a touchpad, three lines per notch, and takes the motion ahead of any `scroll` around it while it can still move that way; once the cursor moves again the view follows it, by the least it has to.
|
||
|
||
**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() )
|
||
# }}
|
||
```
|
||
|
||
`color( c )` sets the colour of the value (the placeholder keeps the theme's), for a field dropped onto a surface the theme does not know about. `on_focus( msg )` emits `msg` when the field takes the keyboard focus — from a press, Tab or `App::take_focus_request` — and not again for a press on the field that already has it; `App::on_text_input_focused` still reports whether *some* text field is focused, `on_focus` says *which*.
|
||
|
||
`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.
|
||
|
||
`purpose( InputPurpose::Terminal )` declares what the field is for: the compositor's input method is told through the text-input content type, and ltk's own on-screen keyboard, when it is the one shown, adds its row of Escape, Tab, Ctrl, Alt and arrows above the layout. The `inputs` example's Shell page is the reference; see [On-screen keyboard](./architecture.md#on-screen-keyboard).
|
||
|
||
**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 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.
|
||
|
||
Without `fade_bottom` the clip is a plain scissor (a mask on the software backend) on the parent canvas: no offscreen pass, no blit, and the child's images and glyphs stay in the parent's caches. A viewport whose height changes every frame — a panel sliding in — therefore costs no more than drawing its child. With `fade_bottom` the child renders into a sub-canvas that is kept across frames and resized in place when the viewport's size changes.
|
||
|
||
**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()
|
||
# }
|
||
```
|
||
|
||
When the children do not fit, the row takes the shortfall off the widest
|
||
ones first, so a long label gives up its width while a narrow neighbour
|
||
(an icon button beside a title) keeps its size, and equal siblings share
|
||
it evenly. Leaves that paint a line of text (`button`, `list_item`)
|
||
truncate with an ellipsis into what they are given. Pinned spacers keep
|
||
their width — an explicit gap is a decision, not slack. Use
|
||
`no_shrink()` for strips that are meant to be wider than their viewport
|
||
and scrolled.
|
||
|
||
### `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.
|