Files
ltk/README.md
Pedro M. de Echanove Pasquin 1fd697aa6d
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
docs overhaul, orientation API, fluid-sizing fixes, examples made honest
Documentation pass: every claim in docs/ and the meta files was audited against the source and the drift fixed — around ninety corrections. CONTRIBUTING and the CI workflow now run cargo test with --features test-support (the gated test_support module made both the documented commands and the CI build fail to compile), make example becomes make examples, make doctest-md and the debhelper requirement of make clean are documented, and patch shape asks for a CHANGELOG entry. theming.md loses the nonexistent surface.backdrop, gains the real gradient defaults (linear-rgb, oklab), the six slot variants including typography, the ten-field palette, a truthful effects-consumer table, the ThemePreference/from_hour API and a responsive-sizing note; the stale docstrings in src/theme that fed the drift are fixed too. architecture.md's "Known gaps" section is rewritten against reality (multi-touch slots, xdg-activation, a11y live regions and SetValue/Increment/Decrement are implemented), gains a module map, subsurfaces and window-lifecycle coverage, and correct crustace/loginmanager paths. widgets.md fixes the ten factual errors (stateless spinner, toast/combo via overlays(), tooltip hover contract, row has no max_width, scroll axes, multiline text_edit, dialog panic wording) and now states the column() 16 px default padding — the recurring ambush — plus row's differing 0 default and dialog's max_width. onboarding, README and cookbook get the remaining sweep: build/test instructions, complete example lists, img_widget, clipping-parity honesty, ~30 Hz software cap, read_rgba_pixels signature, tab indentation in snippets, and rustdoc-style links that rendered literally are gone everywhere. CHANGELOG is restructured per Keep a Changelog with the missing entries (window_resizable, claims_raw_touch, Row::align_top/fill_height, caret fixes, dependency pins) and the pad_v Added/Changed contradiction resolved.
New adaptive-layout API: ltk::orientation() with the Orientation enum, backed by viewport_size()/set_viewport_size — the runtime records the main surface's physical dimensions on every configure, before App::on_resize, so view() can branch a layout on portrait vs landscape without hand-tracking resizes. The portrait rule matches Length::orient (square counts as portrait); embedders driving core::UiSurface call set_viewport_size themselves. Documented in the crate root's responsive-design section and architecture.md.
Fluid-vs-fixed sizing fixes in widgets, all the same disease — fluid content inside a fixed-pixel box. TextEdit::fixed_width takes impl Into<Length> (f32 call sites keep compiling as px) and the time picker's digit fields move to Length::fluid( 72.0 ), matching their fluid font so digits can no longer outgrow the box. Dialog::max_width takes impl Into<Length> with a Length::fluid( 480.0 ) default so the card scales with the stock buttons inside it, and the card's interior no longer stacks the column() default 16 px padding on top of CARD_PADDING — that double inset squeezed the action row until its buttons clipped on narrow windows. App::on_pointer_axis now triggers a view rebuild and repaint; previously state mutated in the hook did not paint until the next unrelated event.
Examples reworked to be honest demos: responsive's mode/density controls become stock buttons in a grid/column so they follow the modes they demonstrate instead of overflowing; dialog's openers stack vertically, and the example gains the app-level ESC handler so the ESC chain closes an open dialog first and quits second; widgets' tab strip now switches real per-tab pages; carousel gains pointer/touch drag through the horizontal-swipe hooks (crustace's pager pattern), one-tile-per-detent mouse wheel, and snap math driven by the real surface width from on_resize instead of a hardcoded 800; clip_path arranges its cells by ltk::orientation() and sizes them from the counter-axis of the flow.
2026-07-30 19:28:26 +02:00

422 lines
15 KiB
Markdown

