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

@@ -6,6 +6,22 @@ pub use calloop::channel::Sender as ChannelSender;
use crate::widget::Element;
/// Wayland `ext-foreign-toplevel-list-v1` event delivered to apps via
/// [`App::on_toplevel_event`]. `Opened` fires after the compositor
/// commits the first `done` for a new handle (so `app_id` is the value
/// in effect at that point — the protocol allows the compositor to
/// re-commit later, but most don't). `Closed` fires when the
/// compositor sends `closed` and the runtime is about to destroy the
/// handle proxy. Both carry the same `id`, the Wayland protocol id of
/// the handle, unique within the session and stable for the handle's
/// lifetime.
#[ derive( Debug, Clone ) ]
pub enum ToplevelEvent
{
Opened { id: u32, app_id: String },
Closed { id: u32 },
}
/// Wayland shell mode for the application surface.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellMode
@@ -405,6 +421,21 @@ pub trait App: 'static
/// Called when a text-input widget gains or loses focus.
fn on_text_input_focused( &mut self, _active: bool ) {}
/// Translate a Wayland `ext-foreign-toplevel-list-v1` event into an
/// app message. The runtime binds the protocol globally and forwards
/// every open / close it sees here; apps that ignore window state
/// leave the default (return `None`) and pay nothing.
///
/// The `id` field is the Wayland object id of the toplevel handle —
/// unique within the session, stable for the handle's lifetime, the
/// same value coming in on `Closed` as the matching `Opened`.
/// `app_id` on `Opened` is the toplevel's `app_id` event (may be
/// empty if the compositor never sent one before the first `done`).
fn on_toplevel_event( &self, _event: ToplevelEvent ) -> Option<Self::Message>
{
None
}
/// Return `Some(id)` once to focus the widget with that [`WidgetId`](crate::types::WidgetId).
/// The app must return `None` after the first call.
fn take_focus_request( &mut self ) -> Option<crate::types::WidgetId> { None }