Skip to content

Wayland: seal xdg-activation tokens with the last pointer-press serial#4612

Closed
Le-Syl21 wants to merge 1 commit into
rust-windowing:masterfrom
Le-Syl21:wayland-activation-set-serial
Closed

Wayland: seal xdg-activation tokens with the last pointer-press serial#4612
Le-Syl21 wants to merge 1 commit into
rust-windowing:masterfrom
Le-Syl21:wayland-activation-set-serial

Conversation

@Le-Syl21

@Le-Syl21 Le-Syl21 commented Jul 2, 2026

Copy link
Copy Markdown

Problem

Window::request_activation_token() (and the xdg-activation path of request_user_attention) builds its xdg_activation_token_v1 request with set_surface + commit, but never calls set_serial.

The protocol marks set_serial as optional, but adds:

Some compositors might refuse to activate toplevels when the token doesn't have a valid and recent enough event serial.

In practice, compositors enforcing focus-stealing prevention (mutter, KWin, niri in strict mode) validate the token's serial against the pointer's grab serial and silently refuse the ensuing activate() when it's absent. On mutter the minted token string ends in _TIME0 and the activation degrades to demands-attention at best. Net effect: an app that requests a token on user click and hands it to a child process via XDG_ACTIVATION_TOKEN sees the child's window open without focus — defeating the exact purpose of the API.

This mirrors what GTK4/GDK and Qt do internally (both seal launch tokens with the serial of the triggering input event), and is the missing piece behind several "launched app opens behind the launcher on GNOME Wayland" reports across the ecosystem (e.g. wezterm/wezterm#3619, ghostty-org/ghostty#5812 discuss the same compositor behavior).

Fix

  • Record the (WlSeat, serial) of the last pointer button press on the window's WindowState (recorded in the pointer handler, which already locks that state).
  • Seal both token issuers (request_activation_token, request_user_attention) with it via set_serial(serial, &seat) before commit.

Why press only: compositors update the pointer grab serial exclusively on button press (mutter meta-wayland-pointer.c). UI toolkits fire click actions on release, so recording the release serial too would always overwrite the press serial with one the compositor doesn't recognize — we hit exactly this during testing (token still refused when sealed with the release serial; accepted with the press serial).

Storing per-WindowState (rather than globally) also keeps the serial and the set_surface target referring to the same surface, which matches how compositors validate the pair, and avoids growing the platform Window enum.

If the window has never seen a button press, behavior is unchanged (token issued without serial, best-effort as before).

Testing

Tested on a GNOME 46 / mutter Wayland cabinet setup (winit-based launcher app, fullscreen):

  1. Launcher requests a token on user click, receives it via ActivationTokenDone, and passes it in XDG_ACTIVATION_TOKEN to a spawned process.
  2. Before: token sealed without serial → mutter refuses activation → child window opens unfocused. Confirmed via mutter source (46.0 meta-wayland-activation.c: token_can_activatemeta_wayland_seat_get_grab_info).
  3. After: spawning gnome-text-editor (GTK4, reference consumer of XDG_ACTIVATION_TOKEN) with the sealed token transfers keyboard focus reliably (3/3 runs, including >30 s after launcher startup, i.e. long past any startup token's validity).
  4. Instrumented traces confirmed the press serial is captured and applied at set_serial time.

cargo +nightly fmt, cargo clippy -p winit-wayland and cargo test -p winit-wayland are clean; the change is confined to winit-wayland.

Future work

Keyboard (wl_keyboard.key) and touch (wl_touch.down) serials can be recorded the same way for apps driven without a pointer; this PR keeps the scope to the pointer path.

@Le-Syl21

Le-Syl21 commented Jul 2, 2026

Copy link
Copy Markdown
Author

Note on CI: the cargo-deny failure is unrelated to this PR — it flags RUSTSEC-2026-0194 / RUSTSEC-2026-0195 (quick-xml) and the ttf-parser unmaintained advisory, all against dependencies already on master (this PR adds no dependencies). Master's last CI run (Jun 27) predates these advisories, which is why it was still green. All 20+ platform test jobs pass.

@kchibisov kchibisov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's already latest button serial stored on wl pointer, can just use it.

@Le-Syl21

Le-Syl21 commented Jul 2, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Reworked in 5660929 — the serial now lives on WinitPointerData and the token issuers read it through the same apply_on_pointer pattern as drag_window / show_window_menu; the WindowState field is gone.

One nuance on reusing the existing latest_button_serial as-is: it's updated on both press and release (winit's copy and sctk's PointerData::latest_button_serial alike — sctk documents it as "Serial from the latest button Press and Release events"). Compositors, however, validate activation serials against the pointer's grab serial, which only changes on button press — mutter 46 meta-wayland-pointer.c::handle_button_event:

implicit_grab = (clutter_event_type (event) == CLUTTER_BUTTON_PRESS) && (pointer->button_count == 1);
...
if (implicit_grab)
    pointer->grab_serial = wl_display_get_serial (seat->wl_display);

and meta_wayland_pointer_can_grab_surface requires pointer->grab_serial == serial. Since toolkits fire click actions on release, the combined press/release serial holds the release value at request time, and the resulting token is refused (we hit exactly this while testing on GNOME 46 — sealing with the release serial failed, press serial succeeded). Hence the separate latest_press_serial next to the existing field rather than reusing it.

@kchibisov kchibisov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in the mood to talk with robot.

Tokens issued by Window::request_activation_token and the
user-attention path were built with set_surface + commit but never
set_serial, so compositors enforcing focus-stealing prevention
(mutter, kwin, niri) refused the ensuing activation and the launched
application's window opened without focus.

Read the serial already stored on the pointer, through the same
apply_on_pointer pattern used by drag_window / show_window_menu.
@Le-Syl21 Le-Syl21 force-pushed the wayland-activation-set-serial branch from 5660929 to cf2aad6 Compare July 2, 2026 19:11
@Le-Syl21

Le-Syl21 commented Jul 2, 2026

Copy link
Copy Markdown
Author

You were right — I misread your comment. Tested on plain master: requesting the token from the window and passing it to the spawned app already transfers focus fine, no set_serial needed. This PR isn't necessary. Sorry for the noise.

@Le-Syl21 Le-Syl21 closed this Jul 2, 2026
Le-Syl21 added a commit to Le-Syl21/PinReady that referenced this pull request Jul 2, 2026
The winit set_serial patch turned out unnecessary: empirical probing
(examples-based test app, GNOME Wayland) shows stock winit's
request_activation_token — set_surface + commit, no serial — already
mints tokens mutter honors. Upstream PR rust-windowing/winit#4612
closed accordingly; PinReady now builds against crates.io winit
0.30.13.

Also drop the emath/epaint/egui-winit patch entries: nothing in the
tree depends on emath or epaint from crates.io anymore (the old
rotation fork did; the egui-rotate plugin only depends on egui), and
egui-winit resolves to the fork automatically as an internal path dep
of the patched eframe. Lockfile confirms a single copy of each.

The launcher-side activation flow is unchanged and still requires the
egui fork (ViewportCommand::RequestActivationToken +
Event::ActivationTokenReceived, upstream PR emilk/egui#8282).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants