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:
@@ -32,7 +32,7 @@ fontdue = "0.9"
|
|||||||
image = { version = "=0.25.2", default-features = false, features = ["png", "jpeg", "webp"] }
|
image = { version = "=0.25.2", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||||
resvg = "0.44"
|
resvg = "0.44"
|
||||||
rust-i18n = "3"
|
rust-i18n = "3"
|
||||||
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
|
wayland-protocols = { version = "0.32", features = ["client", "unstable", "staging"] }
|
||||||
wayland-egl = "0.32"
|
wayland-egl = "0.32"
|
||||||
khronos-egl = { version = "6", features = ["dynamic"] }
|
khronos-egl = { version = "6", features = ["dynamic"] }
|
||||||
glow = "0.17"
|
glow = "0.17"
|
||||||
|
|||||||
31
src/app.rs
31
src/app.rs
@@ -6,6 +6,22 @@ pub use calloop::channel::Sender as ChannelSender;
|
|||||||
|
|
||||||
use crate::widget::Element;
|
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.
|
/// Wayland shell mode for the application surface.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ShellMode
|
pub enum ShellMode
|
||||||
@@ -405,6 +421,21 @@ pub trait App: 'static
|
|||||||
/// Called when a text-input widget gains or loses focus.
|
/// Called when a text-input widget gains or loses focus.
|
||||||
fn on_text_input_focused( &mut self, _active: bool ) {}
|
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).
|
/// Return `Some(id)` once to focus the widget with that [`WidgetId`](crate::types::WidgetId).
|
||||||
/// The app must return `None` after the first call.
|
/// The app must return `None` after the first call.
|
||||||
fn take_focus_request( &mut self ) -> Option<crate::types::WidgetId> { None }
|
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 current_cursor_shape: Option<crate::types::CursorShape>,
|
||||||
pub text_input_manager: Option<ZwpTextInputManagerV3>,
|
pub text_input_manager: Option<ZwpTextInputManagerV3>,
|
||||||
pub text_input: Option<ZwpTextInputV3>,
|
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 shift_pressed: bool,
|
||||||
pub ctrl_pressed: bool,
|
pub ctrl_pressed: bool,
|
||||||
/// Calloop handle for inserting timers / channels. Used by the
|
/// Calloop handle for inserting timers / channels. Used by the
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
use smithay_client_toolkit::
|
use smithay_client_toolkit::
|
||||||
{
|
{
|
||||||
compositor::CompositorHandler,
|
compositor::CompositorHandler,
|
||||||
delegate_compositor, delegate_layer, delegate_output, delegate_registry,
|
delegate_compositor, delegate_foreign_toplevel_list, delegate_layer,
|
||||||
delegate_seat, delegate_keyboard, delegate_pointer, delegate_touch, delegate_shm,
|
delegate_output, delegate_registry, delegate_seat, delegate_keyboard,
|
||||||
delegate_xdg_popup, delegate_xdg_shell, delegate_xdg_window,
|
delegate_pointer, delegate_touch, delegate_shm, delegate_xdg_popup,
|
||||||
|
delegate_xdg_shell, delegate_xdg_window,
|
||||||
|
foreign_toplevel_list::{ ForeignToplevelList, ForeignToplevelListHandler },
|
||||||
output::{ OutputHandler, OutputState },
|
output::{ OutputHandler, OutputState },
|
||||||
registry::{ ProvidesRegistryState, RegistryState },
|
registry::{ ProvidesRegistryState, RegistryState },
|
||||||
registry_handlers,
|
registry_handlers,
|
||||||
@@ -20,6 +22,7 @@ use smithay_client_toolkit::
|
|||||||
},
|
},
|
||||||
shm::{ Shm, ShmHandler },
|
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::
|
use smithay_client_toolkit::reexports::client::
|
||||||
{
|
{
|
||||||
protocol::
|
protocol::
|
||||||
@@ -29,7 +32,7 @@ use smithay_client_toolkit::reexports::client::
|
|||||||
wl_surface::WlSurface,
|
wl_surface::WlSurface,
|
||||||
wl_seat::WlSeat,
|
wl_seat::WlSeat,
|
||||||
},
|
},
|
||||||
Connection, Dispatch, QueueHandle,
|
Connection, Dispatch, Proxy, QueueHandle,
|
||||||
};
|
};
|
||||||
use wayland_protocols::wp::text_input::zv3::client::
|
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 },
|
zwp_text_input_v3::{ self, ZwpTextInputV3 },
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::{ App, ToplevelEvent };
|
||||||
use super::app_data::AppData;
|
use super::app_data::AppData;
|
||||||
|
|
||||||
impl<A: App> CompositorHandler for AppData<A>
|
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_shell!( @<A: App> AppData<A> );
|
||||||
delegate_xdg_window!( @<A: App> AppData<A> );
|
delegate_xdg_window!( @<A: App> AppData<A> );
|
||||||
delegate_xdg_popup!( @<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> );
|
delegate_registry!( @<A: App> AppData<A> );
|
||||||
|
|
||||||
impl<A: App> ProvidesRegistryState for AppData<A>
|
impl<A: App> ProvidesRegistryState for AppData<A>
|
||||||
@@ -393,6 +397,84 @@ impl<A: App> ProvidesRegistryState for AppData<A>
|
|||||||
registry_handlers![ OutputState, SeatState ];
|
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 ---
|
// --- Dispatch impls for zwp_text_input_v3 ---
|
||||||
|
|
||||||
impl<A: App> Dispatch<ZwpTextInputManagerV3, ()> for AppData<A>
|
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();
|
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 debug_layout = std::env::var( "LTK_DEBUG_LAYOUT" ).is_ok();
|
||||||
|
|
||||||
let titlebar_height = if force_window.is_some() { 36.0 } else { 0.0 };
|
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,
|
current_cursor_shape: None,
|
||||||
text_input_manager,
|
text_input_manager,
|
||||||
text_input: None,
|
text_input: None,
|
||||||
|
foreign_toplevel_list,
|
||||||
shift_pressed: false,
|
shift_pressed: false,
|
||||||
ctrl_pressed: false,
|
ctrl_pressed: false,
|
||||||
loop_handle: event_loop.handle(),
|
loop_handle: event_loop.handle(),
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ pub mod core;
|
|||||||
pub use app::
|
pub use app::
|
||||||
{
|
{
|
||||||
Anchor, App, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec,
|
Anchor, App, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec,
|
||||||
InvalidationScope, SurfaceTarget,
|
InvalidationScope, SurfaceTarget, ToplevelEvent,
|
||||||
};
|
};
|
||||||
pub use theme::
|
pub use theme::
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user