From 25c7b4f0274f8faf1c81efc78f22df2d6e2efd5a Mon Sep 17 00:00:00 2001 From: Spike Date: Sat, 13 Jun 2026 19:25:01 -0700 Subject: [PATCH 1/6] Add basic ext_workspace_v1 support note: doesn't support workspace groups fully yet.. --- src/lib.rs | 1 + src/workspace_manager.rs | 221 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 src/workspace_manager.rs diff --git a/src/lib.rs b/src/lib.rs index 71bfdbd1fd..cea4bb17bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,3 +40,4 @@ pub mod session_lock; pub mod shell; pub mod shm; pub mod subcompositor; +pub mod workspace_manager; diff --git a/src/workspace_manager.rs b/src/workspace_manager.rs new file mode 100644 index 0000000000..5d36459e77 --- /dev/null +++ b/src/workspace_manager.rs @@ -0,0 +1,221 @@ +use crate::{dispatch2::Dispatch2, globals::GlobalData, registry::GlobalProxy}; +use std::{ + ops::DerefMut, + sync::{Arc, Mutex}, +}; +use wayland_client::{globals::GlobalList, Connection, Dispatch, Proxy, QueueHandle, WEnum}; +use wayland_protocols::ext::workspace::v1::client::{ + ext_workspace_group_handle_v1, + ext_workspace_handle_v1::{self, State, WorkspaceCapabilities}, + ext_workspace_manager_v1, +}; + +/// Information about a workspace. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub struct WorkspaceInfo { + // ID + pub id: String, + // Name + pub name: String, + // State + pub state: Option>, + // Coordinates + pub coordinates: Vec, + // Capabilities + pub capabilities: Option>, +} + +#[derive(Debug, Default)] +struct WorkspaceInner { + current_info: Option, + pending_info: WorkspaceInfo, +} + +#[doc(hidden)] +#[derive(Debug, Default, Clone)] +pub struct WorkspaceData(Arc>); + +#[derive(Debug)] +pub struct WorkspaceManager { + workspace_manager: GlobalProxy, + workspaces: Vec, +} + +impl WorkspaceManager { + pub fn new(globals: &GlobalList, qh: &QueueHandle) -> Self + where + D: Dispatch + 'static, + { + let workspace_manager = GlobalProxy::from(globals.bind(qh, 1..=1, GlobalData)); + Self { workspace_manager, workspaces: Vec::new() } + } + + /// Returns list of worksapces. + pub fn workspaces(&self) -> &[ext_workspace_handle_v1::ExtWorkspaceHandleV1] { + &self.workspaces + } + + /// Returns information about a workspace. + /// + /// This may be none if the workspace has been destroyed or the compositor has not sent + /// information about the workspace yet. + pub fn info( + &self, + workspace: &ext_workspace_handle_v1::ExtWorkspaceHandleV1, + ) -> Option { + workspace.data::()?.0.lock().unwrap().current_info.clone() + } + + pub fn stop(&self) { + if let Ok(workspace_manager) = self.workspace_manager.get() { + workspace_manager.stop(); + } + } +} + +/// Handler trait for ext workspaces protocol. +pub trait WorkspaceHandler: Sized { + fn ext_workspace_state(&mut self) -> &mut WorkspaceManager; + + /// A new workspace has been created. + fn new_workspace( + &mut self, + conn: &Connection, + qh: &QueueHandle, + workspace_handle: ext_workspace_handle_v1::ExtWorkspaceHandleV1, + ); + + /// An existing workspace has changed. + fn update_workspace( + &mut self, + conn: &Connection, + qh: &QueueHandle, + workspace_handle: ext_workspace_handle_v1::ExtWorkspaceHandleV1, + ); + + /// A workspace has been removed. + fn workspace_removed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + workspace_handle: ext_workspace_handle_v1::ExtWorkspaceHandleV1, + ); + + /// All workspaces/groups have been updated. + fn done(&mut self, _conn: &Connection, _qh: &QueueHandle) {} + + fn finished(&mut self, _conn: &Connection, _qh: &QueueHandle) {} +} + +// Workspace group +impl Dispatch2 for GlobalData +where + D: Dispatch + + Dispatch + + WorkspaceHandler + + 'static, +{ + fn event( + &self, + state: &mut D, + proxy: &ext_workspace_manager_v1::ExtWorkspaceManagerV1, + event: ext_workspace_manager_v1::Event, + conn: &Connection, + qh: &QueueHandle, + ) { + match event { + ext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group: _ } => {} + ext_workspace_manager_v1::Event::Workspace { workspace } => { + state.ext_workspace_state().workspaces.push(workspace.clone()); + } + ext_workspace_manager_v1::Event::Done => { + // TODO: is cloning really the best for performance? + for ref workspace in state.ext_workspace_state().workspaces.clone() { + let handle = workspace.data::().unwrap(); + let mut inner = handle.0.lock().unwrap(); + let just_created = inner.current_info.is_none(); + inner.current_info = Some(inner.pending_info.clone()); + drop(inner); + if just_created { + state.new_workspace(conn, qh, workspace.clone()); + } else { + state.update_workspace(conn, qh, workspace.clone()); + } + } + state.done(conn, qh); + } + ext_workspace_manager_v1::Event::Finished => { + state.finished(conn, qh); + proxy.stop(); + } + _ => unreachable!(), + } + } + + wayland_client::event_created_child!(D, ext_workspace_manager_v1::ExtWorkspaceManagerV1, [ + ext_workspace_manager_v1::EVT_WORKSPACE_GROUP_OPCODE => (ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, Default::default()), + ext_workspace_manager_v1::EVT_WORKSPACE_OPCODE => (ext_workspace_handle_v1::ExtWorkspaceHandleV1, Default::default()), + ]); +} + +impl Dispatch2 for WorkspaceData +where + D: WorkspaceHandler, +{ + fn event( + &self, + state: &mut D, + handle: &ext_workspace_handle_v1::ExtWorkspaceHandleV1, + event: ext_workspace_handle_v1::Event, + conn: &Connection, + qh: &QueueHandle, + ) { + match event { + ext_workspace_handle_v1::Event::Removed => { + state.workspace_removed(conn, qh, handle.clone()); + let workspaces = &mut state.ext_workspace_state().workspaces; + if let Some(idx) = workspaces.iter().position(|x| x == handle) { + workspaces.remove(idx); + } + handle.destroy(); + } + ext_workspace_handle_v1::Event::Id { id } => { + self.0.lock().unwrap().pending_info.id = id; + } + ext_workspace_handle_v1::Event::Name { name } => { + self.0.lock().unwrap().pending_info.name = name; + } + ext_workspace_handle_v1::Event::Coordinates { coordinates } => { + self.0.lock().unwrap().pending_info.coordinates = coordinates; + } + ext_workspace_handle_v1::Event::State { state } => { + self.0.lock().unwrap().pending_info.state = Some(state); + } + ext_workspace_handle_v1::Event::Capabilities { capabilities } => { + self.0.lock().unwrap().pending_info.capabilities = Some(capabilities); + } + _ => unreachable!(), + } + } +} + +impl Dispatch2 for WorkspaceData +where + D: WorkspaceHandler, +{ + fn event( + &self, + state: &mut D, + handle: &ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + event: ext_workspace_group_handle_v1::Event, + conn: &Connection, + qh: &QueueHandle, + ) { + match event { + _ => { + println!("workspace group happened") + } + } + } +} From 2ae684bd02b14ee83769b050f385ea543ea1a75a Mon Sep 17 00:00:00 2001 From: Spike Date: Wed, 17 Jun 2026 18:36:02 -0700 Subject: [PATCH 2/6] Basic workspace_group support --- src/workspace_manager.rs | 95 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 9 deletions(-) diff --git a/src/workspace_manager.rs b/src/workspace_manager.rs index 5d36459e77..ee86bc5db4 100644 --- a/src/workspace_manager.rs +++ b/src/workspace_manager.rs @@ -3,7 +3,9 @@ use std::{ ops::DerefMut, sync::{Arc, Mutex}, }; -use wayland_client::{globals::GlobalList, Connection, Dispatch, Proxy, QueueHandle, WEnum}; +use wayland_client::{ + globals::GlobalList, protocol::wl_output, Connection, Dispatch, Proxy, QueueHandle, WEnum, +}; use wayland_protocols::ext::workspace::v1::client::{ ext_workspace_group_handle_v1, ext_workspace_handle_v1::{self, State, WorkspaceCapabilities}, @@ -26,20 +28,41 @@ pub struct WorkspaceInfo { pub capabilities: Option>, } +/// Information about a workspace group. +#[derive(Clone, Debug, Default)] +#[non_exhaustive] +pub struct GroupInfo { + // Output + pub output: Option, + // Workspaces + pub workspaces: Vec, +} + #[derive(Debug, Default)] struct WorkspaceInner { current_info: Option, pending_info: WorkspaceInfo, } +#[derive(Debug, Default)] +struct GroupInner { + current_info: Option, + pending_info: GroupInfo, +} + #[doc(hidden)] #[derive(Debug, Default, Clone)] pub struct WorkspaceData(Arc>); +#[doc(hidden)] +#[derive(Debug, Default, Clone)] +pub struct GroupData(Arc>); + #[derive(Debug)] pub struct WorkspaceManager { workspace_manager: GlobalProxy, workspaces: Vec, + groups: Vec, } impl WorkspaceManager { @@ -48,14 +71,19 @@ impl WorkspaceManager { D: Dispatch + 'static, { let workspace_manager = GlobalProxy::from(globals.bind(qh, 1..=1, GlobalData)); - Self { workspace_manager, workspaces: Vec::new() } + Self { workspace_manager, workspaces: Vec::new(), groups: Vec::new() } } - /// Returns list of worksapces. + /// Returns list of workspaces. pub fn workspaces(&self) -> &[ext_workspace_handle_v1::ExtWorkspaceHandleV1] { &self.workspaces } + /// Returns list of workspace groups. + pub fn groups(&self) -> &[ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1] { + &self.groups + } + /// Returns information about a workspace. /// /// This may be none if the workspace has been destroyed or the compositor has not sent @@ -67,6 +95,14 @@ impl WorkspaceManager { workspace.data::()?.0.lock().unwrap().current_info.clone() } + /// Returns information about a workspace group. + pub fn info_group( + &self, + group: &ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ) -> Option { + group.data::()?.0.lock().unwrap().current_info.clone() + } + pub fn stop(&self) { if let Ok(workspace_manager) = self.workspace_manager.get() { workspace_manager.stop(); @@ -108,11 +144,10 @@ pub trait WorkspaceHandler: Sized { fn finished(&mut self, _conn: &Connection, _qh: &QueueHandle) {} } -// Workspace group impl Dispatch2 for GlobalData where D: Dispatch - + Dispatch + + Dispatch + WorkspaceHandler + 'static, { @@ -125,12 +160,15 @@ where qh: &QueueHandle, ) { match event { - ext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group: _ } => {} + ext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group: group } => { + state.ext_workspace_state().groups.push(group.clone()); + } ext_workspace_manager_v1::Event::Workspace { workspace } => { state.ext_workspace_state().workspaces.push(workspace.clone()); } ext_workspace_manager_v1::Event::Done => { // TODO: is cloning really the best for performance? + // Workspaces for ref workspace in state.ext_workspace_state().workspaces.clone() { let handle = workspace.data::().unwrap(); let mut inner = handle.0.lock().unwrap(); @@ -143,6 +181,20 @@ where state.update_workspace(conn, qh, workspace.clone()); } } + // Groups + for ref group in state.ext_workspace_state().groups.clone() { + let handle = group.data::().unwrap(); + let mut inner = handle.0.lock().unwrap(); + // FIXME all the commented stuff + // let just_created = inner.current_info.is_none(); + inner.current_info = Some(inner.pending_info.clone()); + drop(inner); + // if just_created { + // state.new_workspace(conn, qh, workspace.clone()); + // } else { + // state.update_workspace(conn, qh, workspace.clone()); + // } + } state.done(conn, qh); } ext_workspace_manager_v1::Event::Finished => { @@ -159,6 +211,7 @@ where ]); } +// Workspace event handler impl Dispatch2 for WorkspaceData where D: WorkspaceHandler, @@ -200,7 +253,8 @@ where } } -impl Dispatch2 for WorkspaceData +// Workspace group event handler +impl Dispatch2 for GroupData where D: WorkspaceHandler, { @@ -213,9 +267,32 @@ where qh: &QueueHandle, ) { match event { - _ => { - println!("workspace group happened") + ext_workspace_group_handle_v1::Event::Removed => { + let groups = &mut state.ext_workspace_state().groups; + if let Some(idx) = groups.iter().position(|x| x == handle) { + groups.remove(idx); + } + handle.destroy(); + } + ext_workspace_group_handle_v1::Event::OutputEnter { output } => { + self.0.lock().unwrap().pending_info.output = Some(output) + } + // TODO: multiple outputs? + ext_workspace_group_handle_v1::Event::OutputLeave { output: _ } => { + self.0.lock().unwrap().pending_info.output = None + } + ext_workspace_group_handle_v1::Event::WorkspaceEnter { workspace } => { + self.0.lock().unwrap().pending_info.workspaces.push(workspace) + } + ext_workspace_group_handle_v1::Event::WorkspaceLeave { workspace } => { + let workspaces = &mut self.0.lock().unwrap().pending_info.workspaces; + if let Some(idx) = workspaces.iter().position(|x| x == &workspace) { + workspaces.remove(idx); + } } + // TODO I don't know what this is and I don't care (for now) + ext_workspace_group_handle_v1::Event::Capabilities { capabilities: _ } => {} + _ => {} } } } From 3925582ca5f8450c5daf969420441d74702f302d Mon Sep 17 00:00:00 2001 From: Spike Date: Fri, 19 Jun 2026 22:47:38 -0700 Subject: [PATCH 3/6] multi-monitor patch --- src/workspace_manager.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/workspace_manager.rs b/src/workspace_manager.rs index ee86bc5db4..89c73cf39b 100644 --- a/src/workspace_manager.rs +++ b/src/workspace_manager.rs @@ -278,8 +278,11 @@ where self.0.lock().unwrap().pending_info.output = Some(output) } // TODO: multiple outputs? - ext_workspace_group_handle_v1::Event::OutputLeave { output: _ } => { - self.0.lock().unwrap().pending_info.output = None + ext_workspace_group_handle_v1::Event::OutputLeave { output } => { + let pending_output = self.0.lock().unwrap().pending_info.output.clone(); + if pending_output.is_some_and(|o| o == output) { + self.0.lock().unwrap().pending_info.output = None; + } } ext_workspace_group_handle_v1::Event::WorkspaceEnter { workspace } => { self.0.lock().unwrap().pending_info.workspaces.push(workspace) From c9dda27277ac4b0721b23270c951900a409495d5 Mon Sep 17 00:00:00 2001 From: Spike Date: Sat, 20 Jun 2026 13:00:26 -0700 Subject: [PATCH 4/6] Treat outputs as a vec --- src/workspace_manager.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/workspace_manager.rs b/src/workspace_manager.rs index 89c73cf39b..f23f263430 100644 --- a/src/workspace_manager.rs +++ b/src/workspace_manager.rs @@ -1,8 +1,5 @@ use crate::{dispatch2::Dispatch2, globals::GlobalData, registry::GlobalProxy}; -use std::{ - ops::DerefMut, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; use wayland_client::{ globals::GlobalList, protocol::wl_output, Connection, Dispatch, Proxy, QueueHandle, WEnum, }; @@ -32,8 +29,8 @@ pub struct WorkspaceInfo { #[derive(Clone, Debug, Default)] #[non_exhaustive] pub struct GroupInfo { - // Output - pub output: Option, + // Outputs + pub outputs: Vec, // Workspaces pub workspaces: Vec, } @@ -275,13 +272,12 @@ where handle.destroy(); } ext_workspace_group_handle_v1::Event::OutputEnter { output } => { - self.0.lock().unwrap().pending_info.output = Some(output) + self.0.lock().unwrap().pending_info.outputs.push(output); } - // TODO: multiple outputs? ext_workspace_group_handle_v1::Event::OutputLeave { output } => { - let pending_output = self.0.lock().unwrap().pending_info.output.clone(); - if pending_output.is_some_and(|o| o == output) { - self.0.lock().unwrap().pending_info.output = None; + let outputs = &mut self.0.lock().unwrap().pending_info.outputs; + if let Some(idx) = outputs.iter().position(|x| x == &output) { + outputs.remove(idx); } } ext_workspace_group_handle_v1::Event::WorkspaceEnter { workspace } => { From 7c4478d816fea84b37265de45382076970c1fb80 Mon Sep 17 00:00:00 2001 From: Spike Date: Sat, 20 Jun 2026 14:52:45 -0700 Subject: [PATCH 5/6] Implement the rest of the protocol --- src/workspace_manager.rs | 66 ++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/workspace_manager.rs b/src/workspace_manager.rs index f23f263430..dbf9d76379 100644 --- a/src/workspace_manager.rs +++ b/src/workspace_manager.rs @@ -4,13 +4,13 @@ use wayland_client::{ globals::GlobalList, protocol::wl_output, Connection, Dispatch, Proxy, QueueHandle, WEnum, }; use wayland_protocols::ext::workspace::v1::client::{ - ext_workspace_group_handle_v1, + ext_workspace_group_handle_v1::{self, GroupCapabilities}, ext_workspace_handle_v1::{self, State, WorkspaceCapabilities}, ext_workspace_manager_v1, }; /// Information about a workspace. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] #[non_exhaustive] pub struct WorkspaceInfo { // ID @@ -33,6 +33,8 @@ pub struct GroupInfo { pub outputs: Vec, // Workspaces pub workspaces: Vec, + // Capabilities + pub capabilities: Option>, } #[derive(Debug, Default)] @@ -135,6 +137,30 @@ pub trait WorkspaceHandler: Sized { workspace_handle: ext_workspace_handle_v1::ExtWorkspaceHandleV1, ); + /// A new workspace group has been created. + fn new_group( + &mut self, + conn: &Connection, + qh: &QueueHandle, + group_handle: ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ); + + /// A workspace group has been updated. + fn update_group( + &mut self, + conn: &Connection, + qh: &QueueHandle, + group_handle: ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ); + + /// A workspace group has been removed. + fn group_removed( + &mut self, + conn: &Connection, + qh: &QueueHandle, + group_handle: ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ); + /// All workspaces/groups have been updated. fn done(&mut self, _conn: &Connection, _qh: &QueueHandle) {} @@ -157,11 +183,12 @@ where qh: &QueueHandle, ) { match event { - ext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group: group } => { - state.ext_workspace_state().groups.push(group.clone()); + ext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => { + println!("new workspace group"); + state.ext_workspace_state().groups.push(workspace_group); } ext_workspace_manager_v1::Event::Workspace { workspace } => { - state.ext_workspace_state().workspaces.push(workspace.clone()); + state.ext_workspace_state().workspaces.push(workspace); } ext_workspace_manager_v1::Event::Done => { // TODO: is cloning really the best for performance? @@ -182,15 +209,14 @@ where for ref group in state.ext_workspace_state().groups.clone() { let handle = group.data::().unwrap(); let mut inner = handle.0.lock().unwrap(); - // FIXME all the commented stuff - // let just_created = inner.current_info.is_none(); + let just_created = inner.current_info.is_none(); inner.current_info = Some(inner.pending_info.clone()); drop(inner); - // if just_created { - // state.new_workspace(conn, qh, workspace.clone()); - // } else { - // state.update_workspace(conn, qh, workspace.clone()); - // } + if just_created { + state.new_group(conn, qh, group.clone()); + } else { + state.update_group(conn, qh, group.clone()); + } } state.done(conn, qh); } @@ -265,6 +291,7 @@ where ) { match event { ext_workspace_group_handle_v1::Event::Removed => { + state.group_removed(conn, qh, handle.clone()); let groups = &mut state.ext_workspace_state().groups; if let Some(idx) = groups.iter().position(|x| x == handle) { groups.remove(idx); @@ -274,24 +301,25 @@ where ext_workspace_group_handle_v1::Event::OutputEnter { output } => { self.0.lock().unwrap().pending_info.outputs.push(output); } - ext_workspace_group_handle_v1::Event::OutputLeave { output } => { + ext_workspace_group_handle_v1::Event::OutputLeave { ref output } => { let outputs = &mut self.0.lock().unwrap().pending_info.outputs; - if let Some(idx) = outputs.iter().position(|x| x == &output) { + if let Some(idx) = outputs.iter().position(|x| x == output) { outputs.remove(idx); } } ext_workspace_group_handle_v1::Event::WorkspaceEnter { workspace } => { self.0.lock().unwrap().pending_info.workspaces.push(workspace) } - ext_workspace_group_handle_v1::Event::WorkspaceLeave { workspace } => { + ext_workspace_group_handle_v1::Event::WorkspaceLeave { ref workspace } => { let workspaces = &mut self.0.lock().unwrap().pending_info.workspaces; - if let Some(idx) = workspaces.iter().position(|x| x == &workspace) { + if let Some(idx) = workspaces.iter().position(|x| x == workspace) { workspaces.remove(idx); } } - // TODO I don't know what this is and I don't care (for now) - ext_workspace_group_handle_v1::Event::Capabilities { capabilities: _ } => {} - _ => {} + ext_workspace_group_handle_v1::Event::Capabilities { capabilities } => { + self.0.lock().unwrap().pending_info.capabilities = Some(capabilities); + } + _ => unreachable!(), } } } From e7190b3dbec2dfdea95cda56f501dc5971dc4c46 Mon Sep 17 00:00:00 2001 From: Spike Date: Sat, 20 Jun 2026 15:03:30 -0700 Subject: [PATCH 6/6] whups, left a println in --- src/workspace_manager.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/workspace_manager.rs b/src/workspace_manager.rs index dbf9d76379..3fa02f5589 100644 --- a/src/workspace_manager.rs +++ b/src/workspace_manager.rs @@ -184,7 +184,6 @@ where ) { match event { ext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => { - println!("new workspace group"); state.ext_workspace_state().groups.push(workspace_group); } ext_workspace_manager_v1::Event::Workspace { workspace } => {