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.
This commit is contained in:
2026-05-11 22:30:29 +02:00
parent 39fbafec24
commit dc781fb78d
6 changed files with 139 additions and 7 deletions

View File

@@ -4,9 +4,11 @@
use smithay_client_toolkit::
{
compositor::CompositorHandler,
delegate_compositor, delegate_layer, delegate_output, delegate_registry,
delegate_seat, delegate_keyboard, delegate_pointer, delegate_touch, delegate_shm,
delegate_xdg_popup, delegate_xdg_shell, delegate_xdg_window,
delegate_compositor, delegate_foreign_toplevel_list, delegate_layer,
delegate_output, delegate_registry, delegate_seat, delegate_keyboard,
delegate_pointer, delegate_touch, delegate_shm, delegate_xdg_popup,
delegate_xdg_shell, delegate_xdg_window,
foreign_toplevel_list::{ ForeignToplevelList, ForeignToplevelListHandler },
output::{ OutputHandler, OutputState },
registry::{ ProvidesRegistryState, RegistryState },
registry_handlers,
@@ -20,6 +22,7 @@ use smithay_client_toolkit::
},
shm::{ Shm, ShmHandler },
};
use smithay_client_toolkit::reexports::protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1;
use smithay_client_toolkit::reexports::client::
{
protocol::
@@ -29,7 +32,7 @@ use smithay_client_toolkit::reexports::client::
wl_surface::WlSurface,
wl_seat::WlSeat,
},
Connection, Dispatch, QueueHandle,
Connection, Dispatch, Proxy, QueueHandle,
};
use wayland_protocols::wp::text_input::zv3::client::
{
@@ -37,7 +40,7 @@ use wayland_protocols::wp::text_input::zv3::client::
zwp_text_input_v3::{ self, ZwpTextInputV3 },
};
use crate::app::App;
use crate::app::{ App, ToplevelEvent };
use super::app_data::AppData;
impl<A: App> CompositorHandler for AppData<A>
@@ -382,6 +385,7 @@ delegate_layer!( @<A: App> AppData<A> );
delegate_xdg_shell!( @<A: App> AppData<A> );
delegate_xdg_window!( @<A: App> AppData<A> );
delegate_xdg_popup!( @<A: App> AppData<A> );
delegate_foreign_toplevel_list!( @<A: App> AppData<A> );
delegate_registry!( @<A: App> AppData<A> );
impl<A: App> ProvidesRegistryState for AppData<A>
@@ -393,6 +397,84 @@ impl<A: App> ProvidesRegistryState for AppData<A>
registry_handlers![ OutputState, SeatState ];
}
// --- `ext-foreign-toplevel-list-v1` handler ---
//
// SCTK does the wire-level dispatch (open / close / done / app_id /
// title / identifier events arrive on the inner `ForeignToplevelList`
// helper); we surface the high-level open / update / close callbacks
// to apps through [`App::on_toplevel_event`]. Returned messages join
// `pending_msgs` and flow through the regular `update` cycle, so the
// invalidation pipeline (`App::invalidate_after`) decides which
// surfaces to redraw — apps that only need the dock to refresh can
// scope to `SurfaceTarget::Main` and skip overlays.
//
// The `id` field reported to apps is the Wayland protocol id of the
// handle proxy — unique per session and stable for the handle's
// lifetime, the same value paired across `Opened` and the matching
// `Closed`.
impl<A: App> ForeignToplevelListHandler for AppData<A>
{
fn foreign_toplevel_list_state( &mut self ) -> &mut ForeignToplevelList
{
&mut self.foreign_toplevel_list
}
fn new_toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
handle: ExtForeignToplevelHandleV1,
)
{
let id = handle.id().protocol_id();
let app_id = self.foreign_toplevel_list.info( &handle )
.map( |i| i.app_id )
.unwrap_or_default();
if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } )
{
self.pending_msgs.push( msg );
}
}
fn update_toplevel(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
handle: ExtForeignToplevelHandleV1,
)
{
// Re-emit `Opened` with the latest info. Compositors usually
// only fire this on title changes; the `app_id` is committed
// once on the initial `done` and never replaced. Apps whose
// `RunningApps`-style bookkeeping is keyed by `(id, app_id)`
// can treat repeated opens as idempotent and pay nothing here,
// but the app_id-can-change-mid-life case still produces a
// correct sequence (old refcount drops, new one bumps).
let id = handle.id().protocol_id();
let app_id = self.foreign_toplevel_list.info( &handle )
.map( |i| i.app_id )
.unwrap_or_default();
if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Opened { id, app_id } )
{
self.pending_msgs.push( msg );
}
}
fn toplevel_closed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
handle: ExtForeignToplevelHandleV1,
)
{
let id = handle.id().protocol_id();
if let Some( msg ) = self.app.on_toplevel_event( ToplevelEvent::Closed { id } )
{
self.pending_msgs.push( msg );
}
}
}
// --- Dispatch impls for zwp_text_input_v3 ---
impl<A: App> Dispatch<ZwpTextInputManagerV3, ()> for AppData<A>