diff --git a/docs/features.md b/docs/features.md index 15a2e344..76df0d1f 100644 --- a/docs/features.md +++ b/docs/features.md @@ -148,6 +148,7 @@ Jay supports the following wayland protocols: | ext_output_image_capture_source_manager_v1 | 1 | | | ext_session_lock_manager_v1 | 1 | Yes | | ext_transient_seat_manager_v1 | 1[^ts_rejected] | Yes | +| ext_workspace_manager_v1 | 1 | Yes | | jay_tray_v1 | 1 | | | org_kde_kwin_server_decoration_manager | 1 | | | wl_compositor | 6 | | diff --git a/release-notes.md b/release-notes.md index 1995af9d..061ab8bd 100644 --- a/release-notes.md +++ b/release-notes.md @@ -8,6 +8,7 @@ - Add an idle grace period. During the grace period, the screen goes black but is neither disabled nor locked. This is similar to how android handles going idle. The default is 5 seconds. +- Implement ext-workspace-v1. # 1.7.0 (2024-10-25) diff --git a/src/client.rs b/src/client.rs index 71fdeb5c..3fcfacda 100644 --- a/src/client.rs +++ b/src/client.rs @@ -56,6 +56,7 @@ bitflags! { CAP_SEAT_MANAGER = 1 << 8, CAP_DRM_LEASE = 1 << 9, CAP_INPUT_METHOD = 1 << 10, + CAP_WORKSPACE = 1 << 11, } pub const CAPS_DEFAULT: ClientCaps = ClientCaps(CAP_LAYER_SHELL.0 | CAP_DRM_LEASE.0); diff --git a/src/client/objects.rs b/src/client/objects.rs index 65122be3..b6ff7ea5 100644 --- a/src/client/objects.rs +++ b/src/client/objects.rs @@ -27,6 +27,7 @@ use { xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel, XdgSurface}, WlSurface, }, + workspace_manager::ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1, wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1, xdg_positioner::XdgPositioner, @@ -39,9 +40,9 @@ use { }, wire::{ ExtDataControlSourceV1Id, ExtForeignToplevelHandleV1Id, ExtImageCaptureSourceV1Id, - ExtImageCopyCaptureSessionV1Id, JayOutputId, JayScreencastId, JayToplevelId, - JayWorkspaceId, WlBufferId, WlDataSourceId, WlOutputId, WlPointerId, WlRegionId, - WlRegistryId, WlSeatId, WlSurfaceId, WpDrmLeaseConnectorV1Id, + ExtImageCopyCaptureSessionV1Id, ExtWorkspaceGroupHandleV1Id, JayOutputId, + JayScreencastId, JayToplevelId, JayWorkspaceId, WlBufferId, WlDataSourceId, WlOutputId, + WlPointerId, WlRegionId, WlRegistryId, WlSeatId, WlSurfaceId, WpDrmLeaseConnectorV1Id, WpLinuxDrmSyncobjTimelineV1Id, XdgPopupId, XdgPositionerId, XdgSurfaceId, XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwpPrimarySelectionSourceV1Id, ZwpTabletToolV2Id, @@ -82,6 +83,8 @@ pub struct Objects { pub ext_copy_sessions: CopyHashMap>, pub ext_data_sources: CopyHashMap>, + pub ext_workspace_groups: + CopyHashMap>, ids: RefCell>, } @@ -119,6 +122,7 @@ impl Objects { foreign_toplevel_handles: Default::default(), ext_copy_sessions: Default::default(), ext_data_sources: Default::default(), + ext_workspace_groups: Default::default(), ids: RefCell::new(vec![]), } } @@ -160,6 +164,7 @@ impl Objects { self.foreign_toplevel_handles.clear(); self.ext_copy_sessions.clear(); self.ext_data_sources.clear(); + self.ext_workspace_groups.clear(); } pub fn id(&self, client_data: &Client) -> Result diff --git a/src/compositor.rs b/src/compositor.rs index ea5f2a05..98a27dc6 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -23,6 +23,7 @@ use { jay_screencast::{perform_screencast_realloc, perform_toplevel_screencasts}, wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, wl_surface::{zwp_input_popup_surface_v2::input_popup_positioning, NoneSurfaceExt}, + workspace_manager::workspace_manager_done, }, io_uring::{IoUring, IoUringError}, leaks, @@ -278,6 +279,7 @@ fn start_compositor2( const_40hz_latch: Default::default(), tray_item_ids: Default::default(), data_control_device_ids: Default::default(), + workspace_managers: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -446,6 +448,10 @@ fn start_global_event_handlers( Phase::Present, handle_const_40hz_latch(state.clone()), ), + eng.spawn( + "workspace manager done", + workspace_manager_done(state.clone()), + ), ] } @@ -593,6 +599,7 @@ fn create_dummy_output(state: &Rc) { before_latch_event: Default::default(), tray_start_rel: Default::default(), tray_items: Default::default(), + ext_workspace_groups: Default::default(), }); let dummy_workspace = Rc::new(WorkspaceNode { id: state.node_ids.next(), @@ -615,6 +622,8 @@ fn create_dummy_output(state: &Rc) { title_texture: Default::default(), attention_requests: Default::default(), render_highlight: Default::default(), + ext_workspaces: Default::default(), + opt: Default::default(), }); *dummy_workspace.output_link.borrow_mut() = Some(dummy_output.workspaces.add_last(dummy_workspace.clone())); diff --git a/src/globals.rs b/src/globals.rs index 7b3a7b5b..6ebd9733 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -40,6 +40,7 @@ use { wl_shm::WlShmGlobal, wl_subcompositor::WlSubcompositorGlobal, wl_surface::xwayland_shell_v1::XwaylandShellV1Global, + workspace_manager::ext_workspace_manager_v1::ExtWorkspaceManagerV1Global, wp_alpha_modifier_v1::WpAlphaModifierV1Global, wp_commit_timing_manager_v1::WpCommitTimingManagerV1Global, wp_content_type_manager_v1::WpContentTypeManagerV1Global, @@ -213,6 +214,7 @@ impl Globals { add_singleton!(WpCommitTimingManagerV1Global); add_singleton!(ExtDataControlManagerV1Global); add_singleton!(WlFixesGlobal); + add_singleton!(ExtWorkspaceManagerV1Global); } pub fn add_backend_singletons(&self, backend: &Rc) { diff --git a/src/ifs.rs b/src/ifs.rs index 8ab0a7de..5f55bd41 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -46,6 +46,7 @@ pub mod wl_shm; pub mod wl_shm_pool; pub mod wl_subcompositor; pub mod wl_surface; +pub mod workspace_manager; pub mod wp_alpha_modifier_v1; pub mod wp_commit_timing_manager_v1; pub mod wp_content_type_manager_v1; diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index 7472df98..82c10f83 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -13,7 +13,7 @@ use { state::{ConnectorData, State}, tree::{calculate_logical_size, OutputNode, TearingMode, VrrMode}, utils::{ - cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, + cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, rc_eq::rc_eq, transform_ext::TransformExt, }, wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id}, @@ -238,6 +238,11 @@ impl WlOutputGlobal { if obj.version >= SEND_DONE_SINCE { obj.send_done(); } + for group in client.objects.ext_workspace_groups.lock().values() { + if rc_eq(&group.output, &self.opt) { + group.handle_new_output(&obj); + } + } Ok(()) } diff --git a/src/ifs/workspace_manager.rs b/src/ifs/workspace_manager.rs new file mode 100644 index 00000000..de415d42 --- /dev/null +++ b/src/ifs/workspace_manager.rs @@ -0,0 +1,64 @@ +use { + crate::{ + client::Client, + ifs::workspace_manager::{ + ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ext_workspace_manager_v1::{ + ExtWorkspaceManagerV1, WorkspaceManagerId, WorkspaceManagerIds, + }, + }, + state::State, + tree::{OutputNode, WorkspaceNode}, + utils::{copyhashmap::CopyHashMap, opt::Opt, queue::AsyncQueue}, + }, + std::rc::Rc, +}; + +pub mod ext_workspace_group_handle_v1; +pub mod ext_workspace_handle_v1; +pub mod ext_workspace_manager_v1; + +#[derive(Default)] +pub struct WorkspaceManagerState { + queue: AsyncQueue>>, + dangling_group: Rc>, + ids: WorkspaceManagerIds, + managers: CopyHashMap>, +} + +impl WorkspaceManagerState { + pub fn clear(&self) { + self.managers.clear(); + self.queue.clear(); + } + + pub fn announce_output(&self, on: &OutputNode) { + for manager in self.managers.lock().values() { + manager.announce_output(on); + } + } + + pub fn announce_workspace(&self, output: &OutputNode, ws: &WorkspaceNode) { + for manager in self.managers.lock().values() { + manager.announce_workspace(output, ws); + } + } +} + +pub async fn workspace_manager_done(state: Rc) { + loop { + let manager = state.workspace_managers.queue.pop().await; + if let Some(manager) = manager.get() { + manager.send_done(); + } + } +} + +fn group_or_dangling( + client: &Client, + group: Option<&ExtWorkspaceGroupHandleV1>, +) -> Rc> { + group + .map(|g| g.opt.clone()) + .unwrap_or_else(|| client.state.workspace_managers.dangling_group.clone()) +} diff --git a/src/ifs/workspace_manager/ext_workspace_group_handle_v1.rs b/src/ifs/workspace_manager/ext_workspace_group_handle_v1.rs new file mode 100644 index 00000000..7c10f4e8 --- /dev/null +++ b/src/ifs/workspace_manager/ext_workspace_group_handle_v1.rs @@ -0,0 +1,164 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::{ + wl_output::{OutputGlobalOpt, WlOutput}, + workspace_manager::{ + ext_workspace_handle_v1::ExtWorkspaceHandleV1, + ext_workspace_manager_v1::{ + ExtWorkspaceManagerV1, WorkspaceChange, WorkspaceManagerId, + }, + }, + }, + leaks::Tracker, + object::{Object, Version}, + utils::opt::Opt, + wire::{ext_workspace_group_handle_v1::*, ExtWorkspaceGroupHandleV1Id}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct ExtWorkspaceGroupHandleV1 { + pub(super) id: ExtWorkspaceGroupHandleV1Id, + pub(super) client: Rc, + pub(super) tracker: Tracker, + pub(super) version: Version, + pub output: Rc, + pub(super) manager_id: WorkspaceManagerId, + pub(super) manager: Rc>, + pub(super) opt: Rc>, +} + +const CAP_CREATE_WORKSPACE: u32 = 1; + +impl ExtWorkspaceGroupHandleV1 { + fn detach(&self) { + self.opt.set(None); + if let Some(node) = self.output.node() { + node.ext_workspace_groups.remove(&self.manager_id); + } + } + + pub(super) fn send_capabilities(&self) { + let capabilities = CAP_CREATE_WORKSPACE; + self.client.event(Capabilities { + self_id: self.id, + capabilities, + }); + } + + pub(super) fn send_output_enter(&self, output: &WlOutput) { + self.client.event(OutputEnter { + self_id: self.id, + output: output.id, + }); + } + + #[expect(dead_code)] + fn send_output_leave(&self, output: &WlOutput) { + self.client.event(OutputLeave { + self_id: self.id, + output: output.id, + }); + } + + pub(super) fn send_workspace_enter(&self, workspace: &ExtWorkspaceHandleV1) { + self.client.event(WorkspaceEnter { + self_id: self.id, + workspace: workspace.id, + }); + } + + pub(super) fn send_workspace_leave(&self, workspace: &ExtWorkspaceHandleV1) { + self.client.event(WorkspaceLeave { + self_id: self.id, + workspace: workspace.id, + }); + } + + fn send_removed(&self) { + self.client.event(Removed { self_id: self.id }); + } + + pub fn handle_destroyed(&self) { + self.detach(); + if let Some(manager) = self.manager.get() { + self.send_removed(); + manager.schedule_done(); + } + } + + pub fn handle_new_output(&self, output: &WlOutput) { + if let Some(manager) = self.manager.get() { + self.send_output_enter(output); + manager.schedule_done(); + } + } +} + +object_base! { + self = ExtWorkspaceGroupHandleV1; + version = self.version; +} + +impl Object for ExtWorkspaceGroupHandleV1 { + fn break_loops(&self) { + self.detach(); + } +} + +dedicated_add_obj!( + ExtWorkspaceGroupHandleV1, + ExtWorkspaceGroupHandleV1Id, + ext_workspace_groups +); + +impl ExtWorkspaceGroupHandleV1RequestHandler for ExtWorkspaceGroupHandleV1 { + type Error = ExtWorkspaceGroupHandleV1Error; + + fn create_workspace( + &self, + req: CreateWorkspace<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + if self.opt.is_none() { + return Ok(()); + } + let Some(manager) = self.manager.get() else { + return Ok(()); + }; + manager.pending.push(WorkspaceChange::CreateWorkspace( + req.workspace.to_string(), + self.output.clone(), + )); + Ok(()) + } + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + if let Some(manager) = self.manager.get() { + if let Some(node) = self.output.node() { + let mut sent_any = false; + for ws in node.workspaces.iter() { + if let Some(ws) = ws.ext_workspaces.get(&self.manager_id) { + self.send_workspace_leave(&ws); + sent_any = true; + } + } + if sent_any { + manager.schedule_done(); + } + } + } + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum ExtWorkspaceGroupHandleV1Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ExtWorkspaceGroupHandleV1Error, ClientError); diff --git a/src/ifs/workspace_manager/ext_workspace_handle_v1.rs b/src/ifs/workspace_manager/ext_workspace_handle_v1.rs new file mode 100644 index 00000000..a23d8c7b --- /dev/null +++ b/src/ifs/workspace_manager/ext_workspace_handle_v1.rs @@ -0,0 +1,215 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::workspace_manager::{ + ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ext_workspace_manager_v1::{ + ExtWorkspaceManagerV1, WorkspaceChange, WorkspaceManagerId, + }, + group_or_dangling, + }, + leaks::Tracker, + object::{Object, Version}, + tree::{OutputNode, WorkspaceNode}, + utils::{clonecell::CloneCell, opt::Opt}, + wire::{ext_workspace_handle_v1::*, ExtWorkspaceHandleV1Id}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +const STATE_ACTIVE: u32 = 1; +const STATE_URGENT: u32 = 2; +#[expect(dead_code)] +const STATE_HIDDEN: u32 = 4; + +const CAP_ACTIVATE: u32 = 1; +#[expect(dead_code)] +const CAP_DEACTIVATE: u32 = 2; +#[expect(dead_code)] +const CAP_REMOVE: u32 = 4; +const CAP_ASSIGN: u32 = 8; + +pub struct ExtWorkspaceHandleV1 { + pub(super) id: ExtWorkspaceHandleV1Id, + pub(super) client: Rc, + pub(super) tracker: Tracker, + pub version: Version, + pub(super) group: CloneCell>>, + pub(super) workspace: Rc>, + pub(super) manager_id: WorkspaceManagerId, + pub(super) manager: Rc>, + pub(super) destroyed: Cell, +} + +impl ExtWorkspaceHandleV1 { + fn detach(&self) { + if let Some(ws) = self.workspace.get() { + ws.ext_workspaces.remove(&self.manager_id); + } + } + + pub(super) fn send_id(&self, id: &str) { + self.client.event(Id { + self_id: self.id, + id, + }); + } + + pub(super) fn send_name(&self, name: &str) { + self.client.event(Name { + self_id: self.id, + name, + }); + } + + #[expect(dead_code)] + fn send_coordinates(&self, coordinates: &[u32]) { + self.client.event(Coordinates { + self_id: self.id, + coordinates, + }); + } + + pub(super) fn send_current_state(&self) { + let Some(ws) = self.workspace.get() else { + return; + }; + let mut state = 0; + let output = ws.output.get(); + if let Some(active) = output.workspace.get() { + if active.id == ws.id { + state |= STATE_ACTIVE; + } + } + if ws.attention_requests.active() { + state |= STATE_URGENT; + } + self.send_state(state); + } + + fn send_state(&self, state: u32) { + self.client.event(State { + self_id: self.id, + state, + }); + } + + pub(super) fn send_capabilities(&self) { + let capabilities = CAP_ACTIVATE | CAP_ASSIGN; + self.client.event(Capabilities { + self_id: self.id, + capabilities, + }); + } + + fn send_removed(&self) { + self.client.event(Removed { self_id: self.id }); + } + + pub fn handle_destroyed(&self) { + self.destroyed.set(true); + if let Some(manager) = self.manager.get() { + if let Some(group) = self.group.get().get() { + group.send_workspace_leave(self); + } + self.group + .set(self.client.state.workspace_managers.dangling_group.clone()); + self.send_state(0); + self.send_removed(); + manager.schedule_done(); + } + } + + pub fn handle_new_output(&self, output: &OutputNode) { + let new = output.ext_workspace_groups.get(&self.manager_id); + let new = group_or_dangling(&self.client, new.as_deref()); + let old = self.group.set(new.clone()); + if let Some(manager) = self.manager.get() { + if let Some(old) = old.get() { + old.send_workspace_leave(self); + } + if let Some(new) = new.get() { + new.send_workspace_enter(self); + } + manager.schedule_done(); + } + } + + pub fn handle_visibility_changed(&self) { + if let Some(manager) = self.manager.get() { + self.send_current_state(); + manager.schedule_done(); + } + } + + pub fn handle_urgent_changed(&self) { + self.handle_visibility_changed(); + } +} + +object_base! { + self = ExtWorkspaceHandleV1; + version = self.version; +} + +impl Object for ExtWorkspaceHandleV1 { + fn break_loops(&self) { + self.detach(); + } +} + +simple_add_obj!(ExtWorkspaceHandleV1); + +impl ExtWorkspaceHandleV1RequestHandler for ExtWorkspaceHandleV1 { + type Error = ExtWorkspaceHandleV1Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } + + fn activate(&self, _req: Activate, _slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + let Some(manager) = self.manager.get() else { + return Ok(()); + }; + manager + .pending + .push(WorkspaceChange::ActivateWorkspace(self.workspace.clone())); + Ok(()) + } + + fn deactivate(&self, _req: Deactivate, _slf: &Rc) -> Result<(), Self::Error> { + Ok(()) + } + + fn assign(&self, req: Assign, _slf: &Rc) -> Result<(), Self::Error> { + if self.destroyed.get() { + return Ok(()); + } + let group = self.client.lookup(req.workspace_group)?; + let Some(manager) = self.manager.get() else { + return Ok(()); + }; + manager.pending.push(WorkspaceChange::AssignWorkspace( + self.workspace.clone(), + group.output.clone(), + )); + Ok(()) + } + + fn remove(&self, _req: Remove, _slf: &Rc) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum ExtWorkspaceHandleV1Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ExtWorkspaceHandleV1Error, ClientError); diff --git a/src/ifs/workspace_manager/ext_workspace_manager_v1.rs b/src/ifs/workspace_manager/ext_workspace_manager_v1.rs new file mode 100644 index 00000000..6ee14716 --- /dev/null +++ b/src/ifs/workspace_manager/ext_workspace_manager_v1.rs @@ -0,0 +1,306 @@ +use { + crate::{ + client::{Client, ClientCaps, ClientError, CAP_WORKSPACE}, + globals::{Global, GlobalName}, + ifs::{ + wl_output::OutputGlobalOpt, + workspace_manager::{ + ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ext_workspace_handle_v1::ExtWorkspaceHandleV1, group_or_dangling, + }, + }, + leaks::Tracker, + object::{Object, Version}, + tree::{move_ws_to_output, OutputNode, WorkspaceNode, WsMoveConfig}, + utils::{clonecell::CloneCell, opt::Opt, syncqueue::SyncQueue}, + wire::{ext_workspace_manager_v1::*, ExtWorkspaceManagerV1Id}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +linear_ids!(WorkspaceManagerIds, WorkspaceManagerId, u64); + +pub struct ExtWorkspaceManagerV1Global { + pub name: GlobalName, +} + +pub struct ExtWorkspaceManagerV1 { + id: ExtWorkspaceManagerV1Id, + pub(super) manager_id: WorkspaceManagerId, + client: Rc, + tracker: Tracker, + version: Version, + pub(super) pending: SyncQueue, + opt: Rc>, + done_scheduled: Cell, +} + +pub(super) enum WorkspaceChange { + CreateWorkspace(String, Rc), + ActivateWorkspace(Rc>), + AssignWorkspace(Rc>, Rc), +} + +impl ExtWorkspaceManagerV1Global { + pub fn new(name: GlobalName) -> Self { + Self { name } + } + + fn bind_( + self: Rc, + id: ExtWorkspaceManagerV1Id, + client: &Rc, + version: Version, + ) -> Result<(), ExtWorkspaceManagerV1Error> { + let obj = Rc::new(ExtWorkspaceManagerV1 { + id, + manager_id: client.state.workspace_managers.ids.next(), + client: client.clone(), + tracker: Default::default(), + version, + pending: Default::default(), + opt: Default::default(), + done_scheduled: Cell::new(false), + }); + track!(client, obj); + client.add_client_obj(&obj)?; + obj.opt.set(Some(obj.clone())); + client + .state + .workspace_managers + .managers + .set(obj.manager_id, obj.clone()); + let dummy_output = client.state.dummy_output.get().unwrap(); + for ws in dummy_output.workspaces.iter() { + if !ws.is_dummy { + obj.announce_workspace(&dummy_output, &ws); + } + } + for output in client.state.root.outputs.lock().values() { + obj.announce_output(output); + } + Ok(()) + } +} + +impl ExtWorkspaceManagerV1 { + pub(super) fn announce_output(&self, node: &OutputNode) { + let id = match self.client.new_id() { + Ok(id) => id, + Err(e) => { + self.client.error(e); + return; + } + }; + let group = Rc::new(ExtWorkspaceGroupHandleV1 { + id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + output: node.global.opt.clone(), + manager_id: self.manager_id, + manager: self.opt.clone(), + opt: Default::default(), + }); + track!(self.client, group); + self.client.add_server_obj(&group); + group.opt.set(Some(group.clone())); + node.ext_workspace_groups + .set(self.manager_id, group.clone()); + self.send_workspace_group(&group); + group.send_capabilities(); + if let Some(bindings) = node.global.bindings.borrow().get(&self.client.id) { + for wl_output in bindings.values() { + group.send_output_enter(wl_output); + } + } + for ws in node.workspaces.iter() { + if let Some(ws) = ws.ext_workspaces.get(&self.manager_id) { + ws.handle_new_output(node); + } else { + self.announce_workspace(node, &ws); + } + } + self.schedule_done(); + } + + pub(super) fn announce_workspace(&self, output: &OutputNode, workspace: &WorkspaceNode) { + let id = match self.client.new_id() { + Ok(id) => id, + Err(e) => { + self.client.error(e); + return; + } + }; + let group = output.ext_workspace_groups.get(&self.manager_id); + let ws = Rc::new(ExtWorkspaceHandleV1 { + id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + group: CloneCell::new(group_or_dangling(&self.client, group.as_deref())), + workspace: workspace.opt.clone(), + manager_id: self.manager_id, + manager: self.opt.clone(), + destroyed: Cell::new(false), + }); + track!(self.client, ws); + self.client.add_server_obj(&ws); + workspace.ext_workspaces.set(self.manager_id, ws.clone()); + self.send_workspace(&ws); + ws.send_capabilities(); + ws.send_id(&workspace.name); + ws.send_name(&workspace.name); + ws.send_current_state(); + if let Some(group) = group { + group.send_workspace_enter(&ws); + } + self.schedule_done(); + } + + fn send_workspace_group(&self, workspace_group: &ExtWorkspaceGroupHandleV1) { + self.client.event(WorkspaceGroup { + self_id: self.id, + workspace_group: workspace_group.id, + }); + } + + fn send_workspace(&self, workspace: &ExtWorkspaceHandleV1) { + self.client.event(Workspace { + self_id: self.id, + workspace: workspace.id, + }); + } + + pub(super) fn send_done(&self) { + self.done_scheduled.set(false); + self.client.event(Done { self_id: self.id }); + } + + fn send_finished(&self) { + self.client.event(Finished { self_id: self.id }); + } + + fn detach(&self) { + self.opt.set(None); + self.pending.clear(); + self.client + .state + .workspace_managers + .managers + .remove(&self.manager_id); + } + + pub(super) fn schedule_done(&self) { + if self.done_scheduled.replace(true) { + return; + } + self.client + .state + .workspace_managers + .queue + .push(self.opt.clone()); + } +} + +global_base!( + ExtWorkspaceManagerV1Global, + ExtWorkspaceManagerV1, + ExtWorkspaceManagerV1Error +); + +impl Global for ExtWorkspaceManagerV1Global { + fn singleton(&self) -> bool { + true + } + + fn version(&self) -> u32 { + 1 + } + + fn required_caps(&self) -> ClientCaps { + CAP_WORKSPACE + } +} + +simple_add_global!(ExtWorkspaceManagerV1Global); + +object_base! { + self = ExtWorkspaceManagerV1; + version = self.version; +} + +impl Object for ExtWorkspaceManagerV1 { + fn break_loops(&self) { + self.detach(); + } +} + +simple_add_obj!(ExtWorkspaceManagerV1); + +impl ExtWorkspaceManagerV1RequestHandler for ExtWorkspaceManagerV1 { + type Error = ExtWorkspaceManagerV1Error; + + fn commit(&self, _req: Commit, _slf: &Rc) -> Result<(), Self::Error> { + while let Some(change) = self.pending.pop() { + match change { + WorkspaceChange::ActivateWorkspace(w) => { + let Some(ws) = w.get() else { + continue; + }; + let output = ws.output.get(); + output.show_workspace(&ws); + ws.flush_jay_workspaces(); + output.schedule_update_render_data(); + self.client.state.tree_changed(); + } + WorkspaceChange::AssignWorkspace(w, o) => { + let Some(ws) = w.get() else { + continue; + }; + let Some(o) = o.node() else { + continue; + }; + let link = match &*ws.output_link.borrow() { + None => continue, + Some(l) => l.to_ref(), + }; + let config = WsMoveConfig { + make_visible_always: false, + make_visible_if_empty: true, + source_is_destroyed: false, + before: None, + }; + move_ws_to_output(&link, &o, config); + ws.desired_output.set(o.global.output_id.clone()); + self.client.state.tree_changed(); + } + WorkspaceChange::CreateWorkspace(name, output) => { + if self.client.state.workspaces.contains(&name) { + return Ok(()); + } + let Some(output) = output.node() else { + return Ok(()); + }; + output.create_workspace(&name); + } + } + } + Ok(()) + } + + fn stop(&self, _req: Stop, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.send_finished(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum ExtWorkspaceManagerV1Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ExtWorkspaceManagerV1Error, ClientError); diff --git a/src/state.rs b/src/state.rs index 273a4a1d..c28d2470 100644 --- a/src/state.rs +++ b/src/state.rs @@ -57,6 +57,7 @@ use { zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, NoneSurfaceExt, }, + workspace_manager::WorkspaceManagerState, wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1, wp_drm_lease_device_v1::WpDrmLeaseDeviceV1Global, wp_linux_drm_syncobj_manager_v1::WpLinuxDrmSyncobjManagerV1Global, @@ -229,6 +230,7 @@ pub struct State { pub const_40hz_latch: EventSource, pub tray_item_ids: TrayItemIds, pub data_control_device_ids: DataControlDeviceIds, + pub workspace_managers: WorkspaceManagerState, } // impl Drop for State { @@ -917,6 +919,7 @@ impl State { self.ei_clients.clear(); self.slow_ei_clients.clear(); self.toplevels.clear(); + self.workspace_managers.clear(); } pub fn damage_hardware_cursors(&self, render: bool) { diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 9348de66..747bbcbd 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -197,6 +197,7 @@ impl ConnectorHandler { before_latch_event: Default::default(), tray_start_rel: Default::default(), tray_items: Default::default(), + ext_workspace_groups: Default::default(), }); on.update_visible(); on.update_rects(); @@ -259,6 +260,7 @@ impl ConnectorHandler { self.state.add_global(&tray); self.state.tree_changed(); on.update_presentation_type(); + self.state.workspace_managers.announce_output(&on); 'outer: loop { while let Some(event) = self.data.connector.event() { match event { @@ -331,6 +333,9 @@ impl ConnectorHandler { }; move_ws_to_output(&ws, &target, config); } + for group in on.ext_workspace_groups.lock().drain_values() { + group.handle_destroyed(); + } for seat in self.state.globals.seats.lock().values() { seat.cursor_group().output_disconnected(&on, &target); } diff --git a/src/tree/output.rs b/src/tree/output.rs index 4e45e820..5c4630e7 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -23,6 +23,10 @@ use { zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1}, SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor, }, + workspace_manager::{ + ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, + ext_workspace_manager_v1::WorkspaceManagerId, + }, wp_content_type_v1::ContentType, zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP}, zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, @@ -97,6 +101,7 @@ pub struct OutputNode { pub before_latch_event: EventSource, pub tray_start_rel: Cell, pub tray_items: LinkedList>, + pub ext_workspace_groups: CopyHashMap>, } #[derive(Copy, Clone, Debug, PartialEq)] @@ -414,6 +419,7 @@ impl OutputNode { self.screencasts.clear(); self.screencopies.clear(); self.ext_copy_sessions.clear(); + self.ext_workspace_groups.clear(); } pub fn on_spaces_changed(self: &Rc) { @@ -635,6 +641,9 @@ impl OutputNode { jw.send_destroyed(); jw.workspace.set(None); } + for wh in old.ext_workspaces.lock().values() { + wh.handle_destroyed(); + } old.clear(); self.state.workspaces.remove(&old.name); } else { @@ -678,7 +687,10 @@ impl OutputNode { title_texture: Default::default(), attention_requests: Default::default(), render_highlight: Default::default(), + ext_workspaces: Default::default(), + opt: Default::default(), }); + ws.opt.set(Some(ws.clone())); ws.update_has_captures(); *ws.output_link.borrow_mut() = Some(self.workspaces.add_last(ws.clone())); self.state.workspaces.set(name.to_string(), ws.clone()); @@ -694,6 +706,7 @@ impl OutputNode { for (client, e) in clients_to_kill.values() { client.error(e); } + self.state.workspace_managers.announce_workspace(self, &ws); self.schedule_update_render_data(); ws } diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 99bbb537..66eaaef7 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -10,6 +10,10 @@ use { wl_surface::{ x_surface::xwindow::Xwindow, xdg_surface::xdg_toplevel::XdgToplevel, WlSurface, }, + workspace_manager::{ + ext_workspace_handle_v1::ExtWorkspaceHandleV1, + ext_workspace_manager_v1::WorkspaceManagerId, + }, }, rect::Rect, renderer::Renderer, @@ -25,6 +29,7 @@ use { copyhashmap::CopyHashMap, linkedlist::{LinkedList, LinkedNode, NodeRef}, numcell::NumCell, + opt::Opt, threshold_counter::ThresholdCounter, }, wire::JayWorkspaceId, @@ -60,6 +65,8 @@ pub struct WorkspaceNode { pub title_texture: RefCell>, pub attention_requests: ThresholdCounter, pub render_highlight: NumCell, + pub ext_workspaces: CopyHashMap>, + pub opt: Rc>, } impl WorkspaceNode { @@ -68,6 +75,8 @@ impl WorkspaceNode { *self.output_link.borrow_mut() = None; self.fullscreen.set(None); self.jay_workspaces.clear(); + self.ext_workspaces.clear(); + self.opt.set(None); } pub fn update_has_captures(&self) { @@ -95,6 +104,9 @@ impl WorkspaceNode { pub fn set_output(&self, output: &Rc) { self.output.set(output.clone()); + for wh in self.ext_workspaces.lock().values() { + wh.handle_new_output(output); + } for jw in self.jay_workspaces.lock().values() { jw.send_output(output); } @@ -171,6 +183,9 @@ impl WorkspaceNode { for jw in self.jay_workspaces.lock().values() { jw.send_visible(visible); } + for wh in self.ext_workspaces.lock().values() { + wh.handle_visibility_changed(); + } for stacked in self.stacked.iter() { stacked.stacked_prepare_set_visible(); } @@ -236,6 +251,9 @@ impl WorkspaceNode { fn mod_attention_requested(&self, set: bool) { let crossed_threshold = self.attention_requests.adj(set); if crossed_threshold { + for wh in self.ext_workspaces.lock().values() { + wh.handle_urgent_changed(); + } self.output.get().schedule_update_render_data(); } } diff --git a/src/utils.rs b/src/utils.rs index ad8e207d..eb098c6e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -35,6 +35,7 @@ pub mod on_drop_event; pub mod once; pub mod opaque; pub mod opaque_cell; +pub mod opt; pub mod option_ext; pub mod oserror; pub mod page_size; diff --git a/src/utils/opt.rs b/src/utils/opt.rs new file mode 100644 index 00000000..86b4387e --- /dev/null +++ b/src/utils/opt.rs @@ -0,0 +1,27 @@ +use {crate::utils::clonecell::CloneCell, std::rc::Rc}; + +pub struct Opt { + t: CloneCell>>, +} + +impl Default for Opt { + fn default() -> Self { + Self { + t: Default::default(), + } + } +} + +impl Opt { + pub fn set(&self, t: Option>) { + self.t.set(t); + } + + pub fn get(&self) -> Option> { + self.t.get() + } + + pub fn is_none(&self) -> bool { + self.t.is_none() + } +} diff --git a/src/utils/syncqueue.rs b/src/utils/syncqueue.rs index 4ab3f24a..4a32c0f2 100644 --- a/src/utils/syncqueue.rs +++ b/src/utils/syncqueue.rs @@ -53,4 +53,10 @@ impl SyncQueue { self.swap(&mut res); res } + + pub fn clear(&self) { + unsafe { + self.el.get().deref_mut().clear(); + } + } } diff --git a/wire/ext_workspace_group_handle_v1.txt b/wire/ext_workspace_group_handle_v1.txt new file mode 100644 index 00000000..64c5b5d1 --- /dev/null +++ b/wire/ext_workspace_group_handle_v1.txt @@ -0,0 +1,31 @@ +event capabilities { + capabilities: u32, +} + +event output_enter { + output: id(wl_output), +} + +event output_leave { + output: id(wl_output), +} + +event workspace_enter { + workspace: id(ext_workspace_handle_v1), +} + +event workspace_leave { + workspace: id(ext_workspace_handle_v1), +} + +event removed { + +} + +request create_workspace { + workspace: str, +} + +request destroy { + +} diff --git a/wire/ext_workspace_handle_v1.txt b/wire/ext_workspace_handle_v1.txt new file mode 100644 index 00000000..9e5267a5 --- /dev/null +++ b/wire/ext_workspace_handle_v1.txt @@ -0,0 +1,43 @@ +event id { + id: str, +} + +event name { + name: str, +} + +event coordinates { + coordinates: array(u32), +} + +event state { + state: u32, +} + +event capabilities { + capabilities: u32, +} + +event removed { + +} + +request destroy { + +} + +request activate { + +} + +request deactivate { + +} + +request assign { + workspace_group: id(ext_workspace_group_handle_v1), +} + +request remove { + +} diff --git a/wire/ext_workspace_manager_v1.txt b/wire/ext_workspace_manager_v1.txt new file mode 100644 index 00000000..16a37803 --- /dev/null +++ b/wire/ext_workspace_manager_v1.txt @@ -0,0 +1,23 @@ +event workspace_group { + workspace_group: id(ext_workspace_group_handle_v1), +} + +event workspace { + workspace: id(ext_workspace_handle_v1), +} + +request commit { + +} + +event done { + +} + +event finished { + +} + +request stop { + +}