atagen

monolith

public
Star0
Watch0

Commit a1a05e799a71

tarball

atagen <boss@atagen.co>2026-10-16 10:21 UTC

9 files changed+676-0
protocols: serve ext-workspace from layout snapshots

The compositor publishes a full snapshot and each client is sent only the difference from what it last saw. That keeps tag and view bookkeeping out of the protocol code, and wl_outputs bound after the manager are picked up without an extra hook. Activating a workspace views its tag alone, matching what a bar click means. Floating windows also become movable and resizable with Mod and the mouse.
Modified file: Cargo.lock+3 additions
@@ -398,6 +398,7 @@ dependencies = [
398398 "monolith-config",
399399 "monolith-ipc",
400400 "monolith-layout",
401+ "monolith-protocols",
401402 "monolith-render-vk",
402403 "serde_json",
403404 "slotmap",
@@ -454,6 +455,8 @@ name = "monolith-protocols"
454455 version = "0.0.0"
455456 dependencies = [
456457 "smithay",
458+ "wayland-client",
459+ "wayland-protocols",
457460 ]
458461
459462 [[package]]
Modified file: crates/monolith/Cargo.toml+1 additions
@@ -16,6 +16,7 @@ calloop-wayland-source = "0.4"
1616 monolith-config.workspace = true
1717 monolith-ipc.workspace = true
1818 monolith-layout.workspace = true
19+monolith-protocols.workspace = true
1920 monolith-render-vk.workspace = true
2021 serde_json.workspace = true
2122 slotmap.workspace = true
Modified file: crates/monolith/src/handlers.rs+36 additions
@@ -27,6 +27,7 @@ use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
2727 use smithay::wayland::shell::xdg::{
2828 PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
2929 };
30+use monolith_protocols::workspace::{Action, WorkspaceHandler, WorkspaceState};
3031 use smithay::wayland::shm::{ShmHandler, ShmState};
3132
3233 impl CompositorHandler for State {
@@ -245,3 +246,38 @@ impl State {
245246 }
246247 }
247248 }
249+
250+impl WorkspaceHandler for State {
251+ fn workspace_state(&mut self) -> &mut WorkspaceState {
252+ &mut self.workspaces
253+ }
254+
255+ fn workspace_actions(&mut self, actions: Vec<Action>) {
256+ for action in actions {
257+ let (group, tag) = match &action {
258+ Action::Activate { group, workspace } | Action::Deactivate { group, workspace } => {
259+ (group, workspace)
260+ }
261+ Action::Create { group, name } => (group, name),
262+ };
263+ let Some(o) = self
264+ .layout
265+ .outputs()
266+ .find(|(_, out)| out.name == *group)
267+ .map(|(o, _)| o)
268+ else {
269+ continue;
270+ };
271+ let t = self.layout.tag(tag);
272+ match action {
273+ Action::Activate { .. } | Action::Create { .. } => self.layout.view(o, t),
274+ Action::Deactivate { .. } => {
275+ if self.layout.is_viewed(o, t) {
276+ self.layout.toggle_view(o, t);
277+ }
278+ }
279+ }
280+ }
281+ self.apply_layout();
282+ }
283+}
Modified file: crates/monolith/src/input.rs+80 additions
@@ -1,9 +1,12 @@
11 //! Host input forwarded into monolith's seat.
22
33 use crate::state::State;
4+use monolith_config::Modifier;
5+use monolith_layout::{Rect, WindowId};
46 use smithay::backend::input::{Axis, ButtonState, InputTime, KeyState};
57 use smithay::input::keyboard::{FilterResult, Keycode};
68 use smithay::input::pointer::{AxisFrame, ButtonEvent, MotionEvent};
9+use smithay::utils::Logical;
710 use smithay::utils::{Point, SERIAL_COUNTER};
811 use wayland_client::protocol::{wl_keyboard, wl_pointer, wl_seat};
912 use wayland_client::{Connection, Dispatch, QueueHandle, WEnum};
@@ -59,6 +62,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
5962 if let Some(host) = state.host.as_mut() {
6063 host.pointer_pos = pos;
6164 }
65+ if drag_to(state, pos) {
66+ return;
67+ }
6268 let under = state.surface_under(pos);
6369 pointer.motion(
6470 state,
@@ -80,8 +86,14 @@ impl Dispatch<wl_pointer::WlPointer, ()> for State {
8086 wl_pointer::ButtonState::Pressed => ButtonState::Pressed,
8187 _ => ButtonState::Released,
8288 };
89+ if button_state == ButtonState::Released && state.drag.take().is_some() {
90+ return;
91+ }
8392 if button_state == ButtonState::Pressed {
8493 focus_under_pointer(state);
94+ if start_drag(state, button) {
95+ return;
96+ }
8597 }
8698 pointer.button(
8799 state,
@@ -184,3 +196,71 @@ fn focus_under_pointer(state: &mut State) {
184196 state.apply_layout();
185197 }
186198 }
199+
200+const BTN_LEFT: u32 = 0x110;
201+const BTN_RIGHT: u32 = 0x111;
202+
203+/// A floating window being moved or resized with Mod and a mouse button.
204+pub struct Drag {
205+ window: WindowId,
206+ from: Point<f64, Logical>,
207+ rect: Rect,
208+ resize: bool,
209+}
210+
211+fn start_drag(state: &mut State, button: u32) -> bool {
212+ if button != BTN_LEFT && button != BTN_RIGHT {
213+ return false;
214+ }
215+ let held = state
216+ .seat
217+ .get_keyboard()
218+ .map(|k| k.modifier_state())
219+ .is_some_and(|m| match state.config.general.mod_key {
220+ Modifier::Super => m.logo,
221+ Modifier::Alt => m.alt,
222+ Modifier::Ctrl => m.ctrl,
223+ Modifier::Shift => m.shift,
224+ });
225+ let Some(from) = state.host.as_ref().map(|h| h.pointer_pos) else {
226+ return false;
227+ };
228+ let Some(id) = state.layout.focused() else {
229+ return false;
230+ };
231+ let Some(rect) = state
232+ .layout
233+ .window(id)
234+ .filter(|w| w.floating && !w.fullscreen)
235+ .and_then(|w| w.float_rect)
236+ else {
237+ return false;
238+ };
239+ if !held {
240+ return false;
241+ }
242+ state.drag = Some(Drag {
243+ window: id,
244+ from,
245+ rect,
246+ resize: button == BTN_RIGHT,
247+ });
248+ true
249+}
250+
251+fn drag_to(state: &mut State, pos: Point<f64, Logical>) -> bool {
252+ let Some(d) = &state.drag else {
253+ return false;
254+ };
255+ let (dx, dy) = (pos.x - d.from.x, pos.y - d.from.y);
256+ let r = d.rect;
257+ let rect = if d.resize {
258+ Rect::new(r.x, r.y, (r.w + dx).max(32.0), (r.h + dy).max(32.0))
259+ } else {
260+ Rect::new(r.x + dx, r.y + dy, r.w, r.h)
261+ };
262+ let id = d.window;
263+ state.layout.set_float_rect(id, rect);
264+ state.apply_layout();
265+ true
266+}
Modified file: crates/monolith/src/state.rs+39 additions
@@ -2,6 +2,7 @@ use crate::commands::Binds;
22 use crate::nested::Host;
33 use monolith_config::Config;
44 use monolith_layout::{Layout, OutputId, Rect, WindowId};
5+use monolith_protocols::workspace::{Group, Snapshot, Workspace, WorkspaceState};
56 use monolith_render_vk::VulkanRenderer;
67 use smithay::backend::renderer::ImportDma;
78 use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};
@@ -49,8 +50,10 @@ pub struct State {
4950 pub config_path: Option<PathBuf>,
5051 pub binds: Binds,
5152 pub suppressed: Vec<Keycode>,
53+ pub drag: Option<crate::input::Drag>,
5254 pub ipc: crate::ipc::Server,
5355 pub launches: crate::launch::Launches,
56+ pub workspaces: WorkspaceState,
5457
5558 pub compositor_state: CompositorState,
5659 pub xdg_shell_state: XdgShellState,
@@ -124,8 +127,10 @@ impl State {
124127 config_path,
125128 binds,
126129 suppressed: Vec::new(),
130+ drag: None,
127131 ipc: crate::ipc::Server::default(),
128132 launches: Default::default(),
133+ workspaces: WorkspaceState::new::<State>(&dh),
129134 compositor_state: CompositorState::new::<State>(&dh),
130135 xdg_shell_state: XdgShellState::new::<State>(&dh),
131136 _xdg_decoration_state: XdgDecorationState::new::<State>(&dh),
@@ -146,6 +151,7 @@ impl State {
146151 self.popups.cleanup();
147152 crate::nested::render(self);
148153 crate::ipc::broadcast(self);
154+ self.publish_workspaces();
149155 let _ = self.display.flush_clients();
150156 }
151157
@@ -286,6 +292,39 @@ impl State {
286292 }
287293 }
288294
295+ fn publish_workspaces(&mut self) {
296+ if !self.workspaces.has_clients() {
297+ return;
298+ }
299+ let l = &self.layout;
300+ let groups = l
301+ .outputs()
302+ .filter_map(|(o, _)| {
303+ let output = self
304+ .host
305+ .as_ref()
306+ .filter(|_| Some(o) == self.output_id)?
307+ .output
308+ .clone();
309+ let workspaces = l
310+ .tags()
311+ .enumerate()
312+ .map(|(i, (t, tag))| Workspace {
313+ key: tag.name.clone(),
314+ name: tag.name.clone(),
315+ coordinates: vec![i as u32],
316+ active: l.is_viewed(o, t),
317+ urgent: tag.urgent,
318+ hidden: false,
319+ })
320+ .collect();
321+ Some(Group { output, workspaces })
322+ })
323+ .collect();
324+ let dh = self.display.clone();
325+ self.workspaces.update::<State>(&dh, Snapshot { groups });
326+ }
327+
289328 pub fn reconfigure_keyboard(&mut self) {
290329 let kb = self.config.keyboard.clone();
291330 if let Some(keyboard) = self.seat.get_keyboard() {
Modified file: crates/protocols/Cargo.toml+4 additions
@@ -10,3 +10,7 @@ workspace = true
1010
1111 [dependencies]
1212 smithay.workspace = true
13+
14+[dev-dependencies]
15+wayland-client = "0.31"
16+wayland-protocols = { version = "0.32", features = ["client", "staging"] }
Added file: crates/protocols/examples/workspaces.rs+114 additions
@@ -0,0 +1,114 @@
1+//! Prints ext-workspace events from the compositor on $WAYLAND_DISPLAY.
2+//! `workspaces activate <name>` activates a workspace and exits.
3+
4+use wayland_client::globals::{GlobalListContents, registry_queue_init};
5+use wayland_client::protocol::{wl_output, wl_registry};
6+use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, event_created_child};
7+use wayland_protocols::ext::workspace::v1::client::{
8+ ext_workspace_group_handle_v1 as group, ext_workspace_handle_v1 as ws,
9+ ext_workspace_manager_v1 as manager,
10+};
11+
12+#[derive(Default)]
13+struct App {
14+ names: Vec<(ws::ExtWorkspaceHandleV1, String)>,
15+}
16+
17+fn main() {
18+ let target = std::env::args().nth(2);
19+ let conn = Connection::connect_to_env().expect("no compositor");
20+ let (globals, mut queue) = registry_queue_init::<App>(&conn).unwrap();
21+ let qh = queue.handle();
22+ let _output: wl_output::WlOutput = globals.bind(&qh, 1..=4, ()).unwrap();
23+ let m: manager::ExtWorkspaceManagerV1 = globals.bind(&qh, 1..=1, ()).unwrap();
24+ let mut app = App::default();
25+ queue.roundtrip(&mut app).unwrap();
26+ queue.roundtrip(&mut app).unwrap();
27+ if let Some(name) = target {
28+ if let Some((h, _)) = app.names.iter().find(|(_, n)| *n == name) {
29+ h.activate();
30+ m.commit();
31+ queue.roundtrip(&mut app).unwrap();
32+ queue.roundtrip(&mut app).unwrap();
33+ }
34+ return;
35+ }
36+ loop {
37+ queue.blocking_dispatch(&mut app).unwrap();
38+ }
39+}
40+
41+impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for App {
42+ fn event(
43+ _: &mut Self,
44+ _: &wl_registry::WlRegistry,
45+ _: wl_registry::Event,
46+ _: &GlobalListContents,
47+ _: &Connection,
48+ _: &QueueHandle<Self>,
49+ ) {
50+ }
51+}
52+
53+impl Dispatch<wl_output::WlOutput, ()> for App {
54+ fn event(
55+ _: &mut Self,
56+ _: &wl_output::WlOutput,
57+ _: wl_output::Event,
58+ _: &(),
59+ _: &Connection,
60+ _: &QueueHandle<Self>,
61+ ) {
62+ }
63+}
64+
65+impl Dispatch<manager::ExtWorkspaceManagerV1, ()> for App {
66+ fn event(
67+ _: &mut Self,
68+ _: &manager::ExtWorkspaceManagerV1,
69+ event: manager::Event,
70+ _: &(),
71+ _: &Connection,
72+ _: &QueueHandle<Self>,
73+ ) {
74+ match event {
75+ manager::Event::WorkspaceGroup { .. } => println!("group"),
76+ manager::Event::Workspace { .. } => println!("workspace"),
77+ e => println!("manager {e:?}"),
78+ }
79+ }
80+
81+ event_created_child!(App, manager::ExtWorkspaceManagerV1, [
82+ manager::EVT_WORKSPACE_GROUP_OPCODE => (group::ExtWorkspaceGroupHandleV1, ()),
83+ manager::EVT_WORKSPACE_OPCODE => (ws::ExtWorkspaceHandleV1, ()),
84+ ]);
85+}
86+
87+impl Dispatch<group::ExtWorkspaceGroupHandleV1, ()> for App {
88+ fn event(
89+ _: &mut Self,
90+ _: &group::ExtWorkspaceGroupHandleV1,
91+ event: group::Event,
92+ _: &(),
93+ _: &Connection,
94+ _: &QueueHandle<Self>,
95+ ) {
96+ println!(" group {event:?}");
97+ }
98+}
99+
100+impl Dispatch<ws::ExtWorkspaceHandleV1, ()> for App {
101+ fn event(
102+ app: &mut Self,
103+ h: &ws::ExtWorkspaceHandleV1,
104+ event: ws::Event,
105+ _: &(),
106+ _: &Connection,
107+ _: &QueueHandle<Self>,
108+ ) {
109+ if let ws::Event::Name { name } = &event {
110+ app.names.push((h.clone(), name.clone()));
111+ }
112+ println!(" workspace {:?} {event:?}", h.id().protocol_id());
113+ }
114+}
Modified file: crates/protocols/src/lib.rs+2 additions
@@ -1 +1,3 @@
1+//! Wayland protocols monolith implements itself.
12
3+pub mod workspace;
Added file: crates/protocols/src/workspace.rs+397 additions
@@ -0,0 +1,397 @@
1+//! ext-workspace-v1. The compositor publishes a snapshot of groups and
2+//! workspaces, and this module sends each client the difference from what
3+//! it has already seen. Client requests are queued until `commit` and then
4+//! handed over as one batch.
5+
6+use smithay::output::Output;
7+use smithay::reexports::wayland_protocols::ext::workspace::v1::server::{
8+ ext_workspace_group_handle_v1::{self, ExtWorkspaceGroupHandleV1, GroupCapabilities},
9+ ext_workspace_handle_v1::{self, ExtWorkspaceHandleV1, WorkspaceCapabilities},
10+ ext_workspace_manager_v1::{self, ExtWorkspaceManagerV1},
11+};
12+use smithay::reexports::wayland_server::backend::{ClientId, GlobalId};
13+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
14+use smithay::reexports::wayland_server::{
15+ Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
16+};
17+use smithay::wayland::{Dispatch2, GlobalDispatch2};
18+use std::sync::Mutex;
19+
20+#[derive(Clone, Debug, Default, PartialEq)]
21+pub struct Snapshot {
22+ pub groups: Vec<Group>,
23+}
24+
25+#[derive(Clone, Debug, PartialEq)]
26+pub struct Group {
27+ pub output: Output,
28+ pub workspaces: Vec<Workspace>,
29+}
30+
31+#[derive(Clone, Debug, PartialEq, Eq)]
32+pub struct Workspace {
33+ /// Unique within its group, and reported to clients as the stable id.
34+ pub key: String,
35+ pub name: String,
36+ pub coordinates: Vec<u32>,
37+ pub active: bool,
38+ pub urgent: bool,
39+ pub hidden: bool,
40+}
41+
42+#[derive(Clone, Debug, PartialEq, Eq)]
43+pub enum Action {
44+ Activate { group: String, workspace: String },
45+ Deactivate { group: String, workspace: String },
46+ Create { group: String, name: String },
47+}
48+
49+pub trait WorkspaceHandler {
50+ fn workspace_state(&mut self) -> &mut WorkspaceState;
51+ fn workspace_actions(&mut self, actions: Vec<Action>);
52+}
53+
54+struct SentGroup {
55+ name: String,
56+ handle: ExtWorkspaceGroupHandleV1,
57+ outputs: Vec<WlOutput>,
58+}
59+
60+struct SentWorkspace {
61+ group: String,
62+ handle: ExtWorkspaceHandleV1,
63+ last: Workspace,
64+}
65+
66+struct Manager {
67+ handle: ExtWorkspaceManagerV1,
68+ groups: Vec<SentGroup>,
69+ workspaces: Vec<SentWorkspace>,
70+}
71+
72+pub struct WorkspaceState {
73+ global: GlobalId,
74+ managers: Vec<Manager>,
75+ snapshot: Snapshot,
76+}
77+
78+pub struct ManagerGlobal;
79+
80+#[derive(Default)]
81+pub struct ManagerData {
82+ pending: Mutex<Vec<Action>>,
83+}
84+
85+pub struct GroupData {
86+ manager: ExtWorkspaceManagerV1,
87+ group: String,
88+}
89+
90+pub struct WorkspaceData {
91+ manager: ExtWorkspaceManagerV1,
92+ group: String,
93+ key: String,
94+}
95+
96+impl WorkspaceState {
97+ pub fn new<D>(dh: &DisplayHandle) -> Self
98+ where
99+ D: GlobalDispatch<ExtWorkspaceManagerV1, ManagerGlobal> + 'static,
100+ {
101+ Self {
102+ global: dh.create_global::<D, ExtWorkspaceManagerV1, _>(1, ManagerGlobal),
103+ managers: Vec::new(),
104+ snapshot: Snapshot::default(),
105+ }
106+ }
107+
108+ pub fn global(&self) -> GlobalId {
109+ self.global.clone()
110+ }
111+
112+ pub fn has_clients(&self) -> bool {
113+ !self.managers.is_empty()
114+ }
115+
116+ /// Publishes a snapshot. Clients only hear about differences, including
117+ /// wl_outputs they bound after the manager.
118+ pub fn update<D>(&mut self, dh: &DisplayHandle, snapshot: Snapshot)
119+ where
120+ D: Dispatch<ExtWorkspaceGroupHandleV1, GroupData>
121+ + Dispatch<ExtWorkspaceHandleV1, WorkspaceData>
122+ + 'static,
123+ {
124+ self.snapshot = snapshot;
125+ self.managers.retain(|m| m.handle.is_alive());
126+ for m in &mut self.managers {
127+ sync::<D>(dh, m, &self.snapshot);
128+ }
129+ }
130+}
131+
132+fn sync<D>(dh: &DisplayHandle, m: &mut Manager, snap: &Snapshot)
133+where
134+ D: Dispatch<ExtWorkspaceGroupHandleV1, GroupData>
135+ + Dispatch<ExtWorkspaceHandleV1, WorkspaceData>
136+ + 'static,
137+{
138+ let Ok(client) = dh.get_client(m.handle.id()) else {
139+ return;
140+ };
141+ let mut changed = false;
142+
143+ let gone: Vec<String> = m
144+ .groups
145+ .iter()
146+ .filter(|g| !snap.groups.iter().any(|s| s.output.name() == g.name))
147+ .map(|g| g.name.clone())
148+ .collect();
149+ m.workspaces.retain(|w| {
150+ let in_snap = snap
151+ .groups
152+ .iter()
153+ .find(|g| g.output.name() == w.group)
154+ .is_some_and(|g| g.workspaces.iter().any(|s| s.key == w.last.key));
155+ if !in_snap {
156+ if let Some(g) = m.groups.iter().find(|g| g.name == w.group) {
157+ g.handle.workspace_leave(&w.handle);
158+ }
159+ w.handle.removed();
160+ changed = true;
161+ }
162+ in_snap
163+ });
164+ m.groups.retain(|g| {
165+ let keep = !gone.contains(&g.name);
166+ if !keep {
167+ g.handle.removed();
168+ changed = true;
169+ }
170+ keep
171+ });
172+
173+ for group in &snap.groups {
174+ let name = group.output.name();
175+ let idx = match m.groups.iter().position(|g| g.name == name) {
176+ Some(i) => i,
177+ None => {
178+ let Ok(handle) = client.create_resource::<ExtWorkspaceGroupHandleV1, _, D>(
179+ dh,
180+ m.handle.version(),
181+ GroupData {
182+ manager: m.handle.clone(),
183+ group: name.clone(),
184+ },
185+ ) else {
186+ continue;
187+ };
188+ m.handle.workspace_group(&handle);
189+ handle.capabilities(GroupCapabilities::CreateWorkspace);
190+ m.groups.push(SentGroup {
191+ name: name.clone(),
192+ handle,
193+ outputs: Vec::new(),
194+ });
195+ changed = true;
196+ m.groups.len() - 1
197+ }
198+ };
199+ let sent = &mut m.groups[idx];
200+ for wl in group.output.client_outputs(&client) {
201+ if !sent.outputs.contains(&wl) {
202+ sent.handle.output_enter(&wl);
203+ sent.outputs.push(wl);
204+ changed = true;
205+ }
206+ }
207+ sent.outputs.retain(|o| o.is_alive());
208+
209+ for ws in &group.workspaces {
210+ match m
211+ .workspaces
212+ .iter_mut()
213+ .find(|w| w.group == name && w.last.key == ws.key)
214+ {
215+ Some(w) => {
216+ if w.last != *ws {
217+ send_props(&w.handle, Some(&w.last), ws);
218+ w.last = ws.clone();
219+ changed = true;
220+ }
221+ }
222+ None => {
223+ let Ok(handle) = client.create_resource::<ExtWorkspaceHandleV1, _, D>(
224+ dh,
225+ m.handle.version(),
226+ WorkspaceData {
227+ manager: m.handle.clone(),
228+ group: name.clone(),
229+ key: ws.key.clone(),
230+ },
231+ ) else {
232+ continue;
233+ };
234+ m.handle.workspace(&handle);
235+ handle.id(ws.key.clone());
236+ handle.capabilities(
237+ WorkspaceCapabilities::Activate | WorkspaceCapabilities::Deactivate,
238+ );
239+ send_props(&handle, None, ws);
240+ m.groups[idx].handle.workspace_enter(&handle);
241+ m.workspaces.push(SentWorkspace {
242+ group: name.clone(),
243+ handle,
244+ last: ws.clone(),
245+ });
246+ changed = true;
247+ }
248+ }
249+ }
250+ }
251+ if changed {
252+ m.handle.done();
253+ }
254+}
255+
256+fn send_props(h: &ExtWorkspaceHandleV1, old: Option<&Workspace>, new: &Workspace) {
257+ if old.is_none_or(|o| o.name != new.name) {
258+ h.name(new.name.clone());
259+ }
260+ if old.is_none_or(|o| o.coordinates != new.coordinates) {
261+ let bytes = new
262+ .coordinates
263+ .iter()
264+ .flat_map(|c| c.to_ne_bytes())
265+ .collect();
266+ h.coordinates(bytes);
267+ }
268+ let state = |w: &Workspace| {
269+ let mut s = ext_workspace_handle_v1::State::empty();
270+ s.set(ext_workspace_handle_v1::State::Active, w.active);
271+ s.set(ext_workspace_handle_v1::State::Urgent, w.urgent);
272+ s.set(ext_workspace_handle_v1::State::Hidden, w.hidden);
273+ s
274+ };
275+ if old.is_none_or(|o| state(o) != state(new)) {
276+ h.state(state(new));
277+ }
278+}
279+
280+fn queue(manager: &ExtWorkspaceManagerV1, action: Action) {
281+ if let Some(data) = manager.data::<ManagerData>() {
282+ data.pending
283+ .lock()
284+ .unwrap_or_else(|e| e.into_inner())
285+ .push(action);
286+ }
287+}
288+
289+impl<D> GlobalDispatch2<ExtWorkspaceManagerV1, D> for ManagerGlobal
290+where
291+ D: WorkspaceHandler
292+ + Dispatch<ExtWorkspaceManagerV1, ManagerData>
293+ + Dispatch<ExtWorkspaceGroupHandleV1, GroupData>
294+ + Dispatch<ExtWorkspaceHandleV1, WorkspaceData>
295+ + 'static,
296+{
297+ fn bind(
298+ &self,
299+ state: &mut D,
300+ dh: &DisplayHandle,
301+ _client: &Client,
302+ resource: New<ExtWorkspaceManagerV1>,
303+ data_init: &mut DataInit<'_, D>,
304+ ) {
305+ let handle = data_init.init(resource, ManagerData::default());
306+ let ws = state.workspace_state();
307+ let mut m = Manager {
308+ handle,
309+ groups: Vec::new(),
310+ workspaces: Vec::new(),
311+ };
312+ sync::<D>(dh, &mut m, &ws.snapshot);
313+ ws.managers.push(m);
314+ }
315+}
316+
317+impl<D: WorkspaceHandler> Dispatch2<ExtWorkspaceManagerV1, D> for ManagerData {
318+ fn request(
319+ &self,
320+ state: &mut D,
321+ _client: &Client,
322+ resource: &ExtWorkspaceManagerV1,
323+ request: ext_workspace_manager_v1::Request,
324+ _dh: &DisplayHandle,
325+ _data_init: &mut DataInit<'_, D>,
326+ ) {
327+ match request {
328+ ext_workspace_manager_v1::Request::Commit => {
329+ let actions =
330+ std::mem::take(&mut *self.pending.lock().unwrap_or_else(|e| e.into_inner()));
331+ if !actions.is_empty() {
332+ state.workspace_actions(actions);
333+ }
334+ }
335+ ext_workspace_manager_v1::Request::Stop => {
336+ resource.finished();
337+ state
338+ .workspace_state()
339+ .managers
340+ .retain(|m| m.handle != *resource);
341+ }
342+ _ => {}
343+ }
344+ }
345+
346+ fn destroyed(&self, state: &mut D, _client: ClientId, resource: &ExtWorkspaceManagerV1) {
347+ state
348+ .workspace_state()
349+ .managers
350+ .retain(|m| m.handle != *resource);
351+ }
352+}
353+
354+impl<D: WorkspaceHandler> Dispatch2<ExtWorkspaceGroupHandleV1, D> for GroupData {
355+ fn request(
356+ &self,
357+ _state: &mut D,
358+ _client: &Client,
359+ _resource: &ExtWorkspaceGroupHandleV1,
360+ request: ext_workspace_group_handle_v1::Request,
361+ _dh: &DisplayHandle,
362+ _data_init: &mut DataInit<'_, D>,
363+ ) {
364+ if let ext_workspace_group_handle_v1::Request::CreateWorkspace { workspace } = request {
365+ queue(
366+ &self.manager,
367+ Action::Create {
368+ group: self.group.clone(),
369+ name: workspace,
370+ },
371+ );
372+ }
373+ }
374+}
375+
376+impl<D: WorkspaceHandler> Dispatch2<ExtWorkspaceHandleV1, D> for WorkspaceData {
377+ fn request(
378+ &self,
379+ _state: &mut D,
380+ _client: &Client,
381+ _resource: &ExtWorkspaceHandleV1,
382+ request: ext_workspace_handle_v1::Request,
383+ _dh: &DisplayHandle,
384+ _data_init: &mut DataInit<'_, D>,
385+ ) {
386+ let (group, workspace) = (self.group.clone(), self.key.clone());
387+ match request {
388+ ext_workspace_handle_v1::Request::Activate => {
389+ queue(&self.manager, Action::Activate { group, workspace });
390+ }
391+ ext_workspace_handle_v1::Request::Deactivate => {
392+ queue(&self.manager, Action::Deactivate { group, workspace });
393+ }
394+ _ => {}
395+ }
396+ }
397+}