Files
ltk/docs/architecture.md
Pedro M. de Echanove Pasquin ce893ac776
Some checks are pending
CI / build + test (push) Waiting to run
CI / cargo audit (push) Waiting to run
responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
Responsive scaling. ltk now offers two first-class ways to size a UI so it adapts across screens, chosen per process via `WidgetScaling { Fluid, Physical }` (`set_widget_scaling` / `widget_scaling`, default `Fluid`). Fluid sizing (`Length::fluid( px )`) makes a design pixel a proportion of the surface's smaller side, calibrated against a reference width (`set_fluid_reference` / `fluid_reference`, 412 px default) and bounded by `FLUID_MIN` / `FLUID_MAX`; physical sizing (`Length::dp( px )`) is a constant-physical-size pixel scaled by display density (`set_density` / `density`). `Length` gains `orient( portrait, landscape )` — resolve one value in portrait, another in landscape — plus `widget( px )`, which picks fluid or dp per the active mode. Canvas exposes `geom_px` (geometry, resolved in physical layout space) and `font_px` (font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename: `set_design_reference` / `design_reference` became `set_fluid_reference` / `fluid_reference`, and `Length::dp` changed meaning — the old surface-proportional behaviour now lives on `Length::fluid`.
Widgets. Every stock widget resolves its default geometry and font through the widget-scaling mode instead of frozen pixels, so a whole UI scales coherently without per-call units. New size builders where they were missing: `button` gains `font_size` / `height`, `text_edit` gains `height` / `font_size_fluid`, `separator` gains `pad_v`, and assorted widgets accept a `Length` where they previously took only `f32`.
Overlays. `OverlaySpec::size` is now `( Length, Length )` instead of `( u32, u32 )`, resolved against the main surface when the overlay is materialized, so overlays can scale with the display; `Length::px( … )` reproduces the old fixed sizing.
API stabilization (toward 1.0). Widget struct fields are now `pub( crate )` — they are configured through builders, not field access — except the value / state types apps genuinely read or construct (`Time`, `Date`, `ComboState`), which stay public. The internal `test_support` helpers move behind a `test-support` Cargo feature (off by default, so third-party builds never see them; ltk's own `make test` enables it). `Separator` drops its `0.0`-means-mode sentinel for `Option<Length>`, so an explicit `pad_v( 0.0 )` is a real flush divider distinct from the mode-following default.
Performance guardrails. Opt-in diagnostics via `LTK_PERF_WARN=1` warn about stuck animations, sustained software-render animation, and low `poll_interval`; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap with `App::cap_software_animation`.
Docs and build. The two scaling modes are documented in README, onboarding and architecture, with the earlier gradient / backdrop doc drift cleaned up. The Makefile now ships the `locales/` directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the new `responsive` example, and runs tests with `--features test-support`.
2026-07-07 17:40:33 +02:00

288 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ltk architecture
If you are new to the library, start with [`docs/onboarding.md`](./onboarding.md)
first. This document assumes you already know how to run an example and what
kind of application surface you are trying to build.
This document covers the patterns that the small `examples/` files cannot show: how a real application is structured on top of the [`App`] trait, how multiple surfaces coordinate, how theming is consumed, how to build animations, and where the cost of a frame actually lives.
For copy-pasteable patterns the canonical references are the two downstream consumers in the Eydos workspace:
- **crustace** (`crustace/src/`) — the Eydos shell. Layer-shell background surface + 8 overlays, system polling, MPRIS, notifications, animated OSD.
- **loginmanager** (`loginmanager/src/`) — greeter. `keyboard_exclusive`, single overlay, focus management, async PAM via `set_channel_sender`.
The rest of this document explains *why* those repos look the way they do.
If you are coming from `cargo doc`, keep the public API split in mind:
- `ltk::window` — normal application windows
- `ltk::shell` — layer-shell and overlays
- `ltk::runtime` — advanced runtime hooks and runtime-free embedding
This document mostly lives in the overlap between `ltk::shell` and
`ltk::runtime`. If you only want to build a plain app window, stay with
`docs/onboarding.md` and the `ltk::window` surface first.
## Mental model
ltk is Elm-shaped. The application is a value implementing [`App`]; ltk drives the loop and the application reacts.
Every frame: ltk calls `view()` and `overlays()`, lays out the returned tree(s), draws them, and dispatches input events back as `Message` values which are fed to `update()`. There are no retained widgets. `Element<Msg>` is rebuilt from scratch every frame from the application's own state.
This sounds expensive and is actually fine. The widget tree is plain enums, the layout pass is a single recursive walk that already has to happen anyway, and as of the `WidgetHandlers` snapshot work the input dispatch path no longer rebuilds the tree per event. The only thing the app must avoid in `view()` is *I/O* (reading files, scanning directories, walking icon caches) — keep those in `poll_external` or behind a `RefCell` cache.
In practice, that model is easiest to adopt in three steps:
1. Start with the `ltk::window` mental model: one app state, one `view()`, one `update()`, one normal window.
2. Add `ltk::shell` concepts only if you need layer-shell or overlays.
3. Reach for `ltk::runtime` hooks only when you need async wakeups, invalidation narrowing, or embedding outside `ltk::run()`.
## The trait surface, by purpose
`App` looks intimidating — most of it is opt-in. Group the methods by what you actually need:
**Always implement**
- `type Message` — your message enum.
- `view(&self) -> Element<Msg>` — main surface contents.
- `update(&mut self, msg: Msg)` — state transitions.
**Implement when your app is multi-surface**
- `overlays(&self) -> Vec<OverlaySpec<Msg>>` — see *Surface composition* below.
**Implement when your app is a shell component, not a window**
- `shell_mode()``ShellMode::Layer( Layer::Background | Bottom | Top | Overlay )`.
- `layer_anchor()`, `layer_size()`, `exclusive_zone()`, `keyboard_exclusive()` — the layer-shell knobs.
- `background_color()``Color::rgba( 0, 0, 0, 0 )` for transparent surfaces (panels, OSDs).
**Implement when external state matters**
- `set_channel_sender(sender)` — saved once at startup; clone into background threads to push messages into the loop without polling.
- `poll_external() -> Vec<Msg>` — called after every Wayland event *and* every `poll_interval()` tick. Drain receivers here.
- `poll_interval()``None` (event-driven only) or `Some( Duration )` (timer wakeups for clocks, expiry, etc.).
**Implement when input gestures matter**
- `on_swipe_up`, `on_swipe_down`, `on_swipe_progress`, `on_swipe_down_progress` (follow-the-finger).
- `on_tap` — taps that miss every widget.
- `on_key` / `on_key_with_modifiers` — global hotkeys.
- `swipe_threshold`, `swipe_down_threshold` — gesture sensitivity.
**Implement for animations and focus**
- `is_animating()` — return `true` while a tween is running; the loop redraws at ~60 Hz.
- `take_focus_request()``Option<WidgetId>` — pull-once focus retargeting.
- `on_text_input_focused(active)` — surface IME state.
The defaults for everything else are sensible enough that a minimal app overrides only the four methods in the first group.
Another way to read the trait is by API layer:
- `ltk::window`: `view`, `update`, plus the widgets/layouts you use to build the tree.
- `ltk::shell`: `shell_mode`, `layer_anchor`, `layer_size`, `exclusive_zone`, `keyboard_exclusive`, `overlays`.
- `ltk::runtime`: `set_channel_sender`, `poll_external`, `poll_interval`, `invalidate_after`, `take_focus_request`, `is_animating`, and `core::UiSurface`.
That is the intended order of adoption for third-party users.
## Surface composition
The main surface is what `view()` paints. `overlays()` returns a `Vec<OverlaySpec<Msg>>` describing additional layer-shell surfaces that should exist this frame. The runtime diffs that list against the previous frame using [`OverlayId`]:
- Same id present last frame and this frame → keep the surface alive, only re-render its `view`.
- New id → create a new layer-shell surface.
- Id missing → destroy the surface.
This is why crustace declares stable `const OVERLAY_LAUNCHER: OverlayId = OverlayId(1)` etc. at the top of `app.rs`. Don't allocate ids dynamically — diffing relies on stability.
Each overlay carries its own `view`, `anchor`, `size`, `layer`, `keyboard_exclusive`, `input_region`, and `on_dismiss`. The `Message` type is shared with the main app: a button inside an overlay produces the same `Msg` that a button on the main surface would, and `update()` handles both. There is no per-overlay state machine — overlays are pure projections of `App` state.
`on_dismiss` is fired by three independent paths: a `popup_done` event from the compositor (xdg-popup mode); a pointer / touch press on the main surface that does not land on the trigger pointed at by `anchor_widget_id` while the overlay is mapped (covers compositors that route the button to the parent surface instead of breaking the popup grab); and Escape pressed while at least one xdg-popup overlay is open. The application only has to flip its `is_open` flag to `false` in `update()`; the runtime tolerates the message arriving more than once for the same open / close cycle.
Common patterns:
- *Modal panel*: `layer: Overlay`, `anchor: ALL`, `keyboard_exclusive: false`, `on_dismiss: Some( CloseMsg )`. Tap-outside dismisses; the panel itself centers via `column().push(spacer()).push(panel).push(spacer())`.
- *Pass-through OSD*: same as above but `input_region: Some(Vec::new())` so pointer events fall through to whatever is below.
- *Top bar / dock*: `layer: Top` or `Bottom`, `anchor: TOP`/`BOTTOM`, fixed `size`, non-zero `exclusive_zone` so app windows reflow around it. Usually returned from `view()` (single-purpose shell), not from `overlays()`.
- *Greeter / lock screen*: `shell_mode: Layer(Overlay)`, `keyboard_exclusive: true`. Loginmanager is the reference.
Overlays do not nest. A "submenu inside the quick settings panel" is just a second overlay with a different id whose `view()` builds the submenu. Crustace uses this for the WiFi and Bluetooth pickers.
If your application does not need overlays or layer-shell, you can ignore this
entire section and stay in the `ltk::window` subset.
## Theming
`ltk::theme` exposes a process-wide active theme. Three layers:
1. **Document** — a [`ThemeDocument`] loaded from disk (`/usr/share/ltk/themes/<id>/theme.json`). Each document carries a `light` and `dark` [`Mode`] with a typed [`SlotStore`] (colors, paints, shadows, surfaces, text styles), wallpaper/lockscreen/launcher specs and a shared `fonts` block. When the `default` document cannot be located ltk falls back to an embedded B/W theme + embedded Sora Regular font, logs a stderr warning, and stamps every frame with a red banner pointing at the `ltk-theme-default` Debian package so the missing-theme signal is visible without the process aborting. `ltk::is_fallback_active()` exposes the state for apps that want to react programmatically.
2. **Mode** — [`ThemeMode::Light`] or `Dark`; flips which mode of the document is active.
3. **Active state**`ltk::active_document()` / `ltk::active_mode()` return the current pair. Per-slot shorthands (`ltk::theme_color`, `theme_paint`, `theme_shadows`, `theme_surface`, `theme_text_style`, `theme_palette`, `theme_window_controls`, `theme_wallpaper`, `theme_lockscreen`) cover the common patterns.
Inside a widget tree, read the palette through the per-slot helper:
```rust,no_run
# fn _ex() {
let _label = ltk::text( "Hello" )
.color( ltk::theme_palette().text_primary );
# }
```
To switch theme at runtime, dispatch a message that calls `ltk::set_active_mode( ThemeMode::Dark )` from `update()` and let the next frame re-resolve. There is no manual invalidation step.
Loading a different document:
```rust
let doc = ltk::ThemeDocument::find( "default" )
.expect( "default theme not installed (ltk-theme-default)" );
ltk::set_active_document( doc );
```
For dev iteration set `LTK_THEMES_DIR=/path/to/ltk/themes` so the lookup picks files in the working tree before the system path. The full search order is:
1. `LTK_THEMES_DIR/<id>/` when the env var is set
2. `$XDG_DATA_HOME/ltk/themes/<id>/` (defaults to `~/.local/share/ltk/themes/<id>/`)
3. `/usr/share/ltk/themes/<id>/`
Wallpapers ship as a single landscape PNG per variant. `ltk::WallpaperBundle::from_path_or_bytes( path, bundled_fallback )` handles the disk-or-builtin fallback, and `bundle.for_size( sw, sh )` returns the right crop for landscape *or* portrait surfaces — no need to ship two PNGs.
For many third-party apps, theming is optional at first. It is reasonable to
start with the default theme and come back to the runtime theme APIs later as
part of the `ltk::runtime` layer.
## Responsive sizing
Every size in a widget tree is a `Length`, resolved to concrete pixels at layout time against the surface. Two coordinate spaces matter. **Geometry** (widths, heights, paddings, gaps, box sizes) is computed in *physical* pixels — the layout root rect is `pw × ph` — so geometry `Length` values resolve against `Canvas::viewport_layout()` (physical). **Font sizes** are the exception: they resolve against `Canvas::viewport_logical()` (physical ÷ `dpi_scale`) and are multiplied by `dpi_scale` again at raster time, so a `vmin` font ends up as a fraction of the *physical* short side regardless of `dpi_scale`. Keep this split in mind when adding a widget: resolve a geometry constant with `Canvas::geom_px(n)` and a font constant with `Canvas::font_px(n)` — the two helpers hide the difference.
`ltk` offers two adaptation strategies, and both live in the same `Length` type so an app can mix them per value:
- **Fluid** (`Length::fluid(n)`, and the raw `vmin` / `vmax` / `vw` / `vh` / `orient` units): surface-proportional. `fluid(n)` reads a single design pixel `n` as `vmin(n / fluid_reference() * 100).clamp(n * FLUID_MIN, n * FLUID_MAX)` — at a surface whose short side equals the reference (412 px by default) it is exactly `n`, and it scales with the short side elsewhere, auto-clamped to `[0.7n, 1.5n]`. This tracks the width in portrait and the height in landscape, because the short side *is* the width in portrait and the height in landscape. `orient(portrait, landscape)` is the escape hatch for a different percentage per orientation.
- **Physical** (`Length::dp(n)`): constant physical size. `dp(n)` is `n × density()`, where `density()` is a process-wide factor (default `1.0`, typically set from the output DPI via `set_density`). It does not scale with the surface, only with pixel density — the mainstream HiDPI `dp`.
Stock widgets do not hard-code either strategy. Each carries a design pixel per dimension (e.g. `button` height 48, font 16) and resolves it through the process-wide `WidgetScaling` mode: `Length::widget(n)` returns `fluid(n)` under `WidgetScaling::Fluid` (the default) or `dp(n)` under `WidgetScaling::Physical`. `set_widget_scaling(mode)` flips it once for the whole app. An explicit `Length` on an individual widget (`button.height(...)`, `text_edit.height(...)`, `font_size(...)`) bypasses the mode entirely — the mode only decides the meaning of the *default* design pixels, never an override the app wrote on purpose.
Both `density()` and `widget_scaling()` are process globals read during layout; set them at startup (or, for density, whenever the surface moves to an output with a different DPI). Because they are global, ltk's own test suite serialises the tests that touch them.
## Animations
The render loop is event-driven by default: it sleeps until input arrives, a `poll_interval` ticks, or `set_channel_sender` is woken from a thread. To run a tween, override `is_animating()`:
```rust,no_run
# struct App { toast: Option<()>, nav_progress: f32 }
# impl App {
fn is_animating( &self ) -> bool
{
self.toast.is_some() // an OSD is fading
|| self.nav_progress < 1.0 // a screen is sliding
}
# }
```
While `is_animating()` returns `true`, ltk redraws at ~60 Hz. Do *not* mutate state in `view()`; instead read `Instant::now()` against a stored start time and compute the tween value:
```rust,no_run
# use std::time::Instant;
# use ltk::Element;
# const TOAST_DURATION: f32 = 3.0;
# #[ derive( Clone ) ] enum Msg {}
# struct App { toast_started: Option<Instant> }
# impl App {
fn view( &self ) -> Element<Msg>
{
let progress = match self.toast_started
{
Some( t ) => ( t.elapsed().as_secs_f32() / TOAST_DURATION ).min( 1.0 ),
None => 0.0,
};
// … fade alpha = 1.0 - progress
# ltk::text( "" ).into()
}
# }
```
The end-of-animation cleanup belongs in `poll_external()`: when `progress >= 1.0` clear `self.toast_started` so `is_animating()` returns `false` and the loop sleeps again.
For follow-the-finger gestures use `on_swipe_progress(progress)` / `on_swipe_down_progress(progress)`. Those fire continuously during the drag with a `0.0..=1.0` value and don't require `is_animating` — the gesture itself drives the redraw.
For a basic application window, defer this whole area until the rest of the UI
is already working. Animation is part of the advanced runtime surface, not the
core onboarding path.
## Larger state patterns
A four-button demo can keep all state in one struct and one flat `Msg` enum. Anything bigger needs structure. Conventions used by crustace and loginmanager:
**One module per screen / panel.** Each module owns its sub-state struct and its sub-message enum, and exposes `fn view(...) -> Element<AppMsg>` and `fn update(&mut self, msg: SubMsg)` (or the parent inlines those calls). See `crustace/src/homescreen.rs`, `launcher/`, `notifications.rs`, `powermenu.rs`.
**Wrap sub-messages in the top-level enum.** `enum AppMsg { Home(HomeMsg), Settings(SettingsMsg), Nav(Route), Tick }`. `update()` matches the outer variant, then forwards to the right sub-module. This avoids one-giant-message-enum bloat once the app passes ~30 variants.
**Ephemeral caches behind `RefCell` (single-threaded).** `view(&self)` is `&self`; if you need a mutable icon cache, scaled-image cache, layout cache, etc., wrap it in `RefCell<...>` on the app struct and `borrow_mut()` inside `view()`. Crustace's `IconCache` does exactly this. Don't reach for `Mutex` — the event loop is single-threaded.
**External state via channel + poll.** Anything that blocks (D-Bus, files, network, IPC) lives on a background thread. At startup save the `ChannelSender<Msg>` from `set_channel_sender`, hand a clone to the worker, and have the worker push messages back. `poll_external()` is the place for non-blocking `try_recv()` against in-process receivers (e.g. `mpsc`/`crossbeam` channels) or for expiry checks like "is this notification past its TTL".
**Stable widget ids only when you need to programmatically focus them.** `WidgetId` is an opt-in tag on a widget that pairs with `App::take_focus_request()`. Don't decorate every widget; tag the one input you want to autofocus on screen entry.
Again, the simplest progression is:
1. one flat app state in `ltk::window`
2. sub-state and overlays once the app becomes shell-like
3. caches, channels, focus retargeting, and cross-surface invalidation only when scale requires them
## Performance
The cheap things and the expensive things, in rough order:
- *Cheap*: building the `Element<Msg>` tree. It's plain enums and `Vec`s. crustace rebuilds the entire shell every frame and stays idle when nothing changes.
- *Cheap*: input dispatch. Per-leaf handler snapshots are captured during the layout pass; pointer/key events are O(N_focusable_leaves) lookups, not tree walks.
- *Cheap*: `active_document()` / `theme_palette()`. The first returns a clone of an `Arc<ThemeDocument>` from a `RwLock`-protected cell; the second projects the active mode's slot table onto the eight canonical palette fields.
- *Avoid in `view()`*: filesystem walks, image decoding, `serde` parsing, regex compilation. Cache the result on the app struct (behind `RefCell` if needed) and look it up.
- *Avoid in `view()`*: cloning large `Vec<u8>` image buffers. `img_widget` takes an `Arc<Vec<u8>>`; build the `Arc` once at load time and clone *the Arc*, not the bytes.
- *Avoid `is_animating() = true` when nothing is moving.* It pegs the loop at 60 Hz and burns battery on the mobile target.
- *Lower `poll_interval()` is not free.* Crustace polls every 30 s because the clock only shows HH:MM. If your UI shows seconds, `Some(Duration::from_secs(1))` is fine; if it shows nothing time-sensitive, leave it `None`.
- *Scroll viewports own a sub-canvas.* They're slightly more expensive to draw than a plain column. Use them when you need clipping or actual scrolling, not as a wrapper.
- *GPU vs software*: the GLES path is selected automatically when EGL is available, with no API-level difference for the application. The two backends are not yet pixel-identical — gradients and the shadow / backdrop pipeline degrade under software; the authoritative list of gaps is the README's *Backend Differences* section.
When a redraw feels sluggish: add a one-line print at the top of `view()` and confirm it's not being called more often than expected. The single most common mistake is leaving `is_animating()` returning `true` after the animation finished.
**Runtime guardrails.** The rules above are the app's responsibility, but the runtime also helps catch and blunt the common footguns. Set `LTK_PERF_WARN=1` to get one-shot stderr diagnostics during development when `is_animating()` stays true for 10 s (a settled animation that forgot to return `false`), when `poll_interval()` is under 100 ms (defeats the idle model), or when the software backend animates continuously for seconds (mobile CPU sink). Independently, animation on the **software** renderer is capped to ~30 Hz by default — GLES is never capped — since 60 fps software rasterization is a battery drain with no GPU offload; override [`App::cap_software_animation`] to keep the full rate. These live in `event_loop/perf.rs`.
## Where to look in the consumer repos
| Pattern | File |
| --- | --- |
| Multi-overlay coordination, overlay id constants | `crustace/src/app.rs` (`overlays()`, lines ~250380) |
| Background poller + channel sender | `crustace/src/app.rs` (`set_channel_sender`, `poll_external`) |
| Sub-module per screen | `crustace/src/{homescreen.rs, notifications.rs, powermenu.rs, launcher/}` |
| Cached icon loading via `RefCell` | `crustace/src/launcher/icon_cache.rs` and use sites in `app.rs` |
| OSD overlay with auto-expiry | `crustace/src/app.rs` (`show_osd`, `build_osd`, `OSD_TIMEOUT_SECS`) |
| `keyboard_exclusive` + `take_focus_request` | `loginmanager/src/main.rs` |
| Theme on disk (slot-typed JSON) | `ltk/themes/default/theme.json`, `ltk::ThemeDocument::find` |
For a self-contained example that exercises overlays, theme switching, and animation in one ~300-line file, see `examples/mini_shell.rs`.
## Known gaps and non-goals
A short, honest list of what ltk does not currently provide. None of these are accidental — each is either deferred work or a deliberate non-goal. The list is here so that downstream consumers and audit reviewers know what to plan around without reading the source.
**AT-SPI2 / assistive technology bridge — wired through AccessKit, with composite widgets still flat.** Combo, Notebook tabs, DatePicker and TimePicker render as collections of inner widgets and currently expose those leaves individually (a combo trigger reads as "Button" + its caption, the popup items as `ListItem`s inside an overlay). Promoting them to their semantic roles (`ComboBoxMenuButton` with `Expanded` state, `TabList`/`Tab`/`TabPanel`, `Date`) needs each compound widget to declare an "outer rol hint" the layout pass can attach to the `LaidOutWidget` it pushes. Tracked separately.
ltk delegates the AT-SPI2 D-Bus protocol to [`accesskit_unix`]. After every layout pass, the runtime hands the platform adapter a fresh [`accesskit::TreeUpdate`] built from `widget_rects` (one accessible node per laid-out widget, plus a `Window` root). Buttons / toggles / checkboxes / radios / list items map to `Role::Button` / `Role::Switch` / `Role::CheckBox` / `Role::RadioButton` / `Role::ListItem`; sliders to `Role::Slider`; single- and multi-line text edits to `Role::TextInput` / `Role::MultilineTextInput`. Each interactive node advertises the `Click` and `Focus` actions, and inbound action requests (Orca pressing a button, switch-control moving focus) are translated into a synthetic press / focus on the matching widget the next iteration of the run loop.
The integration is best-effort: when the AT-SPI2 daemon is not on the session bus (headless CI runners, locked-down compositors) the adapter creation returns `None` and the rest of the pipeline runs unchanged. The current cut covers the common cases — buttons, lists, form fields, dialogs — and intentionally leaves room for follow-up:
- **Hierarchical nodes (groups, lists with explicit children)**: today the tree is flat. AccessKit supports nesting but the layout pass does not expose `Column` / `Row` / `Container` parents to the accessibility builder. Adding that requires either recording the nesting on `LaidOutWidget` or walking `Element` again from the a11y side.
- **Per-widget accessible label / description / `LabelledBy` relations**: the API to set them (`Button::accessible_name(...)`, etc.) is not exposed yet — labels currently fall back to the widget's tooltip text. Adding the builders is mechanical but touches every widget module.
- **Live regions**: status messages, notifications and OSDs that should announce themselves on appearance need `Live::{Polite, Assertive}` on the relevant nodes. Not wired in.
- **`Action::SetValue` for sliders and text inputs**: the inbound action handler does not yet translate these requests into the corresponding widget message. Adding them needs the same plumbing as `Click` / `Focus` but with payload extraction from `ActionData`.
Downstream consumers shipping into regulated environments (EN 301 549, WCAG 2.1 AA, EAA) should still treat the integration as a starting point that needs a real audit with assistive technology users — the foundation is in place but the per-widget metadata work is what determines whether Orca actually reads a useful announcement.
**Cross-application drag-and-drop — deferred.** The clipboard now bridges to the Wayland selection via `wl_data_device_manager` (see `event_loop/data_device.rs`), so Ctrl+C / Ctrl+V crosses application boundaries when the compositor advertises the global. Middle-click primary selection (`zwp_primary_selection_v1`) and inter-app drag-and-drop targets (drop-zone widgets that accept text / URI lists from outside the process) are still pending — they share most of the offer / source plumbing but need widget-level drop-target wiring on top.
**Multi-touch — deferred.** `input/touch/mod.rs` is single-slot by design today; a second finger overwrites the first. Pinch-zoom, two-finger scroll and gesture combos are out until the slot table lands. Tracked separately.
**HarfBuzz shaping — wired in.** `src/text_shaping.rs::shape_line` now drives both renderers: the line is BiDi-reordered, split into per-font sub-runs and shaped through rustybuzz. The glyph cache is keyed on `(glyph_id, size_bits, font_id)` and each glyph is rasterised by index via `fontdue::Font::rasterize_indexed`, so Arabic connected forms, Devanagari clusters and CJK shaped glyphs render correctly.
**xdg-activation-v1 and fractional scale — deferred.** Activation tokens (so an external launcher can raise an ltk window with focus) and `wp_fractional_scale_v1` (so 125 % / 150 % outputs render natively instead of via compositor downscale) are tracked as upcoming protocol work.