diff --git a/Cargo.toml b/Cargo.toml index 39d1a9d..d1cb68d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ fontdue = "0.9" image = { version = "=0.25.2", default-features = false, features = ["png", "jpeg", "webp"] } resvg = "0.44" rust-i18n = "3" -wayland-protocols = { version = "0.32", features = ["client", "unstable"] } +wayland-protocols = { version = "0.32", features = ["client", "unstable", "staging"] } wayland-egl = "0.32" khronos-egl = { version = "6", features = ["dynamic"] } glow = "0.17" diff --git a/src/app.rs b/src/app.rs index e3914ae..b2f24a6 100644 --- a/src/app.rs +++ b/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 + { + 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 { None } diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 9624fed..c7ea1b4 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -619,6 +619,16 @@ pub struct AppData pub current_cursor_shape: Option, pub text_input_manager: Option, pub text_input: Option, + /// 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 diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index edadc16..6993df0 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -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 CompositorHandler for AppData @@ -382,6 +385,7 @@ delegate_layer!( @ AppData ); delegate_xdg_shell!( @ AppData ); delegate_xdg_window!( @ AppData ); delegate_xdg_popup!( @ AppData ); +delegate_foreign_toplevel_list!( @ AppData ); delegate_registry!( @ AppData ); impl ProvidesRegistryState for AppData @@ -393,6 +397,84 @@ impl ProvidesRegistryState for AppData 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 ForeignToplevelListHandler for AppData +{ + fn foreign_toplevel_list_state( &mut self ) -> &mut ForeignToplevelList + { + &mut self.foreign_toplevel_list + } + + fn new_toplevel( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + 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, + 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, + 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 Dispatch for AppData diff --git a/src/event_loop/mod.rs b/src/event_loop/mod.rs index 1be2b71..b945dc6 100644 --- a/src/event_loop/mod.rs +++ b/src/event_loop/mod.rs @@ -250,6 +250,14 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> let text_input_manager: Option = 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( 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(), diff --git a/src/lib.rs b/src/lib.rs index d00bd92..c12977b 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -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:: {