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:
31
src/app.rs
31
src/app.rs
@@ -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 }
|
||||
|
||||
@@ -619,6 +619,16 @@ pub struct AppData<A: App>
|
||||
pub current_cursor_shape: Option<crate::types::CursorShape>,
|
||||
pub text_input_manager: Option<ZwpTextInputManagerV3>,
|
||||
pub text_input: Option<ZwpTextInputV3>,
|
||||
/// Client-side handle to `ext-foreign-toplevel-list-v1`. Holds the
|
||||
/// global proxy plus the live list of open toplevels; SCTK fans
|
||||
/// events through this object into our `ForeignToplevelListHandler`
|
||||
/// impl in `handlers.rs`, which then routes them to
|
||||
/// `App::on_toplevel_event`. Present on every session, even when
|
||||
/// the compositor does not advertise the global — internally the
|
||||
/// list holds a `GlobalProxy` that simply yields no toplevels in
|
||||
/// that case.
|
||||
pub foreign_toplevel_list:
|
||||
smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList,
|
||||
pub shift_pressed: bool,
|
||||
pub ctrl_pressed: bool,
|
||||
/// Calloop handle for inserting timers / channels. Used by the
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -250,6 +250,14 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
|
||||
let text_input_manager: Option<ZwpTextInputManagerV3> = globals.bind( &qh, 1..=1, () ).ok();
|
||||
|
||||
// `ext-foreign-toplevel-list-v1`. SCTK's `ForeignToplevelList` handles the
|
||||
// bind + dispatch routing; absence of the global is fine, the inner
|
||||
// `GlobalProxy` then yields no toplevels and `App::on_toplevel_event` never
|
||||
// fires. The list is built unconditionally so apps that override the
|
||||
// callback always see a consistent stream when the compositor does carry
|
||||
// the protocol.
|
||||
let foreign_toplevel_list = smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList::new( &globals, &qh );
|
||||
|
||||
let debug_layout = std::env::var( "LTK_DEBUG_LAYOUT" ).is_ok();
|
||||
|
||||
let titlebar_height = if force_window.is_some() { 36.0 } else { 0.0 };
|
||||
@@ -280,6 +288,7 @@ pub( crate ) fn try_run<A: App>( app: A ) -> Result<(), RunError>
|
||||
current_cursor_shape: None,
|
||||
text_input_manager,
|
||||
text_input: None,
|
||||
foreign_toplevel_list,
|
||||
shift_pressed: false,
|
||||
ctrl_pressed: false,
|
||||
loop_handle: event_loop.handle(),
|
||||
|
||||
@@ -167,7 +167,7 @@ pub mod core;
|
||||
pub use app::
|
||||
{
|
||||
Anchor, App, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec,
|
||||
InvalidationScope, SurfaceTarget,
|
||||
InvalidationScope, SurfaceTarget, ToplevelEvent,
|
||||
};
|
||||
pub use theme::
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user