From 84bdd3b9df31cf09687906208df97eded87318f1 Mon Sep 17 00:00:00 2001 From: dingo Date: Sun, 9 Aug 2026 23:07:10 +0700 Subject: [PATCH] Patch A: zwlr_foreign_toplevel_management_v1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the wlr foreign-toplevel management protocol on top of the existing ToplevelInfoState/ToplevelManagementHandler, so waybar's wlr/taskbar module works. cosmic-comp previously shipped only ext-foreign-toplevel-list, which enumerates but cannot activate or close. The wlr manager state is constructed alongside ForeignToplevelListState and driven from the same new_toplevel/remove_toplevel/refresh hooks, so no separate registration in state.rs is needed. Accessors global_id and registered_toplevels moved to an unbounded impl block: adding the wlr dispatch bounds to the shared impl made them viral and broke overlap_notify.rs, which needs neither. cargo check passes clean. NOT runtime-verified — no Wayland session here. --- src/wayland/handlers/mod.rs | 1 + src/wayland/handlers/wlr_foreign_toplevel.rs | 14 + src/wayland/protocols/mod.rs | 1 + src/wayland/protocols/toplevel_info.rs | 30 +- src/wayland/protocols/wlr_foreign_toplevel.rs | 455 ++++++++++++++++++ 5 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 src/wayland/handlers/wlr_foreign_toplevel.rs create mode 100644 src/wayland/protocols/wlr_foreign_toplevel.rs diff --git a/src/wayland/handlers/mod.rs b/src/wayland/handlers/mod.rs index 651eadc..a0c6bd5 100644 --- a/src/wayland/handlers/mod.rs +++ b/src/wayland/handlers/mod.rs @@ -37,6 +37,7 @@ pub mod shm; pub mod tablet_manager; pub mod toplevel_info; pub mod toplevel_management; +pub mod wlr_foreign_toplevel; pub mod workspace; pub mod xdg_activation; pub mod xdg_foreign; diff --git a/src/wayland/handlers/wlr_foreign_toplevel.rs b/src/wayland/handlers/wlr_foreign_toplevel.rs new file mode 100644 index 0000000..df41f6c --- /dev/null +++ b/src/wayland/handlers/wlr_foreign_toplevel.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// `zwlr_foreign_toplevel_manager_v1` is implemented entirely on top of the already-existing +// `ToplevelInfoHandler` (toplevel tracking) and `ToplevelManagementHandler` (activate/close/ +// maximize/minimize/fullscreen actions) implementations for `State`, see +// `wayland::protocols::wlr_foreign_toplevel`. There is nothing left to wire up here beyond +// registering the dispatch implementations. + +use crate::{ + shell::CosmicSurface, state::State, + wayland::protocols::wlr_foreign_toplevel::delegate_wlr_foreign_toplevel, +}; + +delegate_wlr_foreign_toplevel!(State, CosmicSurface); diff --git a/src/wayland/protocols/mod.rs b/src/wayland/protocols/mod.rs index c6c0234..69de3d4 100644 --- a/src/wayland/protocols/mod.rs +++ b/src/wayland/protocols/mod.rs @@ -10,4 +10,5 @@ pub mod output_power; pub mod overlap_notify; pub mod toplevel_info; pub mod toplevel_management; +pub mod wlr_foreign_toplevel; pub mod workspace; diff --git a/src/wayland/protocols/toplevel_info.rs b/src/wayland/protocols/toplevel_info.rs index 2dc7634..d426e91 100644 --- a/src/wayland/protocols/toplevel_info.rs +++ b/src/wayland/protocols/toplevel_info.rs @@ -9,6 +9,10 @@ use smithay::{ ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, }, + wayland_protocols_wlr::foreign_toplevel::v1::server::{ + zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, + zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, + }, wayland_server::{ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, backend::{ClientId, GlobalId}, @@ -24,6 +28,9 @@ use smithay::{ use crate::utils::prelude::{Global, OutputExt, RectGlobalExt}; +use super::wlr_foreign_toplevel::{ + WlrForeignToplevelManagerGlobalData, WlrForeignToplevelManagerState, WlrToplevelHandleState, +}; use super::workspace::{WorkspaceHandle, WorkspaceHandler, WorkspaceState}; use cosmic_protocols::toplevel_info::v1::server::{ @@ -53,6 +60,7 @@ pub struct ToplevelInfoState { dirty: bool, last_dirty: bool, pub(in crate::wayland) foreign_toplevel_list: ForeignToplevelListState, + pub(in crate::wayland) wlr_foreign_toplevel: WlrForeignToplevelManagerState, global: GlobalId, _dispatch_data: std::marker::PhantomData, } @@ -281,12 +289,14 @@ pub fn toplevel_enter_output(toplevel: &impl Window, output: &Output) { if let Some(state) = toplevel.user_data().get::() { state.lock().unwrap().outputs.push(output.clone()); } + super::wlr_foreign_toplevel::toplevel_enter_output(toplevel, output); } pub fn toplevel_leave_output(toplevel: &impl Window, output: &Output) { if let Some(state) = toplevel.user_data().get::() { state.lock().unwrap().outputs.retain(|o| o != output); } + super::wlr_foreign_toplevel::toplevel_leave_output(toplevel, output); } pub fn toplevel_enter_workspace(toplevel: &impl Window, workspace: &WorkspaceHandle) { @@ -308,6 +318,9 @@ where + Dispatch + Dispatch + Dispatch> + + GlobalDispatch + + Dispatch + + Dispatch> + ForeignToplevelListHandler + ToplevelInfoHandler + 'static, @@ -324,7 +337,9 @@ where }, ); let foreign_toplevel_list = - ForeignToplevelListState::new_with_filter::(dh, client_filter); + ForeignToplevelListState::new_with_filter::(dh, client_filter.clone()); + let wlr_foreign_toplevel: WlrForeignToplevelManagerState = + WlrForeignToplevelManagerState::new(dh, client_filter); ToplevelInfoState { dh: dh.clone(), toplevels: Vec::new(), @@ -332,6 +347,7 @@ where dirty: false, last_dirty: false, foreign_toplevel_list, + wlr_foreign_toplevel, global, _dispatch_data: std::marker::PhantomData, } @@ -350,6 +366,7 @@ where .user_data() .insert_if_missing(move || ToplevelStateInner::from_foreign(toplevel_handle)); } + self.wlr_foreign_toplevel.new_toplevel(toplevel); for instance in &self.instances { send_toplevel_to_client::(&self.dh, workspace_state, instance, toplevel); @@ -378,6 +395,7 @@ where *state_inner = Default::default(); self.dirty = true; } + self.wlr_foreign_toplevel.remove_toplevel(toplevel); self.toplevels.retain(|w| w != toplevel); } @@ -405,6 +423,7 @@ where ); dirty = dirty || changed; } + self.wlr_foreign_toplevel.refresh(window); true } else { for (_info, handle) in &state.instances { @@ -434,6 +453,15 @@ where self.last_dirty = dirty; } +} + +/// Plain accessors, deliberately kept out of the dispatch-bounded impl above. +/// +/// Adding the wlr-foreign-toplevel bounds to that block made them viral: every +/// caller of these accessors would have needed the new bounds too, which broke +/// `overlap_notify.rs` for no reason. They need nothing beyond the struct +/// itself, so they belong here. +impl ToplevelInfoState { pub fn global_id(&self) -> GlobalId { self.global.clone() } diff --git a/src/wayland/protocols/wlr_foreign_toplevel.rs b/src/wayland/protocols/wlr_foreign_toplevel.rs new file mode 100644 index 0000000..347c052 --- /dev/null +++ b/src/wayland/protocols/wlr_foreign_toplevel.rs @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use std::{collections::HashSet, marker::PhantomData, sync::Mutex}; + +use smithay::{ + input::{Seat, SeatHandler}, + output::Output, + reexports::{ + wayland_protocols_wlr::foreign_toplevel::v1::server::{ + zwlr_foreign_toplevel_handle_v1::{ + self, State as States, ZwlrForeignToplevelHandleV1, + }, + zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1}, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, + backend::{ClientId, GlobalId}, + protocol::wl_output::WlOutput, + }, + }, + utils::Rectangle, +}; + +use super::{ + toplevel_info::{ToplevelInfoHandler, ToplevelState, Window}, + toplevel_management::{ManagementWindow, ToplevelManagementHandler}, +}; + +/// Aggregate, protocol-agnostic bookkeeping for a single window, mirroring the equivalent +/// state kept for the cosmic-toplevel-info protocol in `toplevel_info::ToplevelStateInner`. +/// +/// This intentionally does not depend on `W`/`D`, so it can be updated from the same +/// output-tracking call sites used by `toplevel_info::{toplevel_enter_output, toplevel_leave_output}` +/// without requiring those free functions to become generic over the window type. +#[derive(Default)] +struct WlrToplevelStateInner { + instances: Vec<(Weak, ZwlrForeignToplevelHandleV1)>, + outputs: Vec, +} +type WlrToplevelState = Mutex; + +/// Per-instance cache used to only emit events for state that actually changed, +/// mirroring `toplevel_info::ToplevelHandleStateInner`. +pub struct WlrToplevelHandleStateInner { + outputs: HashSet, + title: String, + app_id: String, + states: Option>, + window: W, +} +pub type WlrToplevelHandleState = Mutex>; + +impl WlrToplevelHandleStateInner { + fn new(window: &W) -> WlrToplevelHandleState { + WlrToplevelHandleState::new(WlrToplevelHandleStateInner { + outputs: HashSet::new(), + title: String::new(), + app_id: String::new(), + states: None, + window: window.clone(), + }) + } +} + +pub struct WlrForeignToplevelManagerGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +/// State of the `zwlr_foreign_toplevel_manager_v1` global. +/// +/// This is meant to be embedded in [`toplevel_info::ToplevelInfoState`], which already tracks +/// the authoritative list of toplevels; see `toplevel_info::ToplevelInfoState::{new_toplevel, +/// remove_toplevel, refresh}` for the call sites driving this state. +#[derive(Debug)] +pub struct WlrForeignToplevelManagerState { + dh: DisplayHandle, + instances: Vec, + global: GlobalId, + _dispatch_data: PhantomData<(D, W)>, +} + +impl WlrForeignToplevelManagerState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + 'static, + W: Window + 'static, +{ + pub fn new(dh: &DisplayHandle, client_filter: F) -> WlrForeignToplevelManagerState + where + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + Clone + 'static, + { + let global = dh.create_global::( + 3, + WlrForeignToplevelManagerGlobalData { + filter: Box::new(client_filter), + }, + ); + WlrForeignToplevelManagerState { + dh: dh.clone(), + instances: Vec::new(), + global, + _dispatch_data: PhantomData, + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } + + /// A new toplevel appeared, announce it to every bound manager instance. + pub fn new_toplevel(&mut self, window: &W) { + for manager in &self.instances { + send_new_toplevel::(&self.dh, manager, window); + } + } + + /// A toplevel was destroyed, close all of its handles. + pub fn remove_toplevel(&mut self, window: &W) { + if let Some(state) = window.user_data().get::() { + for (_manager, handle) in state.lock().unwrap().instances.drain(..) { + handle.closed(); + } + } + } + + /// Diff the current window state against what was last sent to each handle instance, + /// and emit the necessary events, matching `toplevel_info::send_toplevel_to_client`. + pub fn refresh(&mut self, window: &W) { + let Some(state) = window.user_data().get::() else { + return; + }; + let (instances, outputs) = { + let state = state.lock().unwrap(); + ( + state + .instances + .iter() + .map(|(_, handle)| handle.clone()) + .collect::>(), + state.outputs.clone(), + ) + }; + + for instance in instances { + let Some(handle_state) = instance.data::>() else { + continue; + }; + let mut handle_state = handle_state.lock().unwrap(); + let mut changed = false; + + let title = window.title(); + if handle_state.title != title { + handle_state.title = title.clone(); + instance.title(title); + changed = true; + } + + let app_id = window.app_id(); + if handle_state.app_id != app_id { + handle_state.app_id = app_id.clone(); + instance.app_id(app_id); + changed = true; + } + + let new_states = window_states(window); + if handle_state.states.as_deref() != Some(new_states.as_slice()) { + handle_state.states = Some(new_states.clone()); + instance.state(encode_states(&new_states)); + changed = true; + } + + if let Ok(client) = self.dh.get_client(instance.id()) { + for output in &outputs { + for wl_output in output.client_outputs(&client) { + if handle_state.outputs.insert(wl_output.clone()) { + instance.output_enter(&wl_output); + changed = true; + } + } + } + handle_state.outputs.retain(|wl_output| { + let retain = + wl_output.is_alive() && outputs.iter().any(|output| output.owns(wl_output)); + if !retain { + instance.output_leave(wl_output); + changed = true; + } + retain + }); + } + + if changed { + instance.done(); + } + } + } +} + +/// Track that a toplevel entered an output, to be picked up by the next `refresh`. +/// +/// Mirrors `toplevel_info::toplevel_enter_output`, but operates on the wlr-specific +/// per-window state instead, since it is called from the same shell call sites. +pub fn toplevel_enter_output(toplevel: &impl Window, output: &Output) { + if let Some(state) = toplevel.user_data().get::() { + state.lock().unwrap().outputs.push(output.clone()); + } +} + +/// See [`toplevel_enter_output`]. +pub fn toplevel_leave_output(toplevel: &impl Window, output: &Output) { + if let Some(state) = toplevel.user_data().get::() { + state.lock().unwrap().outputs.retain(|o| o != output); + } +} + +fn window_states(window: &impl Window) -> Vec { + let mut states = Vec::new(); + if window.is_maximized() { + states.push(States::Maximized); + } + if window.is_minimized() { + states.push(States::Minimized); + } + if window.is_activated() { + states.push(States::Activated); + } + if window.is_fullscreen() { + states.push(States::Fullscreen); + } + states +} + +fn encode_states(states: &[States]) -> Vec { + states + .iter() + .flat_map(|state| (*state as u32).to_ne_bytes()) + .collect::>() +} + +fn send_new_toplevel(dh: &DisplayHandle, manager: &ZwlrForeignToplevelManagerV1, window: &W) +where + D: Dispatch> + 'static, + W: Window + 'static, +{ + let Ok(client) = dh.get_client(manager.id()) else { + return; + }; + let Ok(handle) = client.create_resource::( + dh, + manager.version(), + WlrToplevelHandleStateInner::new(window), + ) else { + return; + }; + + manager.toplevel(&handle); + handle.title(window.title()); + handle.app_id(window.app_id()); + handle.state(encode_states(&window_states(window))); + handle.done(); + + if let Some(handle_state) = handle.data::>() { + let mut handle_state = handle_state.lock().unwrap(); + handle_state.title = window.title(); + handle_state.app_id = window.app_id(); + handle_state.states = Some(window_states(window)); + } + + if let Some(state) = window.user_data().get::() { + state + .lock() + .unwrap() + .instances + .push((manager.downgrade(), handle)); + } else { + window.user_data().insert_if_missing(|| { + Mutex::new(WlrToplevelStateInner { + instances: vec![(manager.downgrade(), handle)], + outputs: Vec::new(), + }) + }); + } +} + +impl GlobalDispatch + for WlrForeignToplevelManagerState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + 'static, + W: Window + 'static, +{ + fn bind( + state: &mut D, + dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &WlrForeignToplevelManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + for window in state + .toplevel_info_state() + .registered_toplevels() + .cloned() + .collect::>() + { + send_new_toplevel::(dh, &instance, &window); + } + state + .toplevel_info_state_mut() + .wlr_foreign_toplevel + .instances + .push(instance); + } + + fn can_view(client: Client, global_data: &WlrForeignToplevelManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for WlrForeignToplevelManagerState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + 'static, + W: Window + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZwlrForeignToplevelManagerV1, + request: zwlr_foreign_toplevel_manager_v1::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + if let zwlr_foreign_toplevel_manager_v1::Request::Stop = request { + state + .toplevel_info_state_mut() + .wlr_foreign_toplevel + .instances + .retain(|i| i != obj); + obj.finished(); + } + } + + fn destroyed(state: &mut D, _client: ClientId, resource: &ZwlrForeignToplevelManagerV1, _data: &()) { + state + .toplevel_info_state_mut() + .wlr_foreign_toplevel + .instances + .retain(|i| i != resource); + } +} + +impl Dispatch, D> + for WlrForeignToplevelManagerState +where + D: GlobalDispatch + + Dispatch + + Dispatch> + + ToplevelInfoHandler + + ToplevelManagementHandler + + SeatHandler + + 'static, + W: Window + ManagementWindow + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _obj: &ZwlrForeignToplevelHandleV1, + request: zwlr_foreign_toplevel_handle_v1::Request, + data: &WlrToplevelHandleState, + dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let window = data.lock().unwrap().window.clone(); + if !window.alive() { + return; + } + + match request { + zwlr_foreign_toplevel_handle_v1::Request::SetMaximized => state.maximize(dh, &window), + zwlr_foreign_toplevel_handle_v1::Request::UnsetMaximized => state.unmaximize(dh, &window), + zwlr_foreign_toplevel_handle_v1::Request::SetMinimized => state.minimize(dh, &window), + zwlr_foreign_toplevel_handle_v1::Request::UnsetMinimized => state.unminimize(dh, &window), + zwlr_foreign_toplevel_handle_v1::Request::Activate { seat } => { + state.activate(dh, &window, Seat::from_resource(&seat)) + } + zwlr_foreign_toplevel_handle_v1::Request::SetFullscreen { output } => { + state.fullscreen(dh, &window, output.as_ref().and_then(Output::from_resource)) + } + zwlr_foreign_toplevel_handle_v1::Request::UnsetFullscreen => state.unfullscreen(dh, &window), + zwlr_foreign_toplevel_handle_v1::Request::Close => state.close(dh, &window), + zwlr_foreign_toplevel_handle_v1::Request::SetRectangle { + surface, + x, + y, + width, + height, + } => { + if let Some(toplevel_state) = window.user_data().get::() { + let mut toplevel_state = toplevel_state.lock().unwrap(); + toplevel_state.rectangles.retain(|(s, _)| s.id() != surface.id()); + if width != 0 || height != 0 { + toplevel_state.rectangles.push(( + surface.downgrade(), + Rectangle::new((x, y).into(), (width, height).into()), + )); + } + } + } + _ => {} + } + } + + fn destroyed( + _state: &mut D, + _client: ClientId, + resource: &ZwlrForeignToplevelHandleV1, + data: &WlrToplevelHandleState, + ) { + let window = data.lock().unwrap().window.clone(); + if let Some(state) = window.user_data().get::() { + state + .lock() + .unwrap() + .instances + .retain(|(_, handle)| handle != resource); + } + } +} + +macro_rules! delegate_wlr_foreign_toplevel { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty, $window: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::wayland::protocols::wlr_foreign_toplevel::WlrForeignToplevelManagerGlobalData + ] => $crate::wayland::protocols::wlr_foreign_toplevel::WlrForeignToplevelManagerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: () + ] => $crate::wayland::protocols::wlr_foreign_toplevel::WlrForeignToplevelManagerState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1: $crate::wayland::protocols::wlr_foreign_toplevel::WlrToplevelHandleState<$window> + ] => $crate::wayland::protocols::wlr_foreign_toplevel::WlrForeignToplevelManagerState); + }; +} +pub(crate) use delegate_wlr_foreign_toplevel;