# 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) · [`anchored`](#anchored) · [`combo`](#combo) · [`tabs`](#tabs) · [`notebook`](#notebook) · [`tab_strip`](#tab_strip) · [`dialog`](#dialog) - [Pickers](#pickers) - [`date_picker`](#date_picker) · [`time_picker`](#time_picker) · [`color_picker`](#color_picker) · [`file_chooser`](#file_chooser) · [`print_dialog`](#print_dialog) - [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`, 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 { 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>, w: u32, h: u32 ) -> Element { 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. The bitmap keeps its proportions: one that is not square is scaled to fit that size and centred in it, never stretched. 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>, w: u32, h: u32 ) -> Element { 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, # title: Element, # subtitle: Element, # ) -> Pressable { pressable( container( row() .push( icon ) .push( column().push( title ).push( subtitle ) ) ) .surface( "surface-card" ) ) .on_press( Msg::OpenWifiPicker ) # } ``` A tap and a long press go to the deepest interactive widget under them, and a widget is interactive even without a handler: a `list_item` with no `on_press` inside a `pressable` takes the tap and the right click and does nothing with them, so the pressable never fires. Build the content from pieces that take no input (`container`, `row`, `column`, `text`, `img_widget`); keep an inner `button` only when it should win over the pressable, as a row's own trailing action does. **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, Element ) { 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 { 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. `ink( label, secondary )` draws the label in `label`, the subtitle and trailing text in `secondary`, and tints hover and press with `label`, instead of the theme's colours, for a row laid on a surface the theme does not know about, such as a menu drawn in an app's own colours. `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`. 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. `icon_size( len )` overrides the leading icon's size (24 design px). `ink( label, secondary )` draws the label in `label` and the subtitle and trailing text in `secondary`, and tints hover and press with `label`, in place of the theme's colours: for rows on a surface of the app's own, such as a menu in a fixed colour. **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 { 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 { 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 { 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 { 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 { 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 } # impl App { fn _ex( &self ) -> PatternGrid { 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, Element, Element ) { 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. Such a field shows a scroll bar along its right edge when its lines do not all fit; Page Up and Page Down move its cursor a screen at a time. As an editor, `highlights( runs )` colours runs of its text, `tab_text( "\t" )` has Tab type a tab, which reaches the next stop every four spaces, `line_numbers( true )` numbers its lines in a column at its left, and `on_cursor( … )` reports where its cursor is. **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, Element ) { 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 { 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, subtitle: Element ) -> Element { 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`; 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 { 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>, width: u32, height: u32 ) -> Element { 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. - `External::new( w, h, ExternalSource::Frame( … ) )` — the same, but the closure also returns the texture's size and ltk draws it at that size from the top left corner, cut to the rect, instead of stretching it: a web page whose frame lags behind a window resize is not distorted. **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 { 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 { scroll( column() .push( list_item::( "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, panel_height: f32 ) -> Element { 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, # title: Element, # subtitle: Element, # ) -> Element { 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 { carousel() .focused_width_frac( 0.8 ) .gap( 16.0 ) .offset( offset ) .push( container( button::( "Tile 1" ).on_press( Msg::Open( 0 ) ) ) .background( Color::rgb( 0.95, 0.4, 0.4 ) ) .radius( Corners::all( 12.0 ) ) ) .push( container( button::( "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 } # impl App { fn overlays( &self ) -> Vec> { 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 { 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. The hint `Button::tooltip` shows by itself is placed by the runtime. When the main surface is an xdg-shell window it is an xdg-popup child of the window, anchored to the button's rect: the compositor puts it above the button, 6 logical pixels away, flips it below or slides it sideways when it does not fit, and it takes no grab and no input, so the pointer stays with the button. A window does not know where it sits on screen, so this is the only way the hint lands beside the button. On a layer-shell surface (a shell, a panel) the hint is an overlay spanning the output, placed above the button or below it near the top edge. **See also**: [`toast`](#toast) for transient self-dismissing notifications. --- ### `anchored` Draws its child right under another widget, the one built with the matching `.id( WidgetId )`, as that widget was laid out on the previous frame: a menu under the button that opens it, inside the window. `align` lines the child up with the anchor's left edge (`HAlign::Start`, the default), its right edge (`HAlign::End`) or its middle (`HAlign::Center`), and the child is kept within the rect its parent gives it; `gap` is the space under the anchor, in physical pixels. Until the anchor has been drawn once the child fills its parent's rect. **When**: a menu or a panel that drops from a button of a bar, laid over the page in a [`stack`](#stack), usually over a scrim that closes it. For a popup that may leave the window, return an `OverlaySpec` with `anchor_widget_id` from `App::overlays` instead. ```rust,no_run # use ltk::{ anchored, stack, Element, HAlign, WidgetId }; # #[ derive( Clone ) ] enum Msg {} # fn _ex( page: Element, menu: Element ) -> Element { stack() .push( page ) .push( anchored( menu, WidgetId( "menu-button" ) ).align( HAlign::End ).gap( 4.0 ) ) .into() # } ``` **See also**: [`tooltip`](#tooltip) and [`combo`](#combo), which anchor overlays to a widget the same way. ### `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 { # 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 { column().push( self.build_combo().trigger() ).into() } fn overlays( &self ) -> Vec> { 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 { tabs::( [ "Profile", "Network", "Privacy" ] ) .selected( active ) .on_select( Msg::TabChanged ) # } ``` **See also**: [`notebook`](#notebook) for tabs that own their pages, [`tab_strip`](#tab_strip) for the open documents of a browser or a terminal. ### `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 { notebook::() .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. ### `tab_strip` The open documents of an app, as a browser or a terminal shows them. On a desktop, a strip of tabs with their icon, title and close button, sharing the width between a minimum and a fifth of the screen, scrolling sideways when even the minimum does not fit, to show the selected tab when it changes and while a tab is dragged against its edge, and always leaving 7.5 % of the width clear; buttons go before the tabs (`leading`), right after the last one (`after_tabs`) and at the far right (`trailing`). On a phone (`TabLayout::Mobile`), a bar with the active tab, or the app's own `mobile_center`, and a counter that sends `on_overview`; `tab_overview` is the grid of cards it opens, as a whole page with `build` or as the cards alone with `cards`. `TabStripColors` sets every colour, the theme's by default. Tabs grow from nothing as they open and the others narrow; a closed tab leaves a gap that shrinks away while the others keep their width for a second, so the close button that slides in lands under the pointer. The app keeps a `TabMotion`, hands it to every view and reports `TabMotion::is_animating` from `App::is_animating`. With `reorderable( true )` the desktop strip's tabs, and the overview's cards, can be dragged into another order, the others sliding out of the way; the overview takes the strip's `TabMotion` too: the app forwards `App::on_drag_move` to `TabMotion::drag_move` and answers `App::on_drop` with the move `TabMotion::drag_end` reports, as `( from, to )`. **When**: a browser's tabs, a terminal's sessions, an editor's files: documents the user opens and closes, each with a key that stays the same while it is open. ```rust,no_run # use ltk::{ tab, tab_strip, Element, TabLayout, TabMotion }; # #[ derive( Clone ) ] enum Msg { Select( usize ), Close( usize ), Overview, NewTab } # fn _ex( titles: &[ ( u64, String ) ], active: usize, motion: &TabMotion, plus: Element ) -> Element { tab_strip( titles.iter().map( |( key, title )| tab( *key, title.clone() ).symbol( "features/console" ) ) ) .selected( active ) .on_select( Msg::Select ) .on_close( Msg::Close ) .on_overview( Msg::Overview ) .after_tabs( plus ) .layout( TabLayout::Desktop ) .motion( motion ) .into() # } ``` **See also**: [`tabs`](#tabs) for a segmented selector between fixed sections. ### `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 { dialog() .title( "Delete partition?" ) .subtitle( "This will erase every file on /dev/sda2." ) .cancel( Msg::Cancel ) .action( button::( "Cancel" ).variant( ButtonVariant::Tertiary ).on_press( Msg::Cancel ) ) .action( button::( "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 { 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 { 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 { color_picker( current ).on_change( Msg::Picked ) # } ``` ### `file_chooser` A file to open, or a folder and a name to save one as, over `ltk::files`, alike natively and in a browser. It is not a builder but a component the app holds: `FileChooser::open` or `FileChooser::save` makes it, its `view()` goes in the app's view, and its `FileChooserMsg`s, which `wrap` makes the app's own, go to `update()`, which returns `Chosen::Path` or `Chosen::Cancelled` once the user chose. It lists the folder on its own through the app's `ChannelSender`, shows the home folder, the user's folders and the folders mounted in a browser as places, asks before saving over a file, and can create a folder. `dir` starts elsewhere than the home folder, `filter` shows only some types of files, `compact` puts the places behind a button for a phone, `sort` starts in an order kept from `current_sort` (by name, last access or size, as Crate sorts, which the user changes from the header), and `take_focus_request` gives the field to focus. **When**: any app that opens or saves the user's files, instead of a dialog of its own. ```rust,no_run # use std::path::PathBuf; # use ltk::{ ChannelSender, Chosen, Element, FileChooser, FileChooserMsg }; # #[ derive( Clone ) ] enum Msg { Open, Chooser( FileChooserMsg ) } # struct App { chooser: Option>, sender: ChannelSender, opened: Option } # impl App { fn update( &mut self, msg: Msg ) { match msg { Msg::Open => self.chooser = Some( FileChooser::open( &self.sender, Msg::Chooser ).filter( &[ "text/*" ] ) ), Msg::Chooser( msg ) => { let Some( chooser ) = &mut self.chooser else { return }; match chooser.update( msg ) { Some( Chosen::Path( path ) ) => { self.chooser = None; self.opened = Some( path ); } Some( Chosen::Cancelled ) => self.chooser = None, None => {} } } } } # fn view( &self ) -> Element { self.chooser.as_ref().map( FileChooser::view ).unwrap() } # } ``` **See also**: [`ltk::files`](../src/files/mod.rs) for reading and writing the file chosen, and `examples/file_chooser.rs`. --- ### `print_dialog` Where and how to print, with a preview of the pages. Like the file chooser it is a component the app holds: `PrintDialog::new` makes it, its `view()` goes in the app's view, and its `PrintDialogMsg`s, which `wrap` makes the app's own, go to `update()`, which returns a `PrintEvent`. It lists the printers CUPS knows on its own through the app's `ChannelSender`, with "Save as PDF" after them, and offers the copies, the pages, the paper, the orientation, the margins, the scale, two-sided printing, colour and background graphics. The app renders the document itself: with `preview( true )` the dialog sends `PrintEvent::Preview( options )` whenever the layout changes, and the app answers with `PrintDialogMsg::previewed`, a `Preview` of pages rendered about `PREVIEW_WIDTH` pixels wide; `PrintEvent::Print( job )` says where and how once the user accepts, and `ltk::print::send` delivers the PDF to a printer or, in a Flatpak, to the print portal, whose own dialog the user saw instead. `options` starts from options kept from an earlier job, `printer` or `save_as_pdf` choose where it starts, `compact( true )` makes it a whole page for a phone, and `take_focus_request` gives the field to focus. **When**: any app that prints, instead of a dialog of its own. ```rust,no_run # use ltk::{ ChannelSender, Element }; # use ltk::print::{ Destination, PrintDialog, PrintDialogMsg, PrintEvent, PrintOptions }; # #[ derive( Clone ) ] enum Msg { Print, Dialog( PrintDialogMsg ), Sent( Result<(), String> ) } # struct App { print: Option>, sender: ChannelSender } # impl App { # fn render( &self, _options: &PrintOptions ) -> Vec { Vec::new() } fn update( &mut self, msg: Msg ) { match msg { Msg::Print => self.print = Some( PrintDialog::new( &self.sender, Msg::Dialog, "Report" ).preview( true ) ), Msg::Dialog( msg ) => { let Some( dialog ) = &mut self.print else { return }; match dialog.update( msg ) { Some( PrintEvent::Preview( _options ) ) => { /* render the pages, then send Msg::Dialog( PrintDialogMsg::previewed( .. ) ) */ } Some( PrintEvent::Print( job ) ) => { self.print = None; if job.destination == Destination::Pdf { return; /* ask for a file with a FileChooser */ } let pdf = self.render( &job.options ); ltk::print::send( &job.destination, &job.title, &job.options, pdf, &self.sender, Msg::Sent ); } Some( PrintEvent::Cancelled ) => self.print = None, None => {} } } Msg::Sent( _result ) => {} } } # fn view( &self ) -> Element { self.print.as_ref().map( PrintDialog::view ).unwrap() } # } ``` **See also**: [`ltk::print`](../src/print/mod.rs) for the printers, their jobs and the default printer, as a settings page shows them, and `ltk::print::gtk` for the portal's print settings. --- ## 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 { 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, # field: Element, # submit_button: Element, # ) -> Element { 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>, w: u32, h: u32 ) -> Element { 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>, app2_rgba: Arc>, w: u32, h: u32 ) -> Element { 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, footer: Element ) -> Element { 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.