Three changes that came out of making the shell's back button work across Eydos the way it does on Android. The first is the feature; the other two are gaps it exposed. Back is no longer Escape. forge turns its back gesture and button into the `XF86Back` key for the focused client, and ltk treated that key as an alias of `Escape`: close transient UI, else drop the focus and reach `on_key_with_modifiers`. That is "cancel", and it is all an app could get: nothing navigated, and nothing ever left the app. `XF86Back` is now a back request with its own order, one layer per request, top-most first: transient UI the runtime owns (open xdg-popup overlays, the built-in context menu, a text selection, the top-most pressable carrying an `on_escape`, i.e. a dialog's cancel), then text-input focus, then the new `App::on_back`, then `on_key_with_modifiers( XF86_Back )` so an app that bound the key itself keeps receiving it (crustace does, for its launcher), then `App::back_at_root`. The first step is what Escape already did; it moves out of `handle_key_escape` into `AppData::dismiss_transient` in the new `input/keyboard/back.rs`, which both paths call, so Escape behaves exactly as before. Text focus comes before the app on purpose: with a keyboard covering half the screen, back means "put the keyboard away", and navigating instead would discard what the user was typing. A compositor that folds its keyboard itself, as forge now does, only lets the next request through, which makes this step the fallback for the ones that do not. `App::on_back` returns a `BackResponse`: `Message( msg )` with the message that undoes the top-most piece of navigation state, `Unhandled` at the root, or `Consumed` for a host whose back stack lives elsewhere and cannot be inspected ahead of time. `Consumed` exists for rustdroid: an Android app keeps its activities, fragments and back callbacks in its own code, so the host can only hand the request over. `App::back_at_root` then picks `Minimize` (the default), `Close` (through `on_close_requested`, which can still veto) or `Ignore`. Minimising is what a root activity does on Android since version 12 and what suits an app the user comes back to: a browser keeps its tabs, a call goes on. It is also the single point where the two stacks meet: the navigation stack belongs to the app's model, the task stack to the compositor, and `xdg_toplevel.set_minimized` is the one standard request between them, so no custom protocol carries navigation state out of the process and clients that are not ltk apps keep working. Only `ShellMode::Window` surfaces react at the root; a panel or a lock screen that minimised itself would be a bug. Three consequences of back being a navigation request rather than input: the key leaves the default `App::key_repeats` set, since a held button would empty the whole stack; it is no longer reported through `App::on_raw_key`, so a host forwarding raw keys into an embedded web view or guest toolkit stops leaking it as a keystroke; and the mouse's back button (`BTN_SIDE` / `BTN_BACK`) enters through the same `handle_back`. `focused_text_input` becomes `pub( crate )` for the new module. The answer is derived, not recorded, and that is the design decision worth writing down. The runtime keeps no navigation stack of its own. Its entries would have to be messages, which are not serialisable (the trait puts no serde bound on `Message`, by design), so it could not survive a relaunch; and a list of "things to undo" drifts the first time the user leaves a screen some other way, through its own close button or a link, leaving a back press that does nothing visible. An answer computed from the model cannot disagree with it. This is also what ties back to the session persistence added earlier: navigation state is both what `on_back` reads and what `save_state` should persist, and keeping the two the same is what makes back retrace the same places after a session restore or a crash, with nothing extra to serialise. Persist less and a restored app opens on the right screen with nothing behind it; persist more and it restores a menu the user had closed. `NavStack<R>` packages that rule for the common case, an app made of screens. It is a plain value the app keeps in its model, never seen by the runtime: `push`, `pop`, `replace` (the sideways move back should not retrace), `pop_to_root`, `reset`, `current`, `current_mut`, `iter`, `depth`, `can_pop`, and `back( msg )`, which answers `on_back` directly. It is stored as a root plus the routes above it rather than as a `Vec`, so "never empty" holds by construction, including after deserialising an old or damaged state file, and it derives serde whenever `R` does. Six unit tests cover it, a serde round trip among them, and its rustdoc example is a complete `App` where one object answers `on_back`, `save_state` and `restore_state`. Documentation, since the point is for app authors to take navigation state into account. The new `docs/navigation.md` is the reference: transient UI versus navigation state and the test that separates them, the back order and the reason for each step, why the answer is derived, how it lines up with session state, `NavStack`, apps that embed a web view (whose page history is navigation state too, to be persisted with ltk-webkit's `session_state`), hosts with a foreign back stack, the root, who owns which stack, and an author's checklist. `architecture.md` gains a group in the trait surface and a paragraph in the state patterns, `onboarding.md` and the README index link to it, and in the cookbook the multi-screen recipe now uses `NavStack` with `on_back` and persists the stack without serde, which keeps it compiled by the Markdown doctest harness (that harness links `ltk` alone, so the serde example in `navigation.md` is marked `ignore` and compiled as `NavStack`'s own doctest instead). `BackResponse`, `BackAtRoot` and `NavStack` are exported at the crate root and in the `ltk::window` facade. Behaviour change for existing apps, also recorded under Changed: a window app that implements neither `on_back` nor the key is now minimised on back at its root, where nothing happened before; `BackAtRoot::Ignore` restores that. With a text input focused, back only drops the focus and the app sees the next request, where it used to get the key in the same press. Renew the text-input enable on a press. Moving the focus between two text fields, or pressing the one already focused, only committed the refreshed content type on the existing `zwp_text_input_v3`. `activate_text_input` now sends `enable` first, as GTK does when a focused entry is clicked. A compositor that folded its on-screen keyboard while the field kept the focus, which is what back now does, has no other way to learn that the user wants it again: the text-input never stopped being active, so there was nothing for it to observe. ltk sends neither surrounding text nor a cursor rectangle, so the only state `enable` resets is the content type set right after it. Fit an oversized `window_size_hint` to the screen. The hint is pinned as `min_size == max_size` before the first commit, when no output is known yet, so an app asking for a phone-shaped 720x1440 window on a 1080p desktop got a toplevel taller than the screen; rustdroid carried a dead branch that tried to avoid this from a screen size nothing could give it. The first configure is the earliest point the hint can be checked, and the protocol has the right input for it: `xdg_toplevel.configure_bounds`, exposed by sctk as `WindowConfigure::suggested_bounds` and ignored until now. The new `event_loop/window_fit.rs` holds `fit_within`, which scales a size down keeping its aspect ratio until it fits and leaves one that fits untouched (four unit tests), and `AppData::fit_size_hint`, run once behind the new `pending_size_hint_fit` latch: it uses the compositor's bounds when sent and otherwise 90 % of the first output's logical size, since panels and decorations are unknown from the client side. When the hint has to shrink, `min_size` (and `max_size` for a non-resizable window) is re-pinned to the fitted size, which is kept in `fitted_size_hint` and replaces the app's hint as the fallback for later configures that carry no size; without that the window would grow back on the next one. On that one configure the fitted size wins over the compositor's `new_size`, which was derived from the oversized pin. `App::window_size_hint` documents the rule with the worked example (720x1440 opens as 486x972 on a 1080p output). The back order needs a live Wayland connection to exercise and has no test; the unit tests cover `NavStack` and `fit_within`.
494 lines
15 KiB
Markdown
494 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.
|
|
|
|
What those bytes should hold is the app's *navigation state* — which screen,
|
|
which panel, how far into a history — because that is also what the system
|
|
back button walks through `App::on_back`. Keeping the two the same makes back
|
|
retrace the same places after a restore. An app with more than one screen
|
|
should read [`docs/navigation.md`](./navigation.md) before growing its second.
|
|
|
|
## 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
|