Length::dp no longer collapses to absolute pixels at construction: the design value travels in a new LengthBase::Dp variant and the density multiplication happens when the length is resolved. Previously dp( n ) baked in whatever density() returned at view-build time, so correctness across output changes depended on the view being rebuilt after set_density and in that order; now a density change is picked up by the very next paint with no reconstruction. Length::resolve keeps its signature (process density), and the new Length::resolve_with_density takes an explicit factor. dp becomes const in the bargain. Density also becomes overridable per canvas, the first step towards surface-local responsive state. SoftwareCanvas and GlesCanvas carry a density: Option<f32> analogous to the layout_viewport introduced for sub-canvas fluid resolution: None means "use the process global", Canvas::set_density pins a local factor, and sub-canvases inherit it. All canvas-routed resolution honours it — geom_px / font_px for stock-widget design pixels, and the new Canvas::resolve_geom / resolve_font for explicit Length values, which every widget now uses in place of the raw l.resolve( canvas.viewport_layout(), EM ) pattern (row, column, wrap_grid, spacer, container, separator, button, text, rich_text, text_edit, list_item, vslider, image, and the container draw path). Overlay sizing keeps resolving against the main surface with the global density, which is what it describes. New tests cover explicit-density resolution, resolution-time application, the local-over-global override and sub-canvas inheritance. The GLES image texture cache is now bounded. It was content-keyed but unbounded and never evicted, so a stream of distinct buffers — a photo carousel, video thumbnails — grew GPU memory for the lifetime of the canvas. The cache now tracks an estimated byte total (RGBA8, w × h × 4) against a 32 MiB budget and evicts least-recently-drawn textures on insert; the most recent entry is never evicted, so a single texture larger than the whole budget still draws and simply owns the cache until replaced. Drop-time cleanup is unchanged: drain deletes whatever the map holds. CI gains a Clippy step (workspace, all targets, test-support, -D warnings) sharing the build cache of the test job, with make clippy mirroring the invocation locally and CONTRIBUTING listing it. Run make clippy locally before pushing the first time — the gate has not seen the tree yet and pre-existing lints will fail CI until addressed. docs/backends.md formalises the software/GLES capability matrix that was previously scattered across per-method rustdoc: parity set (fills, strokes, text, images, paths, path clips), graceful degradations on software (flat-fill gradients, no shadows, no backdrop blur, hard bottom edge), GPU-only features (external textures), the shared Oklab-fallback limitation, and the cross-backend blit panic. Linked from README, onboarding and architecture's known-gaps list, which now states the parity gaps explicitly. The dp/density prose in architecture.md, lib.rs and the Length rustdoc is updated for resolution-time semantics and the per-canvas override.
13 KiB
ltk onboarding
This guide is for the first hour with ltk: what environment you need, how to
run the examples, how to build a minimal app, when to use layer-shell vs a
regular window, and what theme/font assumptions the toolkit currently makes.
If you already know the basics and want the deeper rationale, read
docs/architecture.md next.
What ltk is
ltk is a Rust UI toolkit for Wayland. It is aimed first at the Eydos shell
stack, but it can also be used to build normal client applications and
runtime-free UI surfaces.
At a high level:
- Implement the
Apptrait. - Return an
Element<Msg>tree fromview(). - React to user input by handling messages in
update(). - Start the event loop with
ltk::run(app).
The model is declarative and Elm-shaped: the widget tree is rebuilt from your
state, then ltk handles layout, drawing and input dispatch.
If you are browsing the crate through cargo doc, the public API is also
grouped conceptually into three entry points:
ltk::window— basic application windowsltk::shell— layer-shell and overlaysltk::runtime— advanced runtime hooks and runtime-free embedding
Most users should start with ltk::window and ignore the other two until they
have a normal app window running.
Before you start
ltk is not a browser toolkit and not a cross-platform desktop toolkit. Today
it assumes:
- a running Wayland session
- Wayland client libraries available through Rust dependencies
- a usable system font — on Debian (the crate's own packaging target):
fonts-sora,fonts-liberationorfonts-dejavu - an installed
defaulttheme, or a development theme directory exposed throughLTK_THEMES_DIR
The rendering backend is selected automatically:
- GLES when EGL/GLES is available
- software fallback otherwise, or when
LTK_FORCE_SOFTWARE=1
Fastest way to see it working
From the repo root:
LTK_THEMES_DIR=themes cargo run --example showcase
The LTK_THEMES_DIR=themes prefix points theme lookup at the in-repo
themes/ directory (see Theme and font setup below); without it, on a
machine without the default theme installed, the example runs on the
embedded B/W fallback with a red banner.
The other examples, same prefix:
cargo run --example widgets— broad widget surveycargo run --example inputs— plain and secure text fields with a show/hide-password togglecargo run --example scroll— the two main scroll use cases: a long list and an app-drawer-style gridcargo run --example sliders— the Glass effect on horizontal and vertical sliderscargo run --example combo— select/dropdown with editable query and multi-select chipscargo run --example pickers— notebook tabs, date, time and color pickerscargo run --example dialog— modal confirm, non-modal pick and the other dialog shapescargo run --example carousel— focused-tile carouselcargo run --example responsive— fluid vs physical scaling side by sidecargo run --example clip_path— per-path canvas clippingcargo run --example mini_shell— overlays, animation and theme switching
All examples require a running Wayland compositor.
Build and test
The Makefile wraps the cargo invocations the repo expects:
make all— release buildmake test—cargo test --features test-support; a barecargo testfails because the integration tests import the feature-gatedltk::test_supportmake doctest-md— typechecks the code snippets indocs/*.md, so API drift surfaces in CI like a normal doctest failuremake examples— runs every example in sequence withLTK_THEMES_DIR=themesmake doc—cargo doc --no-deps
Theme and font setup
ltk currently expects a theme named default. Lookup order is:
LTK_THEMES_DIR/<id>/$XDG_DATA_HOME/ltk/themes/<id>//usr/share/ltk/themes/<id>/
For development inside this repository, the simplest setup is:
export LTK_THEMES_DIR="$PWD/themes"
That makes ThemeDocument::find("default") resolve to
$PWD/themes/default/theme.json.
Font loading is separate from theme lookup. src/system_fonts.rs walks a
chain of common system font file paths — the files installed by the
Debian packages fonts-sora, fonts-liberation, fonts-freefont and
fonts-dejavu, plus the equivalent locations other distros use — and
loads the first one it finds. If nothing matches, it falls back to an
embedded Sora Regular (~50 KB, SIL OFL 1.1) shipped inside the crate, so
font resolution never panics on a system without the expected fonts.
Installing one of the listed packages is still recommended for richer
glyph coverage.
Your first app
The smallest useful ltk app implements App, returns a tree from view(),
updates its state in update(), and calls ltk::run(...).
use ltk::{ App, Element, Keysym, button, column, spacer, text };
#[derive(Clone)]
enum Msg
{
Increment,
}
struct CounterApp
{
value: u32,
}
impl App for CounterApp
{
type Message = Msg;
fn view( &self ) -> Element<Msg>
{
column::<Msg>()
.padding( 32.0 )
.spacing( 16.0 )
.center_y( true )
.push( text( "Hello from ltk" ).size( 28.0 ) )
.push( text( format!( "Count: {}", self.value ) ).size( 18.0 ) )
.push( spacer() )
.push( button( "Increment" ).on_press( Msg::Increment ) )
.into()
}
fn update( &mut self, msg: Msg )
{
match msg
{
Msg::Increment => self.value += 1,
}
}
fn on_key( &mut self, keysym: Keysym ) -> Option<Msg>
{
if keysym == Keysym::Escape
{
std::process::exit( 0 );
}
None
}
}
fn main()
{
ltk::run( CounterApp { value: 0 } );
}
Minimal Cargo.toml
[package]
name = "my-ltk-app"
version = "0.1.0"
edition = "2021"
[dependencies]
ltk = { path = "../ltk" }
If you vend ltk from crates.io later, replace the path dependency with a
versioned one.
Public API Layers
ltk exposes most items at the crate root, but for documentation and discovery
it is useful to think of the library in three layers.
1. ltk::window
This is the default entry point for third-party applications.
Use it for:
- normal application windows
- tools and prototypes
- most widget/layout work
The APIs you will usually touch first live here conceptually:
AppElement<Msg>button,text,text_edit,img_widgetcolumn,row,stack,grid,spacercontainer,scroll,slider,toggle,checkbox,radioColorrun
2. ltk::shell
This layer groups the APIs that matter when your surface is part of the shell rather than a normal app window.
Use it for:
- bars and docks
- homescreens
- notifications
- greeters and lock screens
- transient overlays
The most important APIs in this layer are:
ShellModeLayerAnchorOverlaySpecOverlayIdoverlays()
3. ltk::runtime
This layer is for advanced integration points.
Use it when you need:
- external wakeups via
set_channel_sender() - timer-driven or async state via
poll_external()/poll_interval() - redraw narrowing via
invalidate_after() - runtime theme state access
- runtime-free embedding through
core::UiSurface
Most applications do not need to start here.
Regular app window vs shell surface
Most consumers should start with a regular window.
Default behaviour:
shell_mode()defaults toShellMode::Windowltk::run(app)creates an xdg-shell toplevel
Use this for:
- normal applications
- internal tools
- prototypes while learning the toolkit
Switch to layer-shell only when you are building a shell component:
- top bar
- dock
- homescreen
- notification surface
- lock screen / greeter
The knobs you will usually override are:
shell_mode()layer_anchor()layer_size()exclusive_zone()keyboard_exclusive()background_color()
For a non-trivial multi-surface example, use examples/mini_shell.rs as the
reference entry point — note it runs as a regular window (it never overrides
shell_mode()) and demonstrates screen routing, coordinated overlays, an
animated OSD and live theme switching. The layer-shell knobs themselves are
exercised by the downstream shell components, not by the in-repo examples.
The APIs you will touch first
In practice, most first apps only need a small subset of the surface area.
Start here:
AppElement<Msg>button,text,text_edit,img_widgetcolumn,row,stack,grid,spacercontainer,scroll,slider,toggle,checkbox,radioColorrun
Do not start with these unless you need them:
ltk::shellltk::runtimeoverlays()- gesture hooks such as
on_swipe_* set_channel_sender()/poll_external()core::UiSurface- custom theming APIs
Message flow and state
The expected shape is:
- user interaction emits a
Message update()mutates app stateview()rebuilds the UI from that state
Example:
#[derive(Clone)]
enum Msg
{
NameChanged( String ),
Submit,
}
For small apps, one top-level enum Msg is enough. Once the app grows, split
state by screen/panel and wrap sub-messages in the top-level enum:
# #[ derive( Clone ) ] pub enum HomeMsg {}
# #[ derive( Clone ) ] pub enum SettingsMsg {}
enum AppMsg
{
Home( HomeMsg ),
Settings( SettingsMsg ),
Quit,
}
This is the pattern used by examples/mini_shell.rs.
Responsive sizing: fluid vs physical
ltk gives you two ways to make an interface adapt to the display, and you can
mix them per value. Fluid sizes are a fraction of the surface (best for
full-screen system surfaces); physical sizes stay a constant real-world
size (best for conventional windowed apps). In practice you write a size as a
Length and bound it with .clamp:
use ltk::{ text, Length };
// Fluid: 6 % of the surface's short side, never below 20 px nor above 44 px.
text("Welcome").size(Length::vmin(6.0).clamp(20.0, 44.0));
Stock widgets follow a process-wide mode (set_widget_scaling, fluid by
default); an explicit Length on a widget overrides it. For the units
(vmin / orient / dp / …), the clamp discipline and how it all resolves,
see the Responsive sizing section of
architecture.md and the Length
rustdoc.
Recommended learning order
If you are new to the library, this order minimizes confusion:
- Run
examples/showcase.rs. - Read the crate-level docs in
src/lib.rs, especiallyltk::window. - Build a plain xdg-shell window with
button,text,column. - Add input handling with
text_editorslider. - Only then look at
ltk::shellfor overlays and layer-shell. - Move to
ltk::runtimeonly when you need advanced hooks or embedding.
Performance rules of thumb
ltk is designed to sleep when idle and redraw only on real changes, but the
application can still make bad choices. Keep these rules in mind:
- keep
view()pure and cheap - do not do filesystem I/O, parsing or image decoding inside
view() - cache expensive derived data on your app struct
- leave
poll_interval()asNoneunless you genuinely need periodic wakeups - only return
truefromis_animating()while something is actually moving
On mobile targets, the last two matter directly for battery life.
When to use core::UiSurface
Most apps should ignore core at first.
Use core::UiSurface when you want ltk's layout/drawing/hit-testing without
ltk::run(). Typical cases:
- compositor-side decorations
- embedding
ltkwidgets in another render loop - offscreen rendering or previews
There is coverage for that path in tests/core_surface.rs.
Current assumptions and rough edges
This repo is usable, but a few current behaviours are worth knowing up front:
- examples and docs assume Wayland, not X11
- theming is process-global
- theme discovery currently expects a
defaulttheme on disk (a B/W fallback document kicks in when missing, with a red banner on every frame so the gap is impossible to miss) - the architecture docs mention downstream consumer repos that are not part of this repository
None of that blocks learning the toolkit, but it matters when you evaluate
ltk as a third-party dependency.
What to read next
In the README's recommended order — onboarding, then the widget catalogue, then the cookbook, then architecture:
docs/widgets.md— per-widget catalogue: what each one is, when to use it, minimal exampledocs/cookbook.md— concrete recipes: slide-in panels, password fields, runtime theme toggle, channel-driven statedocs/architecture.md— multi-surface patterns, theming, animation and performancedocs/theming.md— JSON theme schema, slot conventions, runtime APIsdocs/backends.md— software/GLES capability matrixexamples/showcase.rs— smallest visual tourexamples/widgets.rs— broader widget coverageexamples/mini_shell.rs— overlays and shell patternstests/core_surface.rs— runtime-free rendering