`take_focus_request()` looks up the target widget by `WidgetId` in `widget_rects`. Read-only `TextEdit` widgets are not tracked as interactive and therefore absent from `widget_rects`. A swipe-reveal flips the fields from read-only to interactive on `SlideRevealed`, but an intervening input event (e.g. the touch-up at the end of the swipe) can cause `dispatch()` to return before the vblank frame callback clears `frame_pending`, skipping the draw that would have rebuilt the widget tree. When the focus request fires in that same iteration it finds nothing and is consumed without effect. `AppData` gains a `focus_retry` field. When `take_focus_request()` (or a previous retry) returns an id but the widget is not in `widget_rects`, the id is stored in `focus_retry`, `view_dirty` is set and a redraw is requested. The next time around the view has been rebuilt with interactive fields and the widget is present.
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 fromview() - 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 withoutltk::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:
[dependencies]
ltk = { path = "../ltk" }
Minimal app:
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-versioninCargo.toml). - A running Wayland session — there is no X11 backend.
- System headers for
libwayland,libeglandlibxkbcommonat compile time. On Debian / Ubuntu: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 installedltkfalls back to an embedded Sora Regular build with a stderr warning. - A theme named
default, installed system-wide (theltk-theme-defaultDebian package drops it under/usr/share/ltk/themes/default/) or exposed throughLTK_THEMES_DIRfor development.
Rendering backend selection is automatic:
- GLES when EGL is available (every modern Wayland compositor).
- Software fallback otherwise.
- Set
LTK_FORCE_SOFTWARE=1to 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:
export LTK_THEMES_DIR="$PWD/themes"
cargo run --example showcase
Examples
Useful entry points in this repository:
cargo run --example showcasecargo run --example widgetscargo run --example inputscargo run --example scrollcargo run --example combocargo run --example dialogcargo run --example sliderscargo run --example pickerscargo run --example mini_shell
In general:
- start with
showcasefor a regular app window - use
widgetsto see the core controls - use
mini_shellif you need overlays, theme switching, or shell-style composition
Public API Overview
Most applications should start with this subset:
AppElement<Msg>- widgets such as
button,text,text_edit,image - layouts such as
column,row,stack,grid,spacer Colorrun
More advanced APIs are available when needed:
overlays()shell_mode()and layer-shell controlsset_channel_sender()andpoll_external()- gesture hooks such as
on_swipe_* core::UiSurface- runtime theme APIs
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
truefromis_animating()only while something is actually moving - cache decoded images and expensive derived state in your app
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, clipping and gradients all paint identically on both paths. The gap is in the shadow / backdrop pipeline.
Effects that currently render only on the GLES backend, and are silent no-ops on the Software backend:
- Outer drop shadows (
Canvas::fill_shadow_outer) — themed surfaces that declare aShadowslot show the soft halo on GLES and a flat fill on software. - Inner / inset shadows (
Canvas::fill_shadow_inset) —InsetShadowslots paint nothing on software. - Inset shadow blend modes —
PlusLighter,Multiply,ScreenandOverlayare GLES-only; the GLESOverlaypath 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:
LTK_FORCE_SOFTWARE=1 cargo run --example showcase
Closing this gap (porting the shadow / inset-shadow pipeline to tiny-skia) is on the post-v0.1 roadmap.
Documentation
| File | When to read it |
|---|---|
docs/onboarding.md |
First hour with the library — environment, first app, what to ignore at first. |
docs/architecture.md |
Runtime model, overlays, animation, theming, performance and where the cost of a frame lives. |
docs/widgets.md |
Per-widget catalogue: what each one is, when to use it, minimal example, see-also. |
docs/theming.md |
JSON theme schema, slot conventions, runtime APIs. |
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. |
SECURITY.md |
How to report a vulnerability and what is in / out of scope. |
CONTRIBUTING.md |
Build, test, code style, patch shape. |
Recommended reading order for a new contributor:
- run
examples/showcase.rs - read
docs/onboarding.md - browse
docs/widgets.mdfor the catalogue - dip into
docs/cookbook.mdwhen you hit a specific shape - open
docs/architecture.mdonce 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.
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 (CC BY 4.0). © Streamline. Some files have been modified for the symbolic-tinting pipeline; details inthemes/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. © 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; that is the file the .deb
ships under /usr/share/doc/libltk*/copyright.
Contributing
Patches and bug reports are welcome. Read
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 — 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.