aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-06-14 16:17:43 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-07-13 11:43:59 +0300
commitce9ba00d54104f250f0074a54f14d1a5e4ff427f (patch)
treed0a8b4be303b9698357a5da6ff78b0c70c6eb5d7 /src
parent37458d94b288945f6cfbd3c5c233f634d59f246c (diff)
downloadniri-ce9ba00d54104f250f0074a54f14d1a5e4ff427f.tar.gz
niri-ce9ba00d54104f250f0074a54f14d1a5e4ff427f.tar.bz2
niri-ce9ba00d54104f250f0074a54f14d1a5e4ff427f.zip
Implement ext-workspace
Diffstat (limited to 'src')
-rw-r--r--src/handlers/mod.rs44
-rw-r--r--src/niri.rs6
-rw-r--r--src/protocols/ext_workspace.rs715
-rw-r--r--src/protocols/mod.rs1
4 files changed, 764 insertions, 2 deletions
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index 8353cd09..5955bcec 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -76,8 +76,10 @@ use smithay::{
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
+use crate::layout::workspace::WorkspaceId;
use crate::layout::ActivateWindow;
use crate::niri::{DndIcon, NewClient, State};
+use crate::protocols::ext_workspace::{self, ExtWorkspaceHandler, ExtWorkspaceManagerState};
use crate::protocols::foreign_toplevel::{
self, ForeignToplevelHandler, ForeignToplevelManagerState,
};
@@ -92,8 +94,9 @@ use crate::protocols::virtual_pointer::{
};
use crate::utils::{output_size, send_scale_transform, with_toplevel_role};
use crate::{
- delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop,
- delegate_output_management, delegate_screencopy, delegate_virtual_pointer,
+ delegate_ext_workspace, delegate_foreign_toplevel, delegate_gamma_control,
+ delegate_mutter_x11_interop, delegate_output_management, delegate_screencopy,
+ delegate_virtual_pointer,
};
pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10);
@@ -414,6 +417,7 @@ delegate_ext_data_control!(State);
impl OutputHandler for State {
fn output_bound(&mut self, output: Output, wl_output: WlOutput) {
foreign_toplevel::on_output_bound(self, &output, &wl_output);
+ ext_workspace::on_output_bound(self, &output, &wl_output);
}
}
delegate_output!(State);
@@ -574,6 +578,42 @@ impl ForeignToplevelHandler for State {
}
delegate_foreign_toplevel!(State);
+impl ExtWorkspaceHandler for State {
+ fn ext_workspace_manager_state(&mut self) -> &mut ExtWorkspaceManagerState {
+ &mut self.niri.ext_workspace_state
+ }
+
+ fn activate_workspace(&mut self, id: WorkspaceId) {
+ let reference = niri_config::WorkspaceReference::Id(id.get());
+ if let Some((mut output, index)) = self.niri.find_output_and_workspace_index(reference) {
+ if let Some(active) = self.niri.layout.active_output() {
+ if output.as_ref() == Some(active) {
+ output = None;
+ }
+ }
+
+ if let Some(output) = output {
+ self.niri.layout.focus_output(&output);
+ }
+ self.niri.layout.switch_workspace(index);
+ // No mouse warp: assuming the layer-shell bar workspaces use-case.
+
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ }
+
+ fn assign_workspace(&mut self, ws_id: WorkspaceId, output: Output) {
+ let reference = niri_config::WorkspaceReference::Id(ws_id.get());
+ if let Some((old_output, old_idx)) = self.niri.find_output_and_workspace_index(reference) {
+ self.niri
+ .layout
+ .move_workspace_to_output_by_id(old_idx, old_output, output);
+ }
+ }
+}
+delegate_ext_workspace!(State);
+
impl ScreencopyHandler for State {
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
// If with_damage then push it onto the queue for redraw of the output,
diff --git a/src/niri.rs b/src/niri.rs
index fd4112e6..05db2fd0 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -136,6 +136,7 @@ use crate::layout::tile::TileRenderElement;
use crate::layout::workspace::{Workspace, WorkspaceId};
use crate::layout::{HitType, Layout, LayoutElement as _, MonitorRenderElement};
use crate::niri_render_elements;
+use crate::protocols::ext_workspace::{self, ExtWorkspaceManagerState};
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
use crate::protocols::gamma_control::GammaControlManagerState;
use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState;
@@ -259,6 +260,7 @@ pub struct Niri {
pub layer_shell_state: WlrLayerShellState,
pub session_lock_state: SessionLockManagerState,
pub foreign_toplevel_state: ForeignToplevelManagerState,
+ pub ext_workspace_state: ExtWorkspaceManagerState,
pub screencopy_state: ScreencopyManagerState,
pub output_management_state: OutputManagementManagerState,
pub viewporter_state: ViewporterState,
@@ -719,6 +721,7 @@ impl State {
self.niri.refresh_idle_inhibit();
self.refresh_pointer_contents();
foreign_toplevel::refresh(self);
+ ext_workspace::refresh(self);
#[cfg(feature = "xdp-gnome-screencast")]
self.niri.refresh_mapped_cast_outputs();
@@ -2325,6 +2328,8 @@ impl Niri {
VirtualPointerManagerState::new::<State, _>(&display_handle, client_is_unrestricted);
let foreign_toplevel_state =
ForeignToplevelManagerState::new::<State, _>(&display_handle, client_is_unrestricted);
+ let ext_workspace_state =
+ ExtWorkspaceManagerState::new::<State, _>(&display_handle, client_is_unrestricted);
let mut output_management_state =
OutputManagementManagerState::new::<State, _>(&display_handle, client_is_unrestricted);
output_management_state.on_config_changed(config_.outputs.clone());
@@ -2519,6 +2524,7 @@ impl Niri {
layer_shell_state,
session_lock_state,
foreign_toplevel_state,
+ ext_workspace_state,
output_management_state,
screencopy_state,
viewporter_state,
diff --git a/src/protocols/ext_workspace.rs b/src/protocols/ext_workspace.rs
new file mode 100644
index 00000000..3d97cc4f
--- /dev/null
+++ b/src/protocols/ext_workspace.rs
@@ -0,0 +1,715 @@
+//! 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<ExtWorkspaceManagerV1, Vec<Action>>,
+ workspace_groups: HashMap<Output, ExtWorkspaceGroupData>,
+ workspaces: HashMap<WorkspaceId, ExtWorkspaceData>,
+}
+
+struct ExtWorkspaceGroupData {
+ instances: Vec<ExtWorkspaceGroupHandleV1>,
+}
+
+struct ExtWorkspaceData {
+ // id cannot change once set.
+ id: Option<String>,
+ name: String,
+ coordinates: ArrayVec<u32, 2>,
+ state: ext_workspace_handle_v1::State,
+ instances: Vec<ExtWorkspaceHandleV1>,
+ output: Option<Output>,
+}
+
+pub struct ExtWorkspaceGlobalData {
+ filter: Box<dyn for<'c> 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::<State>(&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<Output, ExtWorkspaceGroupData>,
+ 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<Output, ExtWorkspaceGroupData>,
+ data: &ExtWorkspaceData,
+) {
+ send_workspace_enter_leave(workspace_groups, data, false);
+
+ for workspace in &data.instances {
+ workspace.removed();
+ }
+}
+
+fn build_name(ws: &Workspace<Mapped>, 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<Mapped>>,
+ ws_idx: usize,
+ ws: &Workspace<Mapped>,
+) -> 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::<State>(&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::<State>(&protocol_state.display, &client, manager);
+ }
+ }
+
+ send_workspace_enter_leave(&protocol_state.workspace_groups, &data, true);
+ entry.insert(data);
+ true
+ }
+ }
+}
+
+impl ExtWorkspaceGroupData {
+ fn add_instance<D>(
+ &mut self,
+ handle: &DisplayHandle,
+ client: &Client,
+ manager: &ExtWorkspaceManagerV1,
+ output: &Output,
+ ) -> &ExtWorkspaceGroupHandleV1
+ where
+ D: Dispatch<ExtWorkspaceGroupHandleV1, ExtWorkspaceManagerV1>,
+ D: 'static,
+ {
+ let group = client
+ .create_resource::<ExtWorkspaceGroupHandleV1, _, D>(
+ 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<D>(
+ &mut self,
+ handle: &DisplayHandle,
+ client: &Client,
+ manager: &ExtWorkspaceManagerV1,
+ ) -> &ExtWorkspaceHandleV1
+ where
+ D: Dispatch<ExtWorkspaceHandleV1, ExtWorkspaceManagerV1>,
+ D: 'static,
+ {
+ let workspace = client
+ .create_resource::<ExtWorkspaceHandleV1, _, D>(
+ 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<D, F>(display: &DisplayHandle, filter: F) -> Self
+ where
+ D: GlobalDispatch<ExtWorkspaceManagerV1, ExtWorkspaceGlobalData>,
+ D: Dispatch<ExtWorkspaceManagerV1, ()>,
+ D: 'static,
+ F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
+ {
+ let global_data = ExtWorkspaceGlobalData {
+ filter: Box::new(filter),
+ };
+ display.create_global::<D, ExtWorkspaceManagerV1, _>(VERSION, global_data);
+ Self {
+ display: display.clone(),
+ instances: HashMap::new(),
+ workspace_groups: HashMap::new(),
+ workspaces: HashMap::new(),
+ }
+ }
+}
+
+impl<D> GlobalDispatch<ExtWorkspaceManagerV1, ExtWorkspaceGlobalData, D>
+ for ExtWorkspaceManagerState
+where
+ D: GlobalDispatch<ExtWorkspaceManagerV1, ExtWorkspaceGlobalData>,
+ D: Dispatch<ExtWorkspaceManagerV1, ()>,
+ D: Dispatch<ExtWorkspaceHandleV1, ExtWorkspaceManagerV1>,
+ D: ExtWorkspaceHandler,
+{
+ fn bind(
+ state: &mut D,
+ handle: &DisplayHandle,
+ client: &Client,
+ resource: New<ExtWorkspaceManagerV1>,
+ _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::<State>(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::<State>(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<D> Dispatch<ExtWorkspaceManagerV1, (), D> for ExtWorkspaceManagerState
+where
+ D: Dispatch<ExtWorkspaceManagerV1, ()>,
+ D: ExtWorkspaceHandler,
+{
+ fn request(
+ state: &mut D,
+ _client: &Client,
+ resource: &ExtWorkspaceManagerV1,
+ request: <ExtWorkspaceManagerV1 as Resource>::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<D> Dispatch<ExtWorkspaceHandleV1, ExtWorkspaceManagerV1, D> for ExtWorkspaceManagerState
+where
+ D: Dispatch<ExtWorkspaceHandleV1, ExtWorkspaceManagerV1>,
+ D: ExtWorkspaceHandler,
+{
+ fn request(
+ state: &mut D,
+ _client: &Client,
+ resource: &ExtWorkspaceHandleV1,
+ request: <ExtWorkspaceHandleV1 as Resource>::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<D> Dispatch<ExtWorkspaceGroupHandleV1, ExtWorkspaceManagerV1, D> for ExtWorkspaceManagerState
+where
+ D: Dispatch<ExtWorkspaceGroupHandleV1, ExtWorkspaceManagerV1>,
+ D: ExtWorkspaceHandler,
+{
+ fn request(
+ _state: &mut D,
+ _client: &Client,
+ _resource: &ExtWorkspaceGroupHandleV1,
+ request: <ExtWorkspaceGroupHandleV1 as Resource>::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);
+ };
+}
diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs
index 476f24eb..b150cb7f 100644
--- a/src/protocols/mod.rs
+++ b/src/protocols/mod.rs
@@ -1,3 +1,4 @@
+pub mod ext_workspace;
pub mod foreign_toplevel;
pub mod gamma_control;
pub mod mutter_x11_interop;