app: client-side xdg-activation token requests
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

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.
This commit is contained in:
2026-06-09 23:53:31 +02:00
parent cfa0faff26
commit df8fcbf757
3 changed files with 55 additions and 6 deletions

View File

@@ -369,6 +369,18 @@ pub trait App: 'static
/// Return any pending messages from external sources (timers, async, etc.). /// Return any pending messages from external sources (timers, async, etc.).
fn poll_external( &mut self ) -> Vec<Self::Message> { vec![] } fn poll_external( &mut self ) -> Vec<Self::Message> { 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<String> { 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<Self::Message> { None }
/// Called once, immediately after the first buffer for the main surface /// Called once, immediately after the first buffer for the main surface
/// has been rendered and committed to the compositor. Note: this is the /// has been rendered and committed to the compositor. Note: this is the
/// client-side commit, not the compositor's present — under VT switching /// client-side commit, not the compositor's present — under VT switching

View File

@@ -468,16 +468,25 @@ smithay_client_toolkit::delegate_data_device!( @<A: App> AppData<A> );
// --- `xdg-activation-v1` handler --- // --- `xdg-activation-v1` handler ---
// //
// We only honour inbound activation here: when a token issued for our // A token carrying a `RequestData::app_id` was requested by the app via
// own request gets delivered, we activate the main surface. Outbound // [`App::take_activation_requests`]: echo the tag + token back through
// requests (so the app can pass a token to another app) are out of // [`App::on_activation_token`] so the app can pass it to a child it is
// scope for now — adding them only requires a new public method on // launching. A token with no tag was issued for our own surface, so we
// `AppData` and an extra trait method on `App`. // self-activate.
impl<A: App> smithay_client_toolkit::activation::ActivationHandler for AppData<A> impl<A: App> smithay_client_toolkit::activation::ActivationHandler for AppData<A>
{ {
type RequestData = smithay_client_toolkit::activation::RequestData; 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() ) if let ( Some( ref activation ), Some( wl ) ) = ( self.activation_state.as_ref(), self.main.surface.try_wl_surface() )
{ {
activation.activate::<Self>( &wl, token ); activation.activate::<Self>( &wl, token );

View File

@@ -456,6 +456,34 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
let ext: Vec<_> = data.app.poll_external(); let ext: Vec<_> = data.app.poll_external();
data.pending_msgs.extend( ext ); 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::<AppData<A>>( &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 // Drain any inbound clipboard payloads delivered by the
// data-device worker thread. Latest writer wins; the channel // data-device worker thread. Latest writer wins; the channel
// is unbounded but selection payloads are small (capped at // is unbounded but selection payloads are small (capped at