//! ext-workspace protocol implementation. //! //! This is how we map the protocol concepts to the niri concepts: //! //! - Workspace groups are outputs. //! - Workspace coordinates: X = 0, Y = workspace index. They need to be two-dimensional because 1D //! coordinates are defined to be a plain list without a geometric interpretation, while we do //! order workspaces in a vertical line. //! - Workspace id: name for named workspaces, unset for unnamed. Because ids in this protocol are //! expected to be stable across sessions. //! - Workspace name: name for named workspaces, index for unnamed. use std::collections::hash_map::Entry; use std::collections::HashMap; use std::mem; use arrayvec::ArrayVec; use ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1; use ext_workspace_handle_v1::ExtWorkspaceHandleV1; use ext_workspace_manager_v1::ExtWorkspaceManagerV1; use smithay::output::{Output, WeakOutput}; use smithay::reexports::wayland_protocols::ext::workspace::v1::server::{ ext_workspace_group_handle_v1, ext_workspace_handle_v1, ext_workspace_manager_v1, }; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::{ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, }; use wayland_backend::server::ClientId; use crate::layout::monitor::Monitor; use crate::layout::workspace::{Workspace, WorkspaceId}; use crate::niri::State; use crate::window::Mapped; const VERSION: u32 = 1; pub trait ExtWorkspaceHandler { fn ext_workspace_manager_state(&mut self) -> &mut ExtWorkspaceManagerState; fn activate_workspace(&mut self, id: WorkspaceId); fn assign_workspace(&mut self, ws_id: WorkspaceId, output: Output); } enum Action { Assign(WorkspaceId, WeakOutput), Activate(WorkspaceId), } impl Action { fn order(&self) -> u8 { // First assign everything (move across outputs), then activate. match self { Action::Assign(_, _) => 0, Action::Activate(_) => 1, } } } pub struct ExtWorkspaceManagerState { display: DisplayHandle, instances: HashMap>, workspace_groups: HashMap, workspaces: HashMap, } struct ExtWorkspaceGroupData { instances: Vec, } struct ExtWorkspaceData { // id cannot change once set. id: Option, name: String, coordinates: ArrayVec, state: ext_workspace_handle_v1::State, instances: Vec, output: Option, } pub struct ExtWorkspaceGlobalData { filter: Box Fn(&'c Client) -> bool + Send + Sync>, } pub fn refresh(state: &mut State) { let _span = tracy_client::span!("ext_workspace::refresh"); let protocol_state = &mut state.niri.ext_workspace_state; let mut changed = false; // Remove workspaces that no longer exist (sending workspace_leave to workspace groups). let mut seen_workspaces = HashMap::new(); for (mon, _, ws) in state.niri.layout.workspaces() { let output = mon.map(|mon| mon.output()); seen_workspaces.insert(ws.id(), output); } protocol_state.workspaces.retain(|id, workspace| { if seen_workspaces.contains_key(id) { return true; } remove_workspace_instances(&protocol_state.workspace_groups, workspace); changed = true; false }); // Remove workspace groups for outputs that no longer exist. protocol_state.workspace_groups.retain(|output, data| { if state.niri.sorted_outputs.contains(output) { return true; } for group in &data.instances { // Send workspace_leave for all workspaces in this group with matching manager. let manager: &ExtWorkspaceManagerV1 = group.data().unwrap(); for ws in protocol_state.workspaces.values() { if ws.output.as_ref() == Some(output) { for workspace in &ws.instances { if workspace.data() == Some(manager) { group.workspace_leave(workspace); } } } } group.removed(); } changed = true; false }); // Update existing workspaces and create new ones. for (mon, ws_idx, ws) in state.niri.layout.workspaces() { changed |= refresh_workspace(protocol_state, mon, ws_idx, ws); } // Update workspace groups and create new ones, sending workspace_enter events as needed. for output in &state.niri.sorted_outputs { changed |= refresh_workspace_group(protocol_state, output); } if changed { for manager in protocol_state.instances.keys() { manager.done(); } } } pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput) { let Some(client) = wl_output.client() else { return; }; let mut sent = false; let protocol_state = &mut state.niri.ext_workspace_state; if let Some(data) = protocol_state.workspace_groups.get_mut(output) { for group in &mut data.instances { if group.client().as_ref() != Some(&client) { continue; } group.output_enter(wl_output); sent = true; } } if !sent { return; } for manager in protocol_state.instances.keys() { if manager.client().as_ref() == Some(&client) { manager.done(); } } } fn refresh_workspace_group(protocol_state: &mut ExtWorkspaceManagerState, output: &Output) -> bool { if protocol_state.workspace_groups.contains_key(output) { // Existing workspace group. Nothing can actually change since our workspace groups are tied // to an output. return false; } // New workspace group, start tracking it. let mut data = ExtWorkspaceGroupData { instances: Vec::new(), }; // Create workspace group handle for each manager instance. for manager in protocol_state.instances.keys() { if let Some(client) = manager.client() { data.add_instance::(&protocol_state.display, &client, manager, output); } } // Send workspace_enter for all existing workspaces on this output. for group in &data.instances { let manager: &ExtWorkspaceManagerV1 = group.data().unwrap(); for (_, ws) in protocol_state.workspaces.iter() { if ws.output.as_ref() != Some(output) { continue; } for workspace in &ws.instances { if workspace.data() == Some(manager) { group.workspace_enter(workspace); } } } } protocol_state.workspace_groups.insert(output.clone(), data); true } fn send_workspace_enter_leave( workspace_groups: &HashMap, data: &ExtWorkspaceData, enter: bool, ) { if let Some(output) = &data.output { if let Some(group_data) = workspace_groups.get(output) { for group in &group_data.instances { let manager: &ExtWorkspaceManagerV1 = group.data().unwrap(); for workspace in &data.instances { if workspace.data() == Some(manager) { if enter { group.workspace_enter(workspace); } else { group.workspace_leave(workspace); } } } } } } } fn remove_workspace_instances( workspace_groups: &HashMap, data: &ExtWorkspaceData, ) { send_workspace_enter_leave(workspace_groups, data, false); for workspace in &data.instances { workspace.removed(); } } fn build_name(ws: &Workspace, ws_idx: usize) -> String { ws.name().cloned().unwrap_or_else(|| { // Add 1 since this is a human-readable name, and our action indexing is 1-based. (ws_idx + 1).to_string() }) } fn refresh_workspace( protocol_state: &mut ExtWorkspaceManagerState, mon: Option<&Monitor>, ws_idx: usize, ws: &Workspace, ) -> bool { let mut state = ext_workspace_handle_v1::State::empty(); if mon.is_some_and(|mon| mon.active_workspace_idx() == ws_idx) { state |= ext_workspace_handle_v1::State::Active; } if ws.is_urgent() { state |= ext_workspace_handle_v1::State::Urgent; } let output = mon.map(|mon| mon.output()); match protocol_state.workspaces.entry(ws.id()) { Entry::Occupied(entry) => { // Existing workspace, check if anything changed. let data = entry.into_mut(); let mut id_set = false; let mut recreate = false; let id = ws.name(); if data.id.as_ref() != id { if data.id.is_some() { recreate = true; } else { id_set = true; } data.id = id.cloned(); } let mut coordinates_changed = false; if data.coordinates[1] != ws_idx as u32 { data.coordinates[1] = ws_idx as u32; coordinates_changed = true; } let mut state_changed = false; if data.state != state { data.state = state; state_changed = true; } // Recreate means name got changed or unset (meaning data.name is back to ws_idx). let check = recreate || if data.id.is_some() { // True means workspace got named, going from ws_idx to name. id_set } else { // The workspace is unnamed, check if ws_idx changed. coordinates_changed }; let mut name_changed = false; if check { let new_name = build_name(ws, ws_idx); // This will likely be true, except if the workspace got named its index. if data.name != new_name { data.name = new_name; name_changed = true; } } let mut output_changed = false; if data.output.as_ref() != output { send_workspace_enter_leave(&protocol_state.workspace_groups, data, false); data.output = output.cloned(); output_changed = true; } if recreate { remove_workspace_instances(&protocol_state.workspace_groups, data); data.instances.clear(); for manager in protocol_state.instances.keys() { if let Some(client) = manager.client() { data.add_instance::(&protocol_state.display, &client, manager); } } send_workspace_enter_leave(&protocol_state.workspace_groups, data, true); return true; } if output_changed { // Send workspace_enter to the new output's group. If the group doesn't exist yet // (new groups are created after refreshing workspaces), then workspace_enter() will // be sent when the group is created. send_workspace_enter_leave(&protocol_state.workspace_groups, data, true); } let something_changed = id_set || name_changed || coordinates_changed || state_changed; if something_changed { for instance in &data.instances { if id_set { instance.id(data.id.clone().unwrap()); } if name_changed { instance.name(data.name.clone()); } if coordinates_changed { instance.coordinates( data.coordinates .iter() .flat_map(|x| x.to_ne_bytes()) .collect(), ); } if state_changed { instance.state(data.state); } } } output_changed || something_changed } Entry::Vacant(entry) => { // New workspace, start tracking it. let mut data = ExtWorkspaceData { id: ws.name().cloned(), name: build_name(ws, ws_idx), coordinates: ArrayVec::from([0, ws_idx as u32]), state, instances: Vec::new(), output: output.cloned(), }; for manager in protocol_state.instances.keys() { if let Some(client) = manager.client() { data.add_instance::(&protocol_state.display, &client, manager); } } send_workspace_enter_leave(&protocol_state.workspace_groups, &data, true); entry.insert(data); true } } } impl ExtWorkspaceGroupData { fn add_instance( &mut self, handle: &DisplayHandle, client: &Client, manager: &ExtWorkspaceManagerV1, output: &Output, ) -> &ExtWorkspaceGroupHandleV1 where D: Dispatch, D: 'static, { let group = client .create_resource::( handle, manager.version(), manager.clone(), ) .unwrap(); manager.workspace_group(&group); group.capabilities(ext_workspace_group_handle_v1::GroupCapabilities::empty()); for wl_output in output.client_outputs(client) { group.output_enter(&wl_output); } self.instances.push(group); self.instances.last().unwrap() } } impl ExtWorkspaceData { fn add_instance( &mut self, handle: &DisplayHandle, client: &Client, manager: &ExtWorkspaceManagerV1, ) -> &ExtWorkspaceHandleV1 where D: Dispatch, D: 'static, { let workspace = client .create_resource::( handle, manager.version(), manager.clone(), ) .unwrap(); manager.workspace(&workspace); if let Some(id) = self.id.clone() { workspace.id(id); } workspace.name(self.name.clone()); workspace.coordinates( self.coordinates .iter() .flat_map(|x| x.to_ne_bytes()) .collect(), ); workspace.state(self.state); workspace.capabilities( ext_workspace_handle_v1::WorkspaceCapabilities::Activate | ext_workspace_handle_v1::WorkspaceCapabilities::Assign, ); self.instances.push(workspace); self.instances.last().unwrap() } } impl ExtWorkspaceManagerState { pub fn new(display: &DisplayHandle, filter: F) -> Self where D: GlobalDispatch, D: Dispatch, D: 'static, F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, { let global_data = ExtWorkspaceGlobalData { filter: Box::new(filter), }; display.create_global::(VERSION, global_data); Self { display: display.clone(), instances: HashMap::new(), workspace_groups: HashMap::new(), workspaces: HashMap::new(), } } } impl GlobalDispatch for ExtWorkspaceManagerState where D: GlobalDispatch, D: Dispatch, D: Dispatch, D: ExtWorkspaceHandler, { fn bind( state: &mut D, handle: &DisplayHandle, client: &Client, resource: New, _global_data: &ExtWorkspaceGlobalData, data_init: &mut DataInit<'_, D>, ) { let manager = data_init.init(resource, ()); let state = state.ext_workspace_manager_state(); // Send existing workspaces to the new client. let mut new_workspaces: HashMap<_, Vec<_>> = HashMap::new(); for data in state.workspaces.values_mut() { let output = data.output.clone(); let workspace = data.add_instance::(handle, client, &manager); if let Some(output) = output { new_workspaces.entry(output).or_default().push(workspace); } } // Create workspace groups for all outputs. for (output, group_data) in &mut state.workspace_groups { let group = group_data.add_instance::(handle, client, &manager, output); for workspace in new_workspaces.get(output).into_iter().flatten() { group.workspace_enter(workspace); } } manager.done(); state.instances.insert(manager, Vec::new()); } fn can_view(client: Client, global_data: &ExtWorkspaceGlobalData) -> bool { (global_data.filter)(&client) } } impl Dispatch for ExtWorkspaceManagerState where D: Dispatch, D: ExtWorkspaceHandler, { fn request( state: &mut D, _client: &Client, resource: &ExtWorkspaceManagerV1, request: ::Request, _data: &(), _dhandle: &DisplayHandle, _data_init: &mut DataInit<'_, D>, ) { match request { ext_workspace_manager_v1::Request::Commit => { let protocol_state = state.ext_workspace_manager_state(); let actions = protocol_state.instances.get_mut(resource).unwrap(); let mut actions = mem::take(actions); actions.sort_by_key(Action::order); for action in actions { match action { Action::Assign(ws_id, output) => { if let Some(output) = output.upgrade() { state.assign_workspace(ws_id, output); } } Action::Activate(id) => state.activate_workspace(id), } } } ext_workspace_manager_v1::Request::Stop => { resource.finished(); let state = state.ext_workspace_manager_state(); state.instances.retain(|x, _| x != resource); for data in state.workspace_groups.values_mut() { data.instances .retain(|instance| instance.data() != Some(resource)); } for data in state.workspaces.values_mut() { data.instances .retain(|instance| instance.data() != Some(resource)); } } _ => unreachable!(), } } fn destroyed(state: &mut D, _client: ClientId, resource: &ExtWorkspaceManagerV1, _data: &()) { let state = state.ext_workspace_manager_state(); state.instances.retain(|x, _| x != resource); } } impl Dispatch for ExtWorkspaceManagerState where D: Dispatch, D: ExtWorkspaceHandler, { fn request( state: &mut D, _client: &Client, resource: &ExtWorkspaceHandleV1, request: ::Request, data: &ExtWorkspaceManagerV1, _dhandle: &DisplayHandle, _data_init: &mut DataInit<'_, D>, ) { let protocol_state = state.ext_workspace_manager_state(); let Some((workspace, _)) = protocol_state .workspaces .iter() .find(|(_, data)| data.instances.contains(resource)) else { return; }; let workspace = *workspace; match request { ext_workspace_handle_v1::Request::Activate => { let actions = protocol_state.instances.get_mut(data).unwrap(); actions.push(Action::Activate(workspace)); } ext_workspace_handle_v1::Request::Deactivate => (), ext_workspace_handle_v1::Request::Assign { workspace_group } => { if let Some(output) = protocol_state .workspace_groups .iter() .find(|(_, data)| data.instances.contains(&workspace_group)) .map(|(output, _)| output.clone()) { let actions = protocol_state.instances.get_mut(data).unwrap(); actions.push(Action::Assign(workspace, output.downgrade())); } } ext_workspace_handle_v1::Request::Remove => (), ext_workspace_handle_v1::Request::Destroy => (), _ => unreachable!(), } } fn destroyed( state: &mut D, _client: ClientId, resource: &ExtWorkspaceHandleV1, _data: &ExtWorkspaceManagerV1, ) { let state = state.ext_workspace_manager_state(); for data in state.workspaces.values_mut() { data.instances.retain(|instance| instance != resource); } } } impl Dispatch for ExtWorkspaceManagerState where D: Dispatch, D: ExtWorkspaceHandler, { fn request( _state: &mut D, _client: &Client, _resource: &ExtWorkspaceGroupHandleV1, request: ::Request, _data: &ExtWorkspaceManagerV1, _dhandle: &DisplayHandle, _data_init: &mut DataInit<'_, D>, ) { match request { ext_workspace_group_handle_v1::Request::CreateWorkspace { .. } => (), ext_workspace_group_handle_v1::Request::Destroy => (), _ => unreachable!(), } } fn destroyed( state: &mut D, _client: ClientId, resource: &ExtWorkspaceGroupHandleV1, _data: &ExtWorkspaceManagerV1, ) { let state = state.ext_workspace_manager_state(); for data in state.workspace_groups.values_mut() { data.instances.retain(|instance| instance != resource); } } } #[macro_export] macro_rules! delegate_ext_workspace { ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1: $crate::protocols::ext_workspace::ExtWorkspaceGlobalData ] => $crate::protocols::ext_workspace::ExtWorkspaceManagerState); smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1: () ] => $crate::protocols::ext_workspace::ExtWorkspaceManagerState); smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_handle_v1::ExtWorkspaceHandleV1: smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1 ] => $crate::protocols::ext_workspace::ExtWorkspaceManagerState); smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1: smithay::reexports::wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1 ] => $crate::protocols::ext_workspace::ExtWorkspaceManagerState); }; }