Commit Graph
15 Commits
Author SHA1 Message Date
admin 02ab10b3ef State restored on an ordinary launch for apps that ask, focus that follows its widget's id, buttons that do not take the focus when pressed, a focus ring only after the keyboard, and a browser app's surfaces copied rather than transferred, which WebKit got wrong
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
Restore on launch. `restore_state` follows Android's saved-instance-state contract: the saved bytes come back after a crash or a session restore, and a launch from the launcher starts fresh. Some apps keep state their users expect to find however the last run ended, as an Android browser keeps its tabs in its own storage even when closed from the app switcher. `App::restore_on_launch`, false by default, has the runtime restore on a plain launch too; `SessionRuntime::bootstrap` asks it before deciding. Saving, the file layout and the window geometry are the same either way. Its documentation explains when it is asked, that it is not when persistence is off, that `restore_state` must then merge what the launch itself asks for, such as a URL on the command line, and that what `save_state` returns now outlives every exit; it carries a compiled example. `restore_state`'s documentation, `docs/onboarding.md` and the cookbook's session state recipe mention it.
Focus on a press. `Button::focusable( false )` and `WindowButton` kept out of the Tab cycle but still took the focus when pressed, and Enter then pressed them again, which closed a browser's tab when the user pressed Enter in a page after closing another tab. `LaidOutWidget` gains `focus_on_press`, from `Element::focus_on_press`: a button's `focusable`, a `WindowButton`'s, a pressable's new `Pressable::focus_on_press`, true by default, and true for every other widget. `focus_pressed` replaces `set_focus` for pointer and touch presses and leaves the focus where it was when the widget does not take it. Enter's fallback to the widget under the pointer, which `hovered_idx` follows, skips such widgets too, and a rebuild that leaves the focus on one drops it (`drop_misplaced_focus`). The close buttons of `tab_strip`, on the desktop and on the phone's cards, are not focusable; `symbol_button` takes whether its button is.
Focus by id. The focus was held as the index of a widget in the laid-out tree, so a rebuild that inserted or removed widgets before it moved it to whatever widget took that index. `WidgetId` becomes a name and a key, `WidgetId::keyed( name, key )` telling apart widgets of the same kind, such as the tabs of a strip; `WidgetId( "name" )` still builds one, through a `const fn` of that name, so the calls in every app compile unchanged, and only code that read `.0`, the toasts, the combo and the tooltip here, moves to the derived `Hash` or `name()`. After each layout `follow_focused_id` finds the focused id and moves the focus to its new index with the field's pending text, cursor, selection and scroll; it drops the focus when the widget is gone, keeps it when a scroll only clipped it, and leaves it when the widget at the old index still has the id. `draw_surface` returns whether the focus moved, and its callers draw the surface again rather than marking it done, since the frame just drawn showed the focus in the old place. Widgets without an id keep the index. Each tab and each card of `tab_strip` carries `WidgetId::keyed` with the tab's key, so a chosen tab keeps the focus while others close.
Focus ring. A press that focused a button, a toggle or a list item left its ring drawn, which browsers have not done since `:focus-visible`. `SurfaceState::focus_visible` is set by Tab and Shift+Tab and cleared by a press that moves the focus, `show_focus_ring` redrawing the whole surface when it changes, since the partial redraw only sees indices. The draw is given the focused index only while the ring shows or the widget is a text field, which draws its cursor however it was focused; the focus itself is unchanged, so Enter and Space still press the widget.
Key repeat. A key repeated by the platform rather than by ltk's own timer reached the focused widget even while `App::claims_keyboard` was true; `key_repeated` checks it as `key_press` does.
Browser surfaces. Every surface is painted through the app's one WebGL context on one `OffscreenCanvas`, and was handed to its own canvas, a `bitmaprenderer`, with `transferToImageBitmap`. WebKit reuses the buffer of a bitmap it was handed, so in a WebKit browser each surface showed another's frame, or none, and only the last one painted was right: forge/web's background, dock and windows came and went in a WPE browser while Chromium showed them all. A test page with two canvases fed from one offscreen canvas showed their colours swapped in WPE and right with a copy. Each surface's canvas is now a `2d` one, and `present_copy` copies the offscreen canvas with `drawImage` in `copy` composition, set before every copy since resizing a canvas resets its context, so a transparent frame replaces the last rather than leaving trails behind an animation. Subsurfaces go the same way. `WebSurfaceState` loses `bitmap` and `new` its `gpu` argument, and web-sys loses the `ImageBitmap` and `ImageBitmapRenderingContext` features.
CHANGELOG records each of these.
Not done. The tests that build `LaidOutWidget` by hand set the new field, but no test covers a press on a non-focusable button, the focus following an id or the ring. For one frame after a rebuild moves the focused widget, its ring and its cursor may show in the old place.
2026-11-01 09:19:23 +01:00
admin cd1a233b2e A browser app joins the page's desktop: on a page with forge/web its main surface, overlays and subsurfaces are surfaces of that desktop, all painted through one WebGL context
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
The browser backend ran an app alone on a canvas over the page. An Eydos desktop in a browser needs crustace as its shell and other ltk apps as its windows on one page, managed by forge/web (forge's web/ directory), which publishes `globalThis.forgeWeb` and stacks, places and focuses the surfaces its clients ask for (forge/web/DESIGN.md).
The desktop's client. On a page where `globalThis.forgeWeb` exists and `set_canvas` named no canvas, `platform/web/desktop.rs` connects as the app's `app_id` and asks for the main surface in the role the app's hooks give, as a Wayland compositor reads them: `shell_mode`, and for a layer `layer_anchor`, `layer_size`, `exclusive_zone` and `keyboard_exclusive`, for a window `app_id` and `window_size_hint`, and for `SessionLock` a full overlay that holds the keyboard. The app draws into the canvas forge/web gives and listens on the input element above it, takes its size and scale from forge/web's `configure` rather than from the window's `resize`, sends its input region in logical pixels when it changes, and leaves the keyboard focus to the desktop, which honours the surfaces that hold it exclusively. A `close` of the main surface reaches `App::on_close_requested`; on exit the surface is destroyed and the connection closed. The new `RunError::Desktop` says forge/web refused the main surface. Fullscreen inside a desktop does nothing yet. A page without a desktop, or one that named a canvas, runs the app as before.
One WebGL context. A page holds few WebGL contexts, and the browser loses the oldest beyond them, while a shell has many surfaces. Every surface of an app now paints through one WebGL2 context on an `OffscreenCanvas`: the surface's FBO is blitted onto it, taken as an `ImageBitmap` and handed to the surface's own canvas, a `bitmaprenderer`, without a copy. Without WebGL2 each surface draws in software on a 2D context of its own. The context's loss is heard on the `OffscreenCanvas`, and now drops the GPU canvases of every surface, not only the main one's.
Overlays. `platform/web/overlays.rs` keeps the app's overlays in step with `App::overlays()`, as the Wayland backend's `overlays_reconcile.rs` does with layer-shell: a new one becomes a layer surface or, for `Layer::Window`, a window, with its size and exclusive zone in logical pixels; a changed one gets an `update_role`; one that went is destroyed; forge/web's `close` of one delivers its `on_dismiss`. Popups anchored to a widget and the tooltip wait for a popup role forge/web does not have, and without a desktop an app still shows no overlays. What every backend does as overlays come and go, resolving an `OverlaySpec::size` and moving a drag and the focuses off an overlay that goes, moved out of the Wayland backend into `event_loop/overlays.rs` (`AppData::overlay_size`, `AppData::remove_overlay`); Wayland's behaviour is unchanged.
Subsurfaces. `platform/web/subsurfaces.rs` makes each of `App::subsurfaces()` a surface of the desktop sized to its parent with an empty input region, painted on one shared GPU canvas only when it appears, its size changes or its `content_version` does, its frame then staying on its own canvas, and otherwise moved with an `update_role`, which the browser composes without painting. `App::on_subsurface_committed` follows each paint. A subsurface whose parent overlay was destroyed and made again is made again, since forge/web destroys a subsurface with its parent. `frame_presented` now knows whether a surface hosts subsurfaces, so while the app animates by subsurface motion only its frames keep coming without painting the parent, as Wayland keeps a parent's frame callbacks. Painting a view with no focus, hover or press on a canvas of its own, which both backends' subsurfaces do, moved out of the Wayland backend into `draw::paint_detached`.
Input per surface. `input::listen` takes the surface's `SurfaceFocus`, which pointer, touch and keyboard focus events carry; a surface's blur no longer takes the keyboard from the surface focused after it. Listeners remove themselves when dropped, and a surface's live in its `WebSurfaceState`, so they go with it. The frames owed at the next animation frame are a list of surfaces instead of one, which with several surfaces left the main one waiting on a frame that never came.
`examples/web_shell.rs` is a shell for the desktop: a background, a bar in the top layer with an exclusive zone, a panel that takes the keyboard, and a card on a subsurface that slides. `make web-example` builds it with web_hello (`WEB_EXAMPLES`); forge/web's `make hello` serves both at /shell.html. web-sys gains `OffscreenCanvas`, `ImageBitmap` and `ImageBitmapRenderingContext`. `CHANGELOG.md` describes the change.
Not done. Popups and the tooltip in a desktop, overlays and subsurfaces without one, fullscreen and window moves through forge/web, and the cursor's shape over an overlay, which is still set on the main surface's element. The offscreen canvas is resized for each surface it paints, which reallocates its drawing buffer when the surfaces differ in size. There are no tests for the browser backend.
2026-10-19 22:42:12 +02:00
admin 20dc21448e A browser backend: ltk runs on a page's canvas through WebGL2, on a core that builds for wasm32, and a surface's scale may be fractional
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
ltk ran on Wayland and Android only. Running crustace and its apps in a browser needs, first, ltk apps running there; a window manager for the page is the next step. The runtime was already split from Wayland by the platform layer, so what kept it out of a browser was what the core and the runtime took from the operating system: calloop's epoll loop, EGL, a monotonic clock, a file system, threads and environment variables.
The core builds for wasm32-unknown-unknown. `ltk::ChannelSender` is ltk's own type instead of a re-export of calloop's `Sender`, with the same `send` and `Clone`, so apps that only send and clone it build unchanged. The runtime arms its timers and channels through `event_loop/sources.rs` instead of a calloop `LoopHandle`: calloop's natively, and in a browser a queue the backend runs, which takes the due timers out, runs them on `AppData` and hands them back, and does not arm again one removed meanwhile. calloop, khronos-egl and raw-window-handle are dependencies off wasm32 only, where `egl_context.rs`, `UiSurface::try_new_gles` and `from_current_gles_loader` and the native GL names of `BorrowedGlesTexture` are left out; `sub_canvas` clones the uniform locations, which are `Copy` natively and only `Clone` on wasm. The monotonic clock is `crate::time::Instant`, `web_time`'s on wasm32, where `chrono` takes its `wasmbind` feature; the one subtraction that could go below the clock's origin, a surface's first `last_draw`, saturates. Themes, fonts, keyboard layouts and images are read through `vfs.rs`, the file system natively and on wasm32 files held in memory, so a wallpaper or an `Image::from_path` is now decoded from the file's bytes and its format told from its contents rather than its extension. The text-scale watcher, which runs gsettings on a thread, is left out on wasm32, and the built-in keyboard takes its language from `platform::language()`, `LANG` or the page's. criterion is a dev-dependency off wasm32 only, since rayon does not build there.
The browser backend is the `web` feature, `platform/web/`, with wasm-bindgen, wasm-bindgen-futures, js-sys and web-sys. `build.rs` refuses it with another backend or for another target. A page's main thread must not block, so `ltk::run` sets the app up and returns, and the browser drives the loop: `AppData` lives in a closure a thread-local loop holds, which a `setTimeout` runs for messages, timers and page events and a `requestAnimationFrame` runs to draw. Event listeners and `ChannelSender::send` only queue and schedule, never touching `AppData`, so nothing they set off runs inside an iteration. The app draws on a canvas laid over the whole page, or the one `ltk::web::set_canvas` gives, through WebGL2 with the GLES canvas unchanged, its frame kept in its FBO, or without WebGL2 through `putImageData`. A lost context drops the GPU canvases and draws nothing until it is restored on a new `glow::Context`, so the glyph atlas and the caches of the old one are not reused. The main surface's logical size is the canvas's CSS size; the window's `resize` refits it, and on ltk's own canvas a hidden element padded by `env( safe-area-inset-* )` gives the insets. Input comes from Pointer Events, a mouse as the pointer and fingers and pens as touches, the wheel, in detents when it scrolls lines, and keys, whose keysym comes from `KeyboardEvent.key` and code from `.code` as the Linux input code Wayland reports; keys repeat on the browser's clock, AltGr types its character, and on macOS Command is Ctrl. ltk takes every key the browser lets a page have, and while fullscreen those `navigator.keyboard.lock()` adds in Chromium. Copies go to `navigator.clipboard.writeText`; a page reads the clipboard only from a `paste` event, so Ctrl+V is let through to the browser and held until its `paste` has put the text in `AppData::clipboard`, and a paste from the browser's menu comes with a Ctrl+V of its own. The cursor's shape is the canvas's CSS `cursor`. The built-in keyboard serves text where the page is used by touch. Panics go to the browser's console. When the app exits, the listeners are removed, the canvas receives an `ltk-exit` event and the app is dropped. The new `RunError::NoDocument` is returned where there is no page, as in a worker.
Resources are the tree an Android package carries. `scripts/web-assets.sh` builds it through `android-assets.sh`, and `ltk::web::load_resources( base ).await` fetches its `MANIFEST` and every file it lists, with the manifest's hash in their URLs so a changed tree is not served from the browser's cache, into memory under `/ltk`, which becomes the resource directory. The per-script fallback fonts are read through `resources::read_font`, so a path that does not exist is found by file name in the resource directory's `fonts/`, where a page bundles them.
A surface's scale may be fractional. `SurfaceState::scale_factor` is an `f32`, which `Platform::prepare_canvas`, `Presentation::scale` and the paint path take as it is; a surface's physical size is its logical size times the scale, rounded once, and `buffer_scale()` gives the whole number Wayland's buffer scale and regions need. Wayland and Android still set whole scales, so nothing changes there; the browser backend sets the page's `devicePixelRatio`, so the canvas holds the display's own pixels at any zoom. `App::on_scale_changed` keeps its `u32`, the scale rounded, and `App::on_resize` gives the exact physical size.
The fallback-theme banner placed its text as if `draw_text` took the top of the line rather than its baseline, so most of the text fell above the banner, out of the surface; it is now centred as the title bar's is.
Building. `make web-check` typechecks the backend. `make web-deps` installs Debian's std for wasm32 and wasm-bindgen-cli, whose version must be the one of the wasm-bindgen crate in `Cargo.lock`: the crate is pinned to 0.2.117, the last whose CLI builds with Debian's rustc 1.85, and since the lock the CLI was published with needs a newer rustc and `cargo install` ignores rust-version when it resolves, the CLI is built from its sources with a lock resolved for the rust-version it declares, into `target/tools`, where the Makefile finds it. `make web-assets` builds the resources with the in-tree default theme and the fallback fonts `WEB_FONTS` names. `examples/web_hello.rs`, a counter and a text field, and `web/index.html` are the smoke test: `make web-example` builds them into `target/web/` and `make web-serve` serves them. `make install` now copies `web/` into the registry. `README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the backend, and `docs/cookbook.md` points to `ChannelSender`'s new home.
Not done. In a browser there are no overlays, secondary surfaces or subsurfaces, no dead keys or input methods, no accessibility tree and no primary selection, and a canvas that changes size while the window does not is not refitted. Pasting relies on the browser sending `paste` to a focused canvas, which Chrome and Firefox do. The page fetches its whole resource tree, about 7 MB, before the app starts. There are no tests for the browser backend, `Sources`' queue or `vfs.rs`'s in-memory files, and fractional scales may leave one-pixel artefacts where edges fall between pixels.
2026-10-19 20:20:12 +02:00
admin cacf871ddd An Android backend, a platform layer that keeps Wayland out of the runtime, the built-in keyboard for an app's own text input, and APKs packaged with Debian's own tools
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
ltk only ran on a Wayland compositor: the runtime, the input handlers and the draw paths named smithay-client-toolkit's types throughout, so no other windowing system could be added without touching all of them. Prompt, the Eydos terminal, has to run on an Android phone, with its sessions and its sharing, and needs a toolkit that runs there.
The platform layer. Input reaches the runtime as platform-neutral events: `input/event.rs` defines `KeyInput`, `PointerInput` and `TouchInput`, and the pointer, touch and keyboard handlers take them through a few entry points (`pointer_input`, `touch_input`, `key_press`, `key_release`, `key_repeated`, `modifiers_changed`, `keyboard_enter`, `keyboard_leave`, `set_repeat_info`). The sctk handler implementations moved to `platform/wayland/input.rs`, which records the serials later requests need, resolves surfaces and translates. What the runtime asks of the windowing system (moving, resizing, minimising and fullscreening the window, a bare frame request, the cursor's shape, the input method, publishing the clipboard and the primary selection, preparing a canvas, presenting it and its damage) forms the `platform::Platform` trait, with its surface type as `Platform::Surface`. The protocol state moved from `AppData` into `platform::wayland::WaylandPlatform`, held as `AppData::platform`, and each surface's Wayland half (its kind, SHM pool, EGL window surface and what the compositor was last told) from `SurfaceState` into `WaylandSurfaceState`, held as `SurfaceState::platform`. The compositor, shell, seat and output handlers, the data device, the primary selection, the run loop, overlay and subsurface reconciliation, session management, output probing and window fitting moved from `event_loop/` to `platform/wayland/`. Drawing is split from presenting: `draw/paint.rs` paints a surface in full or under its dirty rects, the same for the software and GPU canvases, in place of the four near-identical paths of `draw/software.rs` and `draw/gles.rs`, and the backend presents it (`platform/wayland/present.rs`, which also took the opaque and input regions and `pick_shm_format`). The runtime is generic over the platform, `AppData<A, P: Platform = NativePlatform<A>>` and `SurfaceState<Msg, S = NativeSurface>`, resolved at compile time. The backend-neutral half of a loop iteration is `event_loop/step.rs` (`process_messages`, `draw_due`, `next_wakeup`, `before_draw`, `after_draw`), and `event_loop/setup.rs` builds the state and the event sources every backend has; the accessibility tree and its actions moved to `event_loop/a11y.rs`. `platform/mock.rs` is a platform that logs what is asked of it, and `event_loop/tests.rs` drives the runtime on it through input, update, painting and presentation. The behaviour on Wayland is unchanged, except that a frame whose presentation fails leaves the next one to repaint in full. `ltk::Keysym` now comes from `xkeysym`, the crate sctk re-exported it from, so it is the same type.
Features. `wayland`, on by default, brings the Wayland backend and its dependencies, now optional, AT-SPI2 accessibility among them. `android` brings android-activity 0.6, ndk 0.9 and jni 0.21. A new `build.rs` refuses both at once and sets `cfg( backend )` when one is on, which builds the runtime behind `run` and `try_run`. Without a backend only the runtime-free core builds: `UiSurface`, widgets, layout, theming and rendering.
The Android backend. `ltk::run` runs inside a NativeActivity once `android_main` has handed its `AndroidApp`, re-exported as `ltk::AndroidApp`, to the new `ltk::set_android_app`; without it `run` fails with `RunError::NoAndroidApp`. The first call in the process sends standard output and error, panics included, to logcat, sets `HOME` to the app's internal storage and, unless set, `SSL_CERT_DIR` to Android's CA certificates and `LANG` to the device's language, and unpacks the bundled resources; every call keeps the latest activity, since Android starts the next one in the same process. The backend draws through EGL on the activity's window, or in software into a locked window buffer, and sizes the main surface at the display's density in whole multiples of 160 dpi. Fingers and styluses become touches, a mouse the pointer, and key events keysyms and text through the device's character map. calloop's descriptor rides on the activity's `ALooper`, so the app's channel and timers keep working. Text is typed on ltk's own keyboard, since a NativeActivity gets only key events from the system's. Through JNI on the activity (`platform/android/java.rs`): copies go to `ClipboardManager` and its text is read back whenever the app gains the focus; back with nothing left to go back to minimises the app with `moveTaskToBack` or closes it, as `App::back_at_root` says, which the new `Platform::back_leaves` decides per backend; and the window's insets come from `getRootWindowInsets()` on Android 11 and later, the status bar and any cutout on top and only bars that take taps on the other edges, so there is no gap above the view nor below it in gesture navigation, and a bar of buttons still keeps its room; earlier releases fall back to the content rect. When the app's last surface closes, ltk calls `finishAndRemoveTask` and waits for the activity to be destroyed, since a second activity started in a process whose `android_main` has returned never came up; an app should end the process after `run` returns. `ltk::keep_running( Some( text ) )` starts a foreground service with `text` on an ongoing notification, so Android does not freeze the app once it leaves the screen, and `None` stops it.
Surface insets. `SurfaceState::insets` holds the logical pixels the system covers along each edge; the view is laid out inside them while the background fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too, above a bar of navigation buttons. On Wayland they stay zero.
Resources in a package. `ltk::set_resource_dir( dir )` names an app's own copy of ltk's resources and `ltk::resource_dir()` returns it, so an app also finds its own bundled files there. Its `themes/` is searched after the user's themes and before `/usr/share/ltk/themes`, its `keyboards/` before `/usr/share`, and a font a theme names by an absolute path that does not exist is read from its `fonts/` by file name, so a theme written against Debian's paths works unchanged. On Android `set_android_app` unpacks `assets/ltk/` into the internal storage and uses it as the resource directory; `assets/ltk/MANIFEST` lists the tree, since the asset manager cannot list subdirectories, and its first line, a hash of the contents, decides whether to unpack again. On Android the per-script fallback fonts come from `/system/fonts` and the primary face is the embedded Sora.
APKs. `scripts/android-assets.sh` builds that tree from installed themes, without their cursors, with the fonts each `theme.json` names and ltk's keyboard layouts. `scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]` packages an app built as a `cdylib` with Debian's aapt, zipalign and apksigner, with no Gradle and no SDK download: it writes the manifest, with the library loaded as `libmain.so`, optional permissions and a theme without an action bar, which the content rect would otherwise leave room for; links it against `android-framework-res`; adds the assets and the library; aligns and signs, creating a debug keystore when there is none. `ICON` takes an SVG, rasterised with `rsvg-convert` for every density, or a PNG. `KEEP_ALIVE=1` compiles `java/net/liberux/ltk/KeepAlive.java` with javac and `dx` from art-standalone against the platform 34 `android.jar`, against which aapt then links too, since Debian's framework-res predates the `specialUse` service type, and declares the service with `KEEP_ALIVE_USE` as its reason and the permissions it needs. `examples/android_hello.rs` is the smoke test: `make android-check` typechecks the backend with Debian's rustc, `rust-src` and NDK through `-Zbuild-std`, `make android-apk` and `make android-install` package and install the example, `make deps` and `make android-deps` install what the targets need, and `make install` now copies `examples`, `scripts`, `java` and `build.rs` into the registry.
The built-in keyboard for an app's own text input. While `App::external_text_input` asks for text and no ltk text field has the focus, the built-in keyboard shows with `App::external_text_purpose`, so a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows, comes back when the serial changes and hides when the request ends. Its keys reach the app as physical ones, pressed and released through `App::on_raw_key_event` with the panel's Ctrl and Alt held for them. Before, it only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method.
Clippy. `make clippy` is clean again: derived `Default`s replace the hand-written ones, `div_ceil`, `then_some`, struct update syntax for strokes, the map entry API in the text renderer, function paths in place of closures in the examples and tests, and fewer casts and explicit lifetimes.
`README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the features, the backend, the events and their units, and what stays with a backend.
Not done. On Android there are no overlays, secondary surfaces or subsurfaces, no dead keys from a physical keyboard and no saved state for the activity. Why a second activity in the same process never comes up was not found. The keep-alive service does not ask for the notification permission, so on Android 13 and later its notification may not show until it is granted in the app's settings.
2026-10-16 23:46:50 +02:00
admin b591950f67 Glyphs on demand: FontFace replaces fontdue, DejaVu ahead of the CJK collection in the fallback chain; App::take_clipboard_request and App::on_pointer_secondary
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
Every face fontdue opened was parsed whole: `Font::from_bytes` walks the `cmap` and builds the outline of every glyph before returning. For the theme's Sora that is a few hundred glyphs and nobody notices. For a fallback it is the whole file: the first string with one arrow or one bullet resolved through the chain to `NotoSansCJK-Regular.ttc`, and a compositor or a notification daemon that had drawn a dozen labels was suddenly holding 330 MB of outlines for 44 810 glyphs, after 1.7 s of CPU, to paint the hundred or so glyphs a screen ever shows. Measured on forge: 240 MB before the first app-switcher label, 542 MB after it, and never given back, because the table is live, not garbage.
`FontFace`. The new `src/font_face.rs` is the face type the whole crate now rasterises with, built on the `ttf-parser` that rustybuzz already brings in and on tiny-skia, which already fills paths. `from_bytes( data, index )` keeps the bytes and reads only what a face needs up front: units per em and the vertical line metrics. A `ttf_parser::Face` is rebuilt on every use rather than kept, since parsing one is table-offset arithmetic, in the noise next to the outline work it guards. Each glyph's outline is extracted the first time it is asked for, through `Outliner`, into a `tiny_skia::Path`, and cached for the process lifetime in a `HashMap<u16, Arc<Outline>>`; codepoint lookups are cached the same way. Rasterising fills that path into a `Mask` and hands back the 8-bit coverage, top row first, which is what both canvases stored before. The metrics keep fontdue's conventions, so no widget changed: sizes scale by `px / units_per_em`, `Metrics::xmin` / `ymin` are whole-pixel placements with the fractional remainder folded into the raster offset, `bounds` is the outline box in subpixels, `LineMetrics` carries ascent, descent, line gap and their sum. One thing is deliberately not what `glyf` declares: the box comes from the points the outline actually reaches, quadratic and cubic extrema included, because declared boxes may be loose and a box wider than the ink shifts the glyph on the pixel grid. Tests cover the turning-point arithmetic and the line-metric scaling. `FontFace`, `Metrics`, `LineMetrics` and `OutlineBounds` are exported, since `Canvas::font` and `font_for` hand them out.
What changed around it. `fontdue` leaves `Cargo.toml`. The two canvases, their text and setup modules, the registry, the rich text and text widgets swap `Arc<Font>` for `Arc<FontFace>` and nothing else; the glyph caches still key on `(glyph_id, size_bits, font_id)`. `FontRegistry` now reads a source into an `Arc<Vec<u8>>` and gives the face the bytes it will pull outlines from, so the registry no longer owns a second copy; its parse error names the file rather than a library message. `FontHandle` keeps `font` and `bytes` side by side, now naming the same buffer, so shaping needs no second read and the comment that explained the duplication is gone. `system_fonts` builds fallback faces from the bytes each slot already holds, and the slot stays two-staged: the bytes are read once to probe coverage with `ttf-parser`, the face is built only when a glyph in that slot is needed, and a face that is built no longer costs anything until it draws.
The chain. DejaVu Sans moves from last resort to just before the CJK collection. It owns the geometric shapes, arrows and box-drawing blocks, which are what a user interface most often needs outside its own family; behind the collection, every one of those pulled the 19 MB file in, since probing a slot for coverage reads it whole. The CJK entries stay last for the same reason.
The two hooks are for a host that embeds another UI, trust in this case, and complete the ones `App` already had. `take_clipboard_request` is the counterpart of nothing: an app could paste from the system clipboard through its text fields but had no way to put text into it. Return `Some( text )` once and the run loop stores it and publishes it as the seat's selection through the data device, the same path `handle_copy` takes for Ctrl+C in a text field, so other applications see it. `on_pointer_secondary` reports a right-button press that no widget of ltk's own answered, in physical pixels like `on_pointer_button`: `on_pointer_right_click` already dispatched a widget's `on_long_press` and the built-in text menu, and in the remaining case only dismissed an open menu; it now also tells the app, which can forward the press to an embedded view that has a context menu of its own.
Not changed. Variable fonts are read at their default instance, as before. Colour glyphs, `COLR` or bitmap, are not rendered by either rasteriser; the outline path draws what it can. The glyph caches are still unbounded per face, but they now hold what was drawn, not what the file contains.
2026-09-25 07:43:47 +02:00
admin a2752a5bd3 Handle SIGTERM/SIGINT with a sigaction self-pipe instead of a signalfd, and clear the signal mask of spawned children
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
The signalfd-based source (calloop's `signals` feature) needs SIGTERM and SIGINT blocked in every thread of the process, since the kernel hands a process-directed signal to any thread that does not block it. calloop only blocks them in the thread that creates the source, so the runtime depended on `ltk::run` being entered before the app spawned any thread — a constraint apps do not honour: crustace-notifier builds its tokio runtime and its D-Bus thread first, and in that configuration a SIGTERM lands on a tokio worker and kills the process outright, skipping the clean exit (`save_state`, `mark_clean_exit`) entirely.
The blocked mask had a second consequence: it is inherited across fork and exec, and Rust's std deliberately does not reset it in the child. Every process an ltk app spawns from a thread created after `ltk::run` therefore started with SIGTERM blocked. For the `gsettings monitor` child of the text-scale watcher this meant it never died on systemd's stop, so the unit's cgroup stayed populated until `TimeoutStopSec` (90 s) expired and systemd resorted to SIGKILL — visible as a stop job holding up every session shutdown.
`install_signal_source` now installs a `sigaction` handler for SIGTERM and SIGINT that writes the signal number to a non-blocking `O_CLOEXEC` pipe (an atomic load and a `write`, errno preserved); the read end is a calloop `Generic` source that drains the pipe and sets `exit_requested`. A handler is process-wide, so the clean exit works no matter which thread receives the signal or when it was created, and no thread blocks anything, so children inherit a clean mask. `SA_RESTART` keeps the handler from injecting spurious EINTRs into the app's own threads; the pipe wakes the event loop regardless.
`text_scale` additionally clears the child's mask in `pre_exec` before running `gsettings`, as a guard against a mask already dirty when the process was launched. The `save_state` docs drop the caveat about threads started before `run`, the ordering comment at the call site goes with the constraint it described, and the `signals` feature is dropped from calloop. `libc` becomes a direct dependency for `sigaction`, `pipe2` and `sigprocmask`.
Tests: `child_starts_with_clear_signal_mask` blocks SIGTERM in the test thread, spawns a child through the helper and checks its `SigBlk` in `/proc`, with the inverse control asserting that std really does inherit the mask (the reason the helper exists); `handler_writes_signal_to_pipe` exercises the handler directly and through a real `sigaction` + `raise`, then restores `SIG_DFL`.
2026-08-25 09:47:08 +02:00
admin ccf07de593 Session management: xdg-session-management-v1 client, mandatory App::app_id / save_state / restore_state, runtime-managed state persistence and clean exit on signals (0.3.0)
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
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`.
2026-08-15 10:16:30 +02:00
admin 1234943e86 Pin fontdue to =0.9.3 — 0.9.4 requires Rust 1.87 and the toolchain is 1.85
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
fontdue 0.9.4 started using `u{8,16,32}::cast_signed`, stabilised only in Rust 1.87, so any lockfile regeneration that floats the "0.9" requirement to 0.9.4 breaks the build on the project's rustc 1.85 with E0658 (`integer_sign_cast`). The exact pin keeps 0.9.3 — the version everything has been building against — and also protects downstream consumers of ltk (crustace, forge) whose own lockfiles re-resolve against this requirement. Relax back to "0.9" once the toolchain moves to 1.87 or newer.
2026-07-29 13:43:57 +02:00
admin 3d7f3c53de ltk: opt-in raw touch stream for apps that consume input themselves
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
Add App::claims_raw_touch(), an opt-in hook that routes the primary finger through the raw on_touch_down/move/up callbacks instead of the built-in single-slot gesture machine. Until now the primary finger was consumed entirely by the widget gesture machine (taps, swipes, presses) and never reached the app, which made it impossible to drive a self-contained input consumer like an embedded WebView or a game canvas from a touchscreen: the widget tree saw the taps, the embedded content never did. With the hook active every finger surfaces verbatim through the raw stream, widget gestures never arm, and per-finger positions are cached in SurfaceState::touch_slots for all slots so wl_touch.up (which carries no coordinates) can still report a release point.
The touch module doc and the on_touch_down docs are updated to describe the new contract; the default remains unchanged for every existing app.
Pin the transitive dependency ignore to 0.4.23: rust-i18n pulls it via globwalk, 0.4.24+ requires a rustc newer than Debian's 1.85, and its manifest does not declare that MSRV, so a fresh resolution breaks the build on stable.
2026-07-21 09:56:05 +02:00
admin ce893ac776 responsive fluid/physical scaling, widget-API stabilization, and perf guardrails
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
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
admin d4d7ee742e Bump to 0.2.0: SW/GLES paint parity, shared font resolution, FrameState refactor, docs and packaging fixes
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
Rendering parity (software ↔ GLES). The software backend now rounds glyph pen positions and image destinations to the nearest integer pixel, matching what the GLES backend already did; previously it truncated, so text and 1:1 images could land up to half a pixel off between the two backends and the bilinear sample read ~1 px softer than the source. Gradients and shadows are deliberately left unimplemented on the software backend, and the GLES multi-rect `glScissor` clip is left coarse on purpose: making it exact would need stencil bits the EGL config does not carry, or routing the partial-redraw path through the offscreen clip layer, which would break its `fill` / `clear_rects_transparent` scissor semantics. Adds software-backend pixel tests covering the snapping.
Font resolution unification. The system-font candidate chain, `find_font_opt` and `load_default_font_bytes` lived in two copies (`render/helpers` and `gles_render/helpers`) that had already diverged — one resolved through `find_font_opt`, the other inlined the candidate loop — and now live once in `system_fonts`. The two per-backend `OnceLock` default-font caches and `primary_handle` collapse into a single `system_fonts::default_handle`. Module docs and the `font_registry` caller are updated accordingly.
Shared image validation and rect inflation. `draw_image_data`'s dimension check and its one-line warning were byte-duplicated across both backends and are now `render::helpers::validate_rgba_dims`. The six manual symmetric `Rect`-inflate literals in the GLES primitives reuse the existing `Rect::expand`.
FrameState and DrawCtx de-duplication. The eleven `SurfaceState` fields the draw pass owns and threads through `DrawCtx` — `widget_rects`, the cursor / selection maps, the scroll state, `accessible_extras`, `prev_focused` / `prev_hovered` / `prev_pressed` — move into a `FrameState` sub-struct. The per-frame `build_draw_ctx` / `commit_draw_ctx` helpers can then borrow `&mut ss.frame`, disjoint from `ss.canvas` and `ss.pool`, so the four frame paths (software / GLES × full / partial) replace their duplicated `DrawCtx` construction and write-back with a single helper call each. A whole-`SurfaceState` borrow could not express this (partial borrows do not cross function boundaries), which is why the helpers take the sub-struct. `content_dirty` stays on `SurfaceState` — it is an invalidation flag, not frame state — and is reset at the call site.
rich_text tests. Adds the previously-missing `tests.rs` for the `RichText` widget: one hit rect per visual line a link spans (the widget's core invariant), the single-line and no-link cases, preferred-size growth with hard line breaks, and `map_msg` range preservation — all headless against a software `Canvas`, with line counts forced by `\n` so they do not depend on any system font's measured width.
Documentation. Fills the rustdoc gaps on the embedder-facing surface: `core::UiSurface` accessors, the `egl_context` public API, the `GlesCanvas` methods, `theme::typography` and `theme::error`, and the `RichText` / `Text` builders; adds a crate-level "Rendering backends" overview. `CHANGELOG.md` is added (0.2.0 / 0.1.0), and `docs/widgets.md` / `docs/cookbook.md` gain `rich_text`, `external`, and the CPU-draw / path-clip / externally-laid-out-tree recipes. `debian/changelog` gets the 0.2.0-1 entry. Private intra-doc links to `system_fonts` are demoted to code spans so `cargo doc` is warning-free.
Packaging. The `libltk-dev` registry crate shipped a `Cargo.toml` declaring the `lookup` bench while the `Makefile` install copied only `src/`, so Cargo refused to parse the manifest over a missing `benches/lookup.rs`; the install now ships `benches/` as well (the file alone satisfies the parse — criterion is a dev-dependency and is not resolved when the crate is consumed as a library). `Cargo.toml` is bumped to 0.2.0 to match the package version and the `ltk-0.2.0` registry directory.
2026-06-25 12:43:40 +02:00
admin 4a80165428 event_loop, a11y, text_shaping: AccessKit AT-SPI2 bridge, cross-app clipboard, xdg-activation, HarfBuzz shaping, multi-touch hooks
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
Five orthogonal capabilities land together because they share the same `try_run` plumbing: an optional global is bound at startup, a piece of state is added to `AppData`, the run-loop iteration drains an inbox / pushes a frame snapshot, and the public surface gains a small set of opt-in `App` hooks. Nothing here breaks an existing app — every new path degrades to a no-op when the compositor does not advertise the relevant global or when the platform adapter cannot start.
AT-SPI2 accessibility via AccessKit. A new `src/a11y/` module owns the platform adapter and the inbound `ActionRequest` channel. `A11yState::try_new` constructs an `accesskit_unix::Adapter`; when the AT-SPI2 daemon is not on the session bus (headless CI, locked-down compositors) the constructor returns `None` and the rest of the pipeline runs unchanged. After every successful `draw_frame`, the run loop builds a fresh `accesskit::TreeUpdate` from `widget_rects` and pushes it through the adapter — main surface plus every visible overlay, each translated to global coordinates via `surface_offset_for` so screen readers report positions in the same frame the user sees. Buttons / toggles / checkboxes / radios / list items / sliders / text edits map to the matching `Role`s; `Click` and `Focus` actions are advertised on every interactive node; inbound action requests are drained at the top of each iteration and translated into a synthetic press / focus on the matching widget. The integration is documented as best-effort in `docs/architecture.md` under "Known gaps and non-goals": hierarchical nesting, per-widget accessible names, live regions and `Action::SetValue` are listed as the natural follow-ups that the foundation now supports but does not yet wire.
Cross-application clipboard via `wl_data_device_manager`. A new `src/event_loop/data_device.rs` bridges the existing process-local `clipboard: String` to the Wayland selection. Outbound (Ctrl+C / Cut): after the local clipboard is populated, `publish_clipboard_selection` creates a `CopyPasteSource` offering `text/plain;charset=utf-8` and installs it as the seat's selection; `DataSourceHandler::send` writes the cached string into the fd the peer hands us. Inbound (Ctrl+V from another app): `DataDeviceHandler::selection` asks for the offered text via `WlDataOffer::receive`, spawns a tiny worker thread to drain the read pipe with a 16 MiB cap to prevent paste-bomb DoS, and posts the result back through an `mpsc::Sender` that the run loop drains each iteration into `data.clipboard`. The `clipboard:` field's doc-comment is updated to reflect the new behaviour: process-local when the compositor does not advertise the global, synchronised with the seat selection otherwise.
External drag-and-drop reception. The same `data_device` module handles `DragOffer` enter / motion / leave / drop_performed: `on_drop_motion( x, y )` fires while the drag hovers over the surface, `on_drop_leave()` when it withdraws without dropping, and `on_drop_received( x, y, mime, text )` when an external payload (`text/uri-list`, `text/plain`, …) is released on top of an ltk window. The receive path reuses the same worker-thread / channel pattern as the clipboard so the run loop never blocks on the read fd. Three new `App` hooks expose the events with no-op defaults; apps that ignore them get the previous behaviour.
`xdg-activation-v1`. The global is bound optionally; when it is present, `try_run` reads `$XDG_ACTIVATION_TOKEN` from the environment, removes it immediately (single-use; preventing leaks into child processes) and stashes it on `AppData::activation_token_pending`. After the first successful configure of the main surface — the earliest point at which `xdg_activation_v1.activate` is meaningful — the token is consumed once and the surface raised to focus. Compositors without the global leave `activation_state` as `None` and the inbound path silently degrades. An `App::request_activation_token` outbound path is reserved on the trait but not yet exercised here.
HarfBuzz shaping. A new `src/text_shaping.rs::shape_line` drives both renderers: the logical-order string is run through `unicode-bidi`, split into per-font sub-runs, and shaped through `rustybuzz`. Each `PositionedGlyph` carries the per-font `glyph_id`, the visual advance and the ink offsets — exactly what `fontdue::Font::rasterize_indexed` needs to render Arabic connected forms, Devanagari clusters and CJK shaped glyphs correctly. The GLES atlas is re-keyed on `(glyph_id, size_bits, font_id)` so glyphs from different fonts at the same size no longer collide, and the atlas format is selected per ES profile (`GL_R8` / `GL_RED` on ES3, `GL_LUMINANCE` on ES2) — the fragment shader samples `.r` for both, since `GL_LUMINANCE` replicates the coverage byte into `.r=.g=.b`. Software path follows the same key. New `Cargo.toml` deps: `unicode-bidi = "0.3"`, `rustybuzz = "0.14"`.
Multi-touch hooks. `App::on_touch_down / on_touch_move / on_touch_up( id, x, y )` expose the raw `wl_touch.id` of every secondary finger. The first finger to land remains the *primary slot* and is fed through the regular gesture machine (`on_pointer_*`, swipe, scroll, long-press, drag-and-drop). Every additional finger fires the new callbacks instead, leaving the existing single-slot behaviour untouched for apps that do not override them. This is the substrate for app-defined pinch-zoom / two-finger pan; the toolkit itself does not yet ship a built-in pinch gesture (called out in the same "Known gaps" doc section).
`event_loop::frame` extracted from `draw/mod.rs`. The `draw_frame` orchestrator and its per-format SHM helper (`pick_shm_format`) move into `src/event_loop/frame.rs`, leaving `draw/` strictly responsible for per-surface paint primitives. The import in `event_loop/run.rs` is rewritten accordingly; `draw/mod.rs` shrinks from 192-line orchestrator to a thin module index.
Overlay teardown safety. `AppData::discard_overlay( id )` synchronously removes a destroyed overlay from the map and rewrites every per-device focus that pointed at it (pointer, keyboard, every touch slot), migrating an in-flight long-press drag to the main surface the same way `reconcile_overlays` does. Used by the compositor-driven destruction paths (`PopupHandler::done`, `LayerShellHandler::closed`) where waiting for the next reconcile would leave a window in which `surface()` / `surface_mut()` panic. The non-panicking siblings `try_surface` / `try_surface_mut` are added for callers on async dispatch paths (IME `Done`, tooltip arm) that may race a teardown.
Miscellaneous. CI: `master` → `main` to match the actual default branch. `Makefile` adds `cargo run --example dialog` to the examples target. `src/lib.rs` re-exports `widget::scroll::ScrollAxis` so apps can configure a `scroll()` axis without reaching into a `pub(crate)` module. `Cargo.toml` adds `accesskit = "0.17"` and `accesskit_unix = "0.13"`. `docs/architecture.md` gains the "Known gaps and non-goals" section that enumerates the new capabilities, what still ships flat, and what is deferred (per-widget a11y labels, primary selection, intra-process multi-touch gestures, `wp_fractional_scale_v1`).
2026-05-16 22:09:59 +02:00
admin 4aa3480b64 refactor: split every monolithic module into focused submodules
Each source file that had grown beyond a single concern is replaced by an identically-named directory containing focused submodules. `src/event_loop/mod.rs` (878 lines) becomes a directory with clipboard, context_menu, cursor_shape, drag, focus, handlers, invalidation, overlays_reconcile, repeat, run, surface, text_editing, and tooltip. Every widget, input handler, and theme component follows the same split. Public interfaces are unchanged — only the internal file layout moves.
image bumped from 0.25.2 to 0.25.9.
2026-05-15 23:46:56 +02:00
admin dc781fb78d event_loop: client binding for ext-foreign-toplevel-list-v1
Adds an `App` callback that delivers the live list of open toplevels from the compositor — the data source a shell needs for dock running-app indicators, taskbar tiles, alt-tab and any other "what is currently running" UI. Hand-wiring the protocol binding from every shell that wants it is the kind of boilerplate ltk should absorb once: this is that move.
`Cargo.toml` adds `"staging"` to `wayland-protocols`' feature list. SCTK 0.20 already pulls staging in transitively (it carries `foreign_toplevel_list.rs` and its own dispatch helper), so this is belt-and-braces against a future ltk tree that swaps SCTK for a different client toolkit — it keeps the protocol available crate-wide even then. `src/app.rs` introduces `ToplevelEvent { Opened { id: u32, app_id: String }, Closed { id: u32 } }` and `App::on_toplevel_event( &self, ToplevelEvent ) -> Option<Self::Message>` with the default returning `None` so apps that do not care pay nothing (no allocation, no dispatch). `id` is the Wayland protocol id of the handle proxy — unique per session, stable for the handle's lifetime, the same value paired across `Opened` and `Closed`. `src/lib.rs` re-exports `ToplevelEvent` from the public prelude.
`src/event_loop/app_data.rs` grows a `pub foreign_toplevel_list: ForeignToplevelList` field. `src/event_loop/mod.rs` constructs it via `ForeignToplevelList::new( &globals, &qh )` and stores it on `AppData`. If the compositor does not advertise the global the inner `GlobalProxy` just resolves to "absent" and the list yields no toplevels — no error path needed at construction. `src/event_loop/handlers.rs` adds the `Proxy` import, `delegate_foreign_toplevel_list!( @<A: App> AppData<A> )` next to the rest, and implements `ForeignToplevelListHandler` for `AppData<A>`. The three SCTK callbacks (`new_toplevel`, `update_toplevel`, `toplevel_closed`) each pull the handle's protocol id and its currently-cached `app_id` from the list's info cache, call `self.app.on_toplevel_event( … )`, and push the returned message onto `pending_msgs` so it flows through the normal `update` cycle with the regular `invalidate_after` scoping path. `update_toplevel` re-emits `Opened` with the latest info — compositors fire this on title changes too, not just `app_id` changes, but apps whose state is keyed on `(id, app_id)` can absorb the repeat idempotently and apps that need title-change granularity can scope via `invalidate_after`.
The wire-up is generic: a shell that wants finer behaviour (focus follow, per-title indicators, multi-window grouping) can layer on top by translating the event into more specific app messages. The default app pays zero, the shell that opts in gets a real event stream without touching `smithay-client-toolkit` directly.
2026-05-11 22:30:29 +02:00
admin bbab5e238d First commit. Version 0.1.0 2026-05-10 09:58:23 +02:00