From df8fcbf757a913a4d3318061401157b4d794570f Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Tue, 9 Jun 2026 23:53:31 +0200 Subject: [PATCH] app: client-side xdg-activation token requests Adds an outbound path so an app can obtain an `xdg-activation-v1` token from the compositor and hand it to a child it is about to launch (the `$XDG_ACTIVATION_TOKEN` convention). Until now ltk only honoured inbound activation (self-activating the main surface from an inherited token); requesting a token for another app was out of scope. Two new `App` trait methods, both defaulting to no-op: `take_activation_requests` returns the tags the app wants tokens for this iteration, and `on_activation_token` delivers the issued token paired with its tag. The run loop drains the requests right after `poll_external` and calls `ActivationState::request_token`, carrying the tag in `RequestData::app_id`; `ActivationHandler::new_token` reads the tag back out and routes the token to the app through `on_activation_token`. When the compositor never advertised the activation global, each request is answered immediately with an empty token so the caller still proceeds and can fall back to its own matching instead of stalling. --- src/app.rs | 12 ++++++++++++ src/event_loop/handlers.rs | 21 +++++++++++++++------ src/event_loop/run.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0a1db11..20c0874 100644 --- a/src/app.rs +++ b/src/app.rs @@ -369,6 +369,18 @@ pub trait App: 'static /// Return any pending messages from external sources (timers, async, etc.). fn poll_external( &mut self ) -> Vec { vec![] } + /// Tags for which the app wants a fresh `xdg-activation-v1` token this + /// iteration. Drained once per loop; each tag is echoed back verbatim + /// through [`App::on_activation_token`] once the compositor issues the + /// token, so the app can hand it to a child it is about to spawn via the + /// `$XDG_ACTIVATION_TOKEN` launch convention. Default none. + fn take_activation_requests( &mut self ) -> Vec { Vec::new() } + + /// Deliver a token issued for an earlier [`App::take_activation_requests`] + /// tag. `tag` is the string the app handed out; `token` is the opaque + /// activation token to place in the launched child's environment. + fn on_activation_token( &mut self, _tag: String, _token: String ) -> Option { None } + /// Called once, immediately after the first buffer for the main surface /// has been rendered and committed to the compositor. Note: this is the /// client-side commit, not the compositor's present — under VT switching diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 8c491ce..63bb900 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -468,16 +468,25 @@ smithay_client_toolkit::delegate_data_device!( @ AppData ); // --- `xdg-activation-v1` handler --- // -// We only honour inbound activation here: when a token issued for our -// own request gets delivered, we activate the main surface. Outbound -// requests (so the app can pass a token to another app) are out of -// scope for now — adding them only requires a new public method on -// `AppData` and an extra trait method on `App`. +// A token carrying a `RequestData::app_id` was requested by the app via +// [`App::take_activation_requests`]: echo the tag + token back through +// [`App::on_activation_token`] so the app can pass it to a child it is +// launching. A token with no tag was issued for our own surface, so we +// self-activate. impl smithay_client_toolkit::activation::ActivationHandler for AppData { type RequestData = smithay_client_toolkit::activation::RequestData; - fn new_token( &mut self, token: String, _data: &Self::RequestData ) + fn new_token( &mut self, token: String, data: &Self::RequestData ) { + use smithay_client_toolkit::activation::RequestDataExt; + if let Some( tag ) = data.app_id() + { + if let Some( msg ) = self.app.on_activation_token( tag.to_string(), token ) + { + self.pending_msgs.push( msg ); + } + return; + } if let ( Some( ref activation ), Some( wl ) ) = ( self.activation_state.as_ref(), self.main.surface.try_wl_surface() ) { activation.activate::( &wl, token ); diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index 52cdefb..28be50a 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -456,6 +456,34 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> let ext: Vec<_> = data.app.poll_external(); data.pending_msgs.extend( ext ); + // Issue any xdg-activation tokens the app asked for. The tag rides + // in `RequestData::app_id` and comes back through + // `ActivationHandler::new_token` (handlers.rs) as the token. When the + // compositor never advertised the global, answer with an empty token + // so the caller still proceeds (falling back to its own matching). + for tag in data.app.take_activation_requests() + { + match data.activation_state + { + Some( ref activation ) => + { + let req = smithay_client_toolkit::activation::RequestData { + app_id: Some( tag ), + seat_and_serial: None, + surface: data.main.surface.try_wl_surface().cloned(), + }; + activation.request_token::>( &data.qh, req ); + } + None => + { + if let Some( msg ) = data.app.on_activation_token( tag, String::new() ) + { + data.pending_msgs.push( msg ); + } + } + } + } + // Drain any inbound clipboard payloads delivered by the // data-device worker thread. Latest writer wins; the channel // is unbounded but selection payloads are small (capped at