Draft: needs testing across desktops. It has only been verified on Hyprland (Arch). Each target in the matrix below still has to be tested on Fedora and Arch before this leaves draft.
Why
On Wayland the GTK host can't honour always_on_top. The X11 path (_NET_WM_STATE_ABOVE) does nothing there, and the host prints always_on_top is unavailable on this Linux compositor. Companion and overlay windows (desktop pets, HUDs, widgets) end up as ordinary floating toplevels:
- they sit under other floating windows;
- they live on one workspace only;
- tiling compositors draw their border and shadow around the transparent canvas;
- the client never learns its own position, so
restore_statecan't put the window back.
Most non-GNOME Wayland compositors (KDE Plasma, Sway, Hyprland, niri, river, COSMIC, labwc, Wayfire) offer wlr-layer-shell for exactly this. Related: #156 and #359 for the separate fractional-scale problem. This PR doesn't touch scaling.
What
Build: opt-in -Dlayer-shell=true (in build/app.zig and the generated build.zig in src/tooling/templates.zig)
- It links gtk4-layer-shell ahead of
gtk4, pluswayland-client. - It's opt-in rather than auto-detected, unlike the host's other optional libraries, because gtk4-layer-shell can't be
dlopened. It interposes libwayland-client symbols and only works when linked before it, so enabling it makes it a hard runtime dependency of the binary. - A build without the flag is unchanged: same sources, no new link inputs.
GTK host (src/platform/linux/gtk_host.c), all behind NATIVE_SDK_HAS_LAYER_SHELL:
- Window type: an
always_on_topwindow becomes a layer surface whengtk_layer_is_supported():- TOP layer: above normal windows, below fullscreen ones, like the floating/topmost levels on the other hosts;
- keyboard mode ON_DEMAND;
- exclusive zone
-1, so margins map onto the output's geometry; - namespace = the app id.
- Fallback: GNOME, X11 sessions and
NATIVE_SDK_LAYER_SHELL=0keep today's toplevel and X11 keep-above path. - Drag: layer surfaces have no xdg move, so the host drags them itself from the existing
window-dragpress claim and the runtimestart_window_dragchannel.- Pointer travel comes from
relative-pointer-unstable-v1. The protocol tables are inlined aswayland-scanner private-codewould emit them, and the manager is bound on a private queue. - Surface-local pointer coordinates aren't usable. A compositor that animates layer moves (Hyprland's default) reports them against a mid-animation origin the client can't know, and deriving the drag from them made the window overshoot the cursor (+106 px for a +74 px move in testing).
- Travel is clipped at the desktop edge like the cursor. Without the protocol, the host falls back to the applied-origin method.
- Pointer travel comes from
- Multi-monitor: mid-drag the surface stays on its output, because a remap would end the pointer grab. On release it's dropped where the travel says, on the output under it.
- Placement and persistence:
initial_placement.restoredand.explicitplace the layer at the given origin, clamped to a connected output..defaultstays unanchored, which layer-shell centres. Window frame events report the real origin, also after a drag ends, so the existing window-state store persists and restores it with no new API. - Lifetimes: the window teardown clears the static drag pointer and the frame-clock
after-painthandler (held through a weak pointer).
Docs: docs/content/docs/windows.mdx describes the option and its runtime fallbacks.
Open questions for maintainers
- Opt-in vs auto-detect. The flag defaults to
falseso no build picks up a runtime dependency silently. Auto-detecting with pkg-config is friendlier for local builds but surprising for redistributed binaries. Should it be off, auto, or an app-manifest field? - TOP vs OVERLAY. TOP mirrors
always_on_topon macOS and Windows. Apps that want to sit over fullscreen windows would need OVERLAY. Is that worth a separate option later? - Keyboard mode. ON_DEMAND (focus on click) keeps keyboard shortcuts working on interactive overlays. Should
activate_on_show = falsemap to NONE instead?
How to test
- Install the library:
- Fedora:
sudo dnf install gtk4-layer-shell-devel - Arch:
sudo pacman -S gtk4-layer-shell
- Fedora:
- Build this branch's CLI with
zig build cli, then create a test app withnative init overlay-test --framework <this checkout>. - In
app.json:- add
"linux"toplatforms; - on the window, set
"width": 260, "height": 160, "titlebar": "chromeless", "transparent": true, "always_on_top": true, "resizable": false; - on the view, set
"gpu_alpha_mode": "premultiplied".
- add
- In
src/app.native, change the root to<column gap="12" padding="16" background="surface" radius="lg" window-drag="true">. - Build with
native build -Dlayer-shell=trueand run./zig-out/bin/overlay-test. - Regression builds:
- the plain
native buildmust behave exactly asmain; NATIVE_SDK_LAYER_SHELL=0 ./zig-out/bin/overlay-testmust give the old toplevel and warning.
- the plain
Useful checks:
hyprctl layers/swaymsg -t get_tree/niri msg layers(KDE:qdbus org.kde.KWin /KWin supportInformation) to confirm a layer surface with namespacedev.native_sdk.overlay-test;readelf -d zig-out/bin/overlay-test | grep NEEDEDto confirm the link (libgtk4-layer-shell.so.0only with the flag).
Test cases
| # | Case | Expected with -Dlayer-shell=true |
|---|---|---|
| T1 | Build | Links; NEEDED has libgtk4-layer-shell.so.0 ahead of libgtk-4.so.1. Without the flag: no new NEEDED. |
| T2 | Surface type | Layer surface, TOP layer, namespace = app id. GNOME/XFCE: ordinary window, unchanged from main. |
| T3 | Stacking | Stays above normal and floating windows; below fullscreen ones. |
| T4 | Workspaces | Visible on every workspace of its output. |
| T5 | Decorations | No compositor border, shadow or titlebar; transparent corners stay transparent. |
| T6 | Drag | window-drag area follows the cursor exactly, also with compositor animations on. A plain click doesn't move it. |
| T7 | Screen edge | Push past the outer edge and come back: the window follows the cursor at once and ends at cursor minus grab offset. |
| T8 | Multi-monitor | Dragging onto another output drops it there under the cursor. |
| T9 | Restore | Quit and relaunch: same origin (clamped if that output is gone). |
| T10 | Interaction | Buttons and toggles inside still work; keyboard input works after clicking it. |
| T11 | Opt-out | NATIVE_SDK_LAYER_SHELL=0: old toplevel plus the existing always_on_top is unavailable warning. |
| T12 | Default build | Plain native build: identical behaviour to main. |
Test matrix
✅ pass · ❌ fail · ➖ n/a (expected fallback) · ⬜ not run
| Target | Host | Session | T1 | T2 | T3 | T4 | T5 | T6 | T7 | T8 | T9 | T10 | T11 | T12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Hyprland 0.56 (reference) | Arch | Wayland | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| KDE Plasma | Fedora | Wayland | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
| KDE Plasma | Arch | Wayland | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
| GNOME (regression: fallback) | Fedora | Wayland | ⬜ | ⬜ | ➖ | ➖ | ➖ | ⬜ | ➖ | ➖ | ➖ | ⬜ | ⬜ | ⬜ |
| GNOME (regression: fallback) | Arch | Wayland | ⬜ | ⬜ | ➖ | ➖ | ➖ | ⬜ | ➖ | ➖ | ➖ | ⬜ | ⬜ | ⬜ |
| niri | Fedora | Wayland | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
| niri | Arch | Wayland | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
| Sway | Fedora | Wayland | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
| Sway | Arch | Wayland | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ |
| XFCE (regression: X11) | Fedora | X11 | ⬜ | ⬜ | ⬜ | ➖ | ➖ | ⬜ | ➖ | ➖ | ➖ | ⬜ | ⬜ | ⬜ |
| XFCE (regression: X11) | Arch | X11 | ⬜ | ⬜ | ⬜ | ➖ | ➖ | ⬜ | ➖ | ➖ | ➖ | ⬜ | ⬜ | ⬜ |
On GNOME and XFCE the layer path must stay inactive. There, T2 means "an ordinary window, as on main", T3 on XFCE means the existing X11 keep-above still works, and T6 means the compositor-driven drag still works.
Checklist
- Hyprland (Arch): reference, tested during development
- KDE Plasma (Fedora)
- KDE Plasma (Arch)
- GNOME (Fedora): regression, fallback unchanged
- GNOME (Arch): regression, fallback unchanged
- niri (Fedora)
- niri (Arch)
- Sway (Fedora)
- Sway (Arch)
- XFCE (Fedora): regression, X11 keep-above unchanged
- XFCE (Arch): regression, X11 keep-above unchanged
-
scripts/gate.sh fastgreen (see the gate note below) - Maintainer decision on the open questions
Verification so far (Hyprland 0.56, Arch, two monitors at scale 1 and 1.333, layer animations on)
- Drags: cursor vs window travel
+99/+99,-122,+91 / -122,+92,-1932 / -1931. - Edge push-and-return: ends exactly at cursor minus grab offset.
- Cross-monitor: dropped onto the other output under the cursor.
- Restore: relaunch restores the dragged origin through the window-state store.
- Fallbacks:
NATIVE_SDK_LAYER_SHELL=0and the default build both give the existing toplevel and warning. The default build'sNEEDEDlist is unchanged. - Origin: this was first built and run as a downstream SDK patch for a desktop-pet app (crafter-station/petdex#828). That's where the overshoot, edge, teardown and monitor re-layout issues were found and fixed.
Gate
scripts/gate.sh fast on Arch:
- Pass:
zig-validate,examples-native,examples-frontends,ts-services-frontend,ts-services-e2e,surface-claims,docs-install,docs-check. zig-test: failed once inside the gate, then passed when re-run on its own (zig build test, exit 0). I don't have the log of the first failure.examples-mobile: fails because this machine has no Android SDK (no Android SDK at …, so theaarch64-linux-androidbuild ofexamples/mobile-canvasaborts). Unrelated to this diff.
Activity
2This pull request is a draft
Merging is unavailable until it is marked ready for review.
@cesasol is attempting to deploy a commit to the Vercel Labs Team on Vercel.
A member of the Team first needs to authorize it.