# ltk
`ltk` is a public Rust UI toolkit for Wayland applications.
It is developed by Liberux as part of the Eydos stack, where it powers
shell and application surfaces, but it is published as a reusable library for
third-party developers building their own Wayland software.
Being written in Rust is also part of the project's value proposition:
- memory safety without a garbage collector
- predictable resource lifetimes through ownership and borrowing
- good control over allocations and data movement in rendering-heavy code
- a strong fit for low-level UI, graphics, and system-integration work
For a Wayland toolkit, that combination is useful in practice: it reduces an
entire class of memory-management bugs common in lower-level UI stacks while
still allowing tight control over performance-sensitive paths.
## What It Is
`ltk` is a lightweight, declarative toolkit with an Elm-shaped model:
- implement `App`
- return an `Element<Msg>` tree from `view()`
- update your state in `update()`
- run the event loop with `ltk::run(app)`
The runtime handles layout, drawing, input dispatch, focus, overlays, and
backend selection between GLES and software rendering.
## What It Is Not
`ltk` is not:
- a browser UI toolkit
- a cross-platform desktop toolkit
- a general-purpose web-style framework
Today it is specifically a Wayland toolkit. If you are building native Wayland
applications, panels, launchers, lock screens, or other shell-adjacent
surfaces, it is in scope. If you need Windows, macOS, or browser targets, it
is not.
## Project Status
`ltk` is a public library intended for third-party use, but it is still shaped
by real production needs inside the Liberux / Eydos ecosystem.
That means:
- the API is usable for external applications today
- the project is optimized first for native Wayland workloads
- some advanced APIs are still more shell-oriented than app-oriented
- public documentation and examples are present, but the project is not trying
to present itself as a cross-platform beginner toolkit
If you are evaluating `ltk` for a third-party application, the right mental
model is "public Wayland toolkit with production consumers" rather than
"experimental demo crate".
## Why Third Parties Might Use It
`ltk` is designed around a few practical goals:
- low idle wakeups and event-driven redraws
- partial redraws and damage tracking
- a simple declarative tree instead of retained widgets
- direct support for normal windows, layer-shell, and ext-session-lock surfaces
- a runtime-free core (`ltk::core::UiSurface`) for embedding layout and drawing
without `ltk::run()`
This makes it especially relevant for:
- Wayland applications
- mobile-first Linux shells
- launchers and dashboards
- greeters and lock screens
- compositor-side or embedded UI surfaces
## Quick Start
Add `ltk` to your `Cargo.toml`:
```toml
[dependencies]
ltk = { path = "../ltk" }
```
If you consume `ltk` from outside this workspace, replace the `path` dependency with a versioned one.
Minimal app:
```rust
use ltk::{ App, Element, 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 main()
{
ltk::run( CounterApp { value: 0 } );
}
```
## Requirements
`ltk` currently assumes:
- **Rust 1.85** or newer (the toolchain shipped with Debian stable; declared
as `rust-version` in `Cargo.toml`).
- A running **Wayland** session — there is no X11 backend.
- System headers for `libwayland`, `libegl` and `libxkbcommon` at compile
time. On Debian / Ubuntu:
```bash
sudo apt-get install libwayland-dev libegl-dev libxkbcommon-dev pkg-config
```
- A usable system font (`fonts-sora`, `fonts-liberation`, `fonts-dejavu`,
…). If none is installed `ltk` falls back to an embedded Sora Regular
build with a stderr warning.
- A theme named `default`, installed system-wide (the
`ltk-theme-default` Debian package drops it under
`/usr/share/ltk/themes/default/`) or exposed through
`LTK_THEMES_DIR` for development.
Rendering backend selection is automatic:
- **GLES** when EGL is available (every modern Wayland compositor).
- **Software** fallback otherwise.
- Set `LTK_FORCE_SOFTWARE=1` to force the software path even when EGL is
available — useful for headless test runs and for diagnosing
driver-specific bugs.
For development inside this repository:
```bash
export LTK_THEMES_DIR="$PWD/themes"
cargo run --example showcase
```
## Examples
Useful entry points in this repository:
- `cargo run --example showcase`
- `cargo run --example responsive`
- `cargo run --example widgets`
- `cargo run --example inputs`
- `cargo run --example scroll`
- `cargo run --example combo`
- `cargo run --example dialog`
- `cargo run --example sliders`
- `cargo run --example pickers`
- `cargo run --example carousel`
- `cargo run --example clip_path`
- `cargo run --example mini_shell`
In general:
- start with `showcase` for a regular app window
- use `responsive` to see the fluid vs physical modes on stock widgets
- use `widgets` to see the core controls
- use `mini_shell` if you need overlays, theme switching, or shell-style
composition
## Public API Overview
Most applications should start with this subset:
- `App`
- `Element<Msg>`
- widgets such as `button`, `text`, `text_edit`, `img_widget`
- layouts such as `column`, `row`, `stack`, `grid`, `spacer`
- `Color`
- `run`
More advanced APIs are available when needed:
- `overlays()`
- `shell_mode()` and layer-shell controls
- `set_channel_sender()` and `poll_external()`
- gesture hooks such as `on_swipe_*`
- `core::UiSurface`
- runtime theme APIs
## Responsive Design
`ltk` offers **two** first-class ways to make an interface adapt to the
display, chosen per value or per process:
- **Fluid** — sizes are a fraction of the surface, tracking the short side
(width in portrait, height in landscape). Best for full-screen system
surfaces. Written with `Length::vmin` / `orient` / `fluid` and bounded with
`.clamp`.
- **Physical** — sizes stay a constant real-world size across displays (the
mainstream HiDPI `dp` model). Best for conventional windowed apps. Written
with `Length::dp` plus `set_density`.
Stock widgets follow the process-wide mode (`set_widget_scaling`, fluid by
default); an explicit `Length` on a widget always overrides it. The full
mechanics live in [`docs/architecture.md`](docs/architecture.md#responsive-sizing),
a walkthrough in [`docs/onboarding.md`](docs/onboarding.md), and the per-item
reference in the `Length` / `WidgetScaling` rustdoc.
## Windows and Shell Surfaces
By default, `ltk` creates a regular `xdg-shell` window.
That is the right starting point for:
- normal applications
- internal tools
- prototypes
Switch to layer-shell only when you are building shell surfaces such as:
- top bars
- docks
- homescreens
- notifications
- greeters
- lock screens
For a screen locker, use `ShellMode::SessionLock` instead of layer-shell: it
presents an `ext-session-lock-v1` surface that the compositor keeps on top of
everything until the app returns `true` from `requested_exit()`, which makes
the runtime call `unlock` and lift the lock.
## Performance Notes
`ltk` is designed to sleep when idle and redraw only on real work.
The main rules for downstream applications are:
- keep `view()` pure and cheap
- do not perform I/O inside `view()`
- use `poll_interval()` sparingly
- return `true` from `is_animating()` only while something is actually moving — note that on the software backend animation redraws are capped at ~30 Hz by default (override with `App::cap_software_animation`)
- cache decoded images and expensive derived state in your app
Set `LTK_PERF_WARN=1` during development for opt-in diagnostics: it warns about stuck animations, sustained software-render animation, and low `poll_interval` values.
The library already provides:
- event-driven redraw scheduling
- per-surface invalidation
- partial redraws for interaction-only changes
- GPU and software backends behind the same widget API
## Backend Differences
The public API is the same across backends, but visual parity is not perfect
yet. The widget tree, layout, hit-testing, text, images, fills, strokes and
single-rect / path clipping all paint identically on both paths. The gaps are in
gradients, the shadow / backdrop pipeline, and multi-rect clipping: the GLES
backend installs the bounding-box union of the rects as its `glScissor` — a
coarse clip that does not cull pixels between disjoint rects — while the
software backend clips each rect exactly.
Effects that currently render only on the **GLES** backend, and degrade on the
**Software** backend:
- **Gradients** (linear and radial, via `Canvas::fill_paint_rect`) — rendered with
dedicated shaders on GLES; on software they collapse to a flat fill from the
first stop (tiny-skia can render gradients natively, but that is not wired up
yet).
- **Outer drop shadows** (`Canvas::fill_shadow_outer`) — themed surfaces that
declare a `Shadow` slot show the soft halo on GLES and a flat fill on
software.
- **Inner / inset shadows** (`Canvas::fill_shadow_inset`) — `InsetShadow`
slots paint nothing on software.
- **Inset shadow blend modes** — `PlusLighter`, `Multiply`, `Screen` and
`Overlay` are GLES-only; the GLES `Overlay` path snapshots the framebuffer
and computes the CSS Overlay formula in-shader, which has no software
equivalent today.
Calls to these APIs are safe on both backends — they simply produce a flatter
appearance under software. No widget panics, returns an error, or skips
unrelated drawing.
If your application leans heavily on shadows or inset effects, validate both
rendering paths before shipping. Force the software path with:
```bash
LTK_FORCE_SOFTWARE=1 cargo run --example showcase
```
Closing this gap (porting the shadow / inset-shadow pipeline to tiny-skia) is
on the roadmap.
## Documentation
| File | When to read it |
| --- | --- |
| [`docs/onboarding.md`](docs/onboarding.md) | First hour with the library — environment, first app, what to ignore at first. |
| [`docs/architecture.md`](docs/architecture.md) | Runtime model, overlays, animation, theming, performance and where the cost of a frame lives. |
| [`docs/widgets.md`](docs/widgets.md) | Per-widget catalogue: what each one is, when to use it, minimal example, see-also. |
| [`docs/theming.md`](docs/theming.md) | JSON theme schema, slot conventions, runtime APIs. |
| [`docs/cookbook.md`](docs/cookbook.md) | Concrete recipes — slide-in panels, password fields, runtime theme toggle, channel-driven state, embedding without `ltk::run`. |
| `cargo doc --open` | Per-item rustdoc for the public API. |
| [`CHANGELOG.md`](CHANGELOG.md) | What changed in each release, and what is pending unreleased. |
| [`code_style_guide.md`](code_style_guide.md) | The Modified Allman style rules the codebase follows. |
| [`SECURITY.md`](SECURITY.md) | How to report a vulnerability and what is in / out of scope. |
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Build, test, code style, patch shape. |
Recommended reading order for a new contributor:
1. run `examples/showcase.rs`
2. read `docs/onboarding.md`
3. browse `docs/widgets.md` for the catalogue
4. dip into `docs/cookbook.md` when you hit a specific shape
5. open `docs/architecture.md` once you need overlays, animations, or
runtime theming.
## Relationship to Liberux and Eydos
Liberux is the promoter and primary maintainer of `ltk`.
The project exists because Eydos needs a native Wayland toolkit for its
own shell and application stack, but `ltk` is intentionally published as a
public library rather than kept as a private internal component. Third-party
developers are part of the intended audience.
That origin matters because it explains the current priorities:
- strong Wayland focus
- support for layer-shell and shell-style overlays
- attention to mobile power usage
- theming and runtime surfaces that fit an operating system environment
## License
This project is licensed under `LGPL-2.1-only`.
That means third parties can use `ltk` in their own applications, including
proprietary ones, subject to the obligations of the GNU Lesser General Public
License v2.1. If you are planning a commercial or closed-source product, read
the license text carefully and make sure your distribution model complies with
it.
See [LICENSE](./LICENSE).
## Third-party assets
`ltk`'s default theme bundles two third-party asset sets that travel under
their own licences. Anyone redistributing the toolkit (or a binary that
embeds the default theme) must propagate the attributions below.
- **Symbolic icons** under `themes/default/icons/catalogue/` come from
Streamline's *Core Line Free* set, distributed under
[Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/)
(CC BY 4.0). © Streamline. Some files have been modified for the
symbolic-tinting pipeline; details in
[`themes/default/icons/catalogue/LICENSE.md`](./themes/default/icons/catalogue/LICENSE.md).
Upstream: <https://www.streamlinehq.com/icons/core-line-free>.
- **Sora Regular** (`src/theme/fallback/Sora-Regular.otf`) is the
embedded font fallback, distributed under
[SIL Open Font Licence 1.1](https://scripts.sil.org/OFL).
© 2019-2020 The Sora Project Authors, Jonathan Barnbrook, Julián Moncada.
Upstream: <https://github.com/sora-xor/sora-font>.
The remaining artwork in the default theme — wallpapers, lockscreens,
launcher logo, brand-mark variants and per-application icons — is
original to Liberux Labs and travels under the toolkit's own
`LGPL-2.1-only` licence.
The full Debian-style declaration of every asset and its licence lives
in [`debian/copyright`](./debian/copyright); that is the file the `.deb`
ships under `/usr/share/doc/libltk*/copyright`.
## Contributing
Patches and bug reports are welcome. Read
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the practical mechanics: build
prerequisites, how to run tests, the project's Modified Allman code
style, and what shape a pull request should take.
For security-sensitive issues see [`SECURITY.md`](SECURITY.md) — please
do not file those through the public issue tracker.
If you are evaluating `ltk` for a third-party product and are unsure
whether your use case is in scope, open a discussion before writing
code. That is especially useful when you are:
- missing an app-facing example,
- blocked by a shell-oriented assumption in the API,
- trying to understand whether a given platform target is realistic.