Applications built on ltk had no way to come back where the user left them: the toolkit hardcoded `app_id = "ltk"` on every toplevel, never wrote anything to disk, and died on SIGTERM without a chance to save. This release gives the runtime the whole plumbing and asks each application only for the bytes worth keeping, in the spirit of Android's saved-instance state. The `App` trait gains three mandatory methods, deliberately without default bodies so every application states its position: `app_id()` (reverse-DNS, used for `xdg_toplevel.set_app_id`, the AccessKit application name and the state directory — the `app_id` element of the deprecated `window_config` tuple is now ignored and a one-time warning reports a mismatch), `save_state() -> Option<Vec<u8>>` and `restore_state(Vec<u8>)`. The bytes are opaque; the trait carries no serde bound. Their rustdoc is the contract: when the runtime saves, where the files live, when the bytes come back and when they do not, what must never go in them, and a worked serde_json example. The runtime persists under `$XDG_STATE_HOME/<app_id>/` (falling back to `~/.local/state`): `session.json` holds the compositor session id, a clean-exit marker and the writer's pid; `state.bin` holds the application bytes. Writes are atomic (temp file + rename, mode 0600, directory 0700) and best-effort. State is saved every 30 s when the bytes changed, once after the event loop exits (which covers `on_close_requested`, `requested_exit` and lost connections), and on SIGTERM/SIGINT — a calloop signal source, installed before any thread exists, now turns those into a clean exit of the loop instead of process death. `restore_state` runs synchronously in `try_run` before the window is created and before the first `view()`, and only when the process is relaunched as part of a session restore (`LTK_SESSION_RESTORE=1`, removed from the environment before the app can spawn children) or when the previous run left `clean_exit: false`; a plain launch starts fresh. A second concurrent instance detects the live pid and runs with persistence disabled rather than clobbering the first. The compositor side of geometry restore goes through `xdg-session-management-v1`. Neither wayland-protocols nor sctk ship generated code for it yet, so the XML is vendored under `protocols/` and `wayland-scanner` generates the client module in-tree (`src/protocol/`), resolving the crate names through sctk's reexports so the bindings stay on the crate instances sctk links. Before the first commit of a `ShellMode::Window` toplevel the runtime binds `xdg_session_manager_v1`, calls `get_session(reason, stored_id)` and `restore_toplevel(toplevel, "main")`; the three window-creation paths in `run.rs` are folded into one `make_window` helper so the attach always sits immediately before `commit()`. `created` persists the id, `replaced` destroys the objects and stops persisting. Compositors without the global lose only the geometry half. Layer-shell and session-lock surfaces skip the whole machinery. Every `App` implementor in the tree is updated: the twelve examples (`showcase`, `scroll` and `mini_shell` persist real state; the rest return `None`), both integration tests (`event_loop_flow` gains `save_restore_round_trip`), the in-source and markdown doctests, README, onboarding, cookbook (new recipe "Surviving relaunch: session state") and architecture docs, and the changelog. `src/session_state.rs` carries unit tests over a temporary state directory. `Makefile install` now copies `protocols/` into the cargo registry — without it downstream builds would fail inside the proc-macro — and `debian/copyright` covers the vendored XML. The trait change is breaking, hence 0.3.0. Also fixes the pre-existing `viewport_tests` module in `render/mod.rs`, which used `Length` without importing it and broke `cargo test`.
488 lines
15 KiB
Markdown
488 lines
15 KiB
Markdown
# 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`](./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 `App` trait.
|
|
- Return an `Element<Msg>` tree from `view()`.
|
|
- 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 windows
|
|
- `ltk::shell` — layer-shell and overlays
|
|
- `ltk::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-liberation` or `fonts-dejavu`
|
|
- an installed `default` theme, or a development theme directory exposed
|
|
through `LTK_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:
|
|
|
|
```bash
|
|
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 survey
|
|
- `cargo run --example inputs` — plain and secure text fields with a
|
|
show/hide-password toggle
|
|
- `cargo run --example scroll` — the two main scroll use cases: a long list
|
|
and an app-drawer-style grid
|
|
- `cargo run --example sliders` — the Glass effect on horizontal and
|
|
vertical sliders
|
|
- `cargo run --example combo` — select/dropdown with editable query and
|
|
multi-select chips
|
|
- `cargo run --example pickers` — notebook tabs, date, time and color
|
|
pickers
|
|
- `cargo run --example dialog` — modal confirm, non-modal pick and the
|
|
other dialog shapes
|
|
- `cargo run --example carousel` — focused-tile carousel
|
|
- `cargo run --example responsive` — fluid vs physical scaling side by side
|
|
- `cargo run --example clip_path` — per-path canvas clipping
|
|
- `cargo 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 build
|
|
- `make test` — `cargo test --features test-support`; a bare `cargo test`
|
|
fails because the integration tests import the feature-gated
|
|
`ltk::test_support`
|
|
- `make doctest-md` — typechecks the code snippets in `docs/*.md`, so API
|
|
drift surfaces in CI like a normal doctest failure
|
|
- `make examples` — runs every example in sequence with
|
|
`LTK_THEMES_DIR=themes`
|
|
- `make doc` — `cargo doc --no-deps`
|
|
|
|
## Theme and font setup
|
|
|
|
`ltk` currently expects a theme named `default`. Lookup order is:
|
|
|
|
1. `LTK_THEMES_DIR/<id>/`
|
|
2. `$XDG_DATA_HOME/ltk/themes/<id>/`
|
|
3. `/usr/share/ltk/themes/<id>/`
|
|
|
|
For development inside this repository, the simplest setup is:
|
|
|
|
```bash
|
|
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(...)`.
|
|
|
|
```rust,no_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;
|
|
|
|
// Names the window for the compositor and the a11y tree, and the
|
|
// `$XDG_STATE_HOME/<app_id>/` directory the runtime saves state in.
|
|
fn app_id( &self ) -> &str { "net.example.Counter" }
|
|
|
|
// Opaque bytes ltk persists for you; handed back only on a session
|
|
// restore or after a crash — a plain launch starts fresh.
|
|
fn save_state( &self ) -> Option<Vec<u8>>
|
|
{
|
|
Some( self.value.to_string().into_bytes() )
|
|
}
|
|
|
|
fn restore_state( &mut self, state: Vec<u8> )
|
|
{
|
|
if let Some( v ) = String::from_utf8( state ).ok().and_then( |s| s.parse().ok() )
|
|
{
|
|
self.value = v;
|
|
}
|
|
}
|
|
|
|
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`
|
|
|
|
```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:
|
|
|
|
- `App`
|
|
- `Element<Msg>`
|
|
- `button`, `text`, `text_edit`, `img_widget`
|
|
- `column`, `row`, `stack`, `grid`, `spacer`
|
|
- `container`, `scroll`, `slider`, `toggle`, `checkbox`, `radio`
|
|
- `Color`
|
|
- `run`
|
|
|
|
### 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:
|
|
|
|
- `ShellMode`
|
|
- `Layer`
|
|
- `Anchor`
|
|
- `OverlaySpec`
|
|
- `OverlayId`
|
|
- `overlays()`
|
|
|
|
### 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 to `ShellMode::Window`
|
|
- `ltk::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:
|
|
|
|
- `App` — `app_id`, `view`, `update`, `save_state` / `restore_state`
|
|
- `Element<Msg>`
|
|
- `button`, `text`, `text_edit`, `img_widget`
|
|
- `column`, `row`, `stack`, `grid`, `spacer`
|
|
- `container`, `scroll`, `slider`, `toggle`, `checkbox`, `radio`
|
|
- `Color`
|
|
- `run`
|
|
|
|
Do not start with these unless you need them:
|
|
|
|
- `ltk::shell`
|
|
- `ltk::runtime`
|
|
- `overlays()`
|
|
- gesture hooks such as `on_swipe_*`
|
|
- `set_channel_sender()` / `poll_external()`
|
|
- `core::UiSurface`
|
|
- custom theming APIs
|
|
|
|
## Message flow and state
|
|
|
|
The expected shape is:
|
|
|
|
1. user interaction emits a `Message`
|
|
2. `update()` mutates app state
|
|
3. `view()` rebuilds the UI from that state
|
|
|
|
Example:
|
|
|
|
```rust
|
|
#[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:
|
|
|
|
```rust,no_run
|
|
# #[ 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`:
|
|
|
|
```rust
|
|
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`](architecture.md#responsive-sizing) and the `Length`
|
|
rustdoc.
|
|
|
|
## Recommended learning order
|
|
|
|
If you are new to the library, this order minimizes confusion:
|
|
|
|
1. Run `examples/showcase.rs`.
|
|
2. Read the crate-level docs in `src/lib.rs`, especially `ltk::window`.
|
|
3. Build a plain xdg-shell window with `button`, `text`, `column`.
|
|
4. Add input handling with `text_edit` or `slider`.
|
|
5. Only then look at `ltk::shell` for overlays and layer-shell.
|
|
6. Move to `ltk::runtime` only 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()` as `None` unless you genuinely need periodic wakeups
|
|
- only return `true` from `is_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 `ltk` widgets in another render loop
|
|
- offscreen rendering or previews
|
|
|
|
There is coverage for that path in `tests/core_surface.rs`.
|
|
|
|
## Session state
|
|
|
|
`ltk` owns the plumbing, you own the bytes. `save_state` returns whatever a
|
|
relaunch needs (any encoding — the runtime never looks inside) and the
|
|
runtime writes it to `$XDG_STATE_HOME/<app_id>/state.bin` every 30 s when it
|
|
changed, when the window closes and on `SIGTERM` / `SIGINT`. `restore_state`
|
|
gets those bytes back before the first frame — but only when the shell
|
|
relaunches the app as part of a session restore (`LTK_SESSION_RESTORE=1`) or
|
|
when the previous run did not exit cleanly. Opening the app from the launcher
|
|
starts fresh; window size and position still come back on every launch
|
|
because the compositor restores them through `xdg-session-management-v1`.
|
|
Shell components (panels, lock screens) return `None` and no-op. See the
|
|
cookbook recipe *Surviving relaunch: session state* for a worked example.
|
|
|
|
## 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 `default` theme 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`](./widgets.md) — per-widget catalogue: what each one
|
|
is, when to use it, minimal example
|
|
- [`docs/cookbook.md`](./cookbook.md) — concrete recipes: slide-in panels,
|
|
password fields, runtime theme toggle, channel-driven state
|
|
- [`docs/architecture.md`](./architecture.md) — multi-surface patterns,
|
|
theming, animation and performance
|
|
- [`docs/theming.md`](./theming.md) — JSON theme schema, slot conventions,
|
|
runtime APIs
|
|
- [`docs/backends.md`](./backends.md) — software/GLES capability matrix
|
|
- [`examples/showcase.rs`](../examples/showcase.rs) — smallest visual tour
|
|
- [`examples/widgets.rs`](../examples/widgets.rs) — broader widget coverage
|
|
- [`examples/mini_shell.rs`](../examples/mini_shell.rs) — overlays and shell
|
|
patterns
|
|
- [`tests/core_surface.rs`](../tests/core_surface.rs) — runtime-free rendering
|