From 96e9bf3b81718e2a9ac28b2c24bd1149400b286a Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 31 Jan 2025 14:13:33 -0800 Subject: [PATCH] Initial support for workspace pinning and moving Adds support for cosmic-workspace-v2 pin, unpin, move_after, and move_before requests. Both features need some work with workspaces span displays mode, so that will need more fixes later. We also want to generate a unique id for pinned workspaces to send in the ext-workspace-v1 protocol. But that isn't a strict requirement for anything. So I haven't yet fully implemented that. We'll also want to persist other things, like workspace naming when that's added. Overall, though, with separate workspaces per display, this is working pretty well. --- Cargo.lock | 123 ++++++++++++-- Cargo.toml | 8 +- cosmic-comp-config/Cargo.toml | 1 + cosmic-comp-config/src/lib.rs | 3 + cosmic-comp-config/src/output.rs | 27 +++ cosmic-comp-config/src/workspace.rs | 15 ++ src/config/mod.rs | 24 +-- src/shell/mod.rs | 168 +++++++++++++++++-- src/shell/workspace.rs | 114 +++++++++---- src/wayland/handlers/workspace.rs | 51 +++++- src/wayland/handlers/xdg_activation.rs | 10 +- src/wayland/protocols/workspace/cosmic_v2.rs | 119 ++++++++++++- src/wayland/protocols/workspace/ext.rs | 19 ++- src/wayland/protocols/workspace/mod.rs | 58 ++++--- 14 files changed, 622 insertions(+), 118 deletions(-) create mode 100644 cosmic-comp-config/src/output.rs diff --git a/Cargo.lock b/Cargo.lock index 649d0941..7dc17334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,10 +40,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -798,7 +798,7 @@ dependencies = [ [[package]] name = "cosmic-client-toolkit" version = "0.1.0" -source = "git+https://github.com/pop-os//cosmic-protocols?rev=67df697#67df697105486fa4c9dd6ce00889c8b0526c9bb4" +source = "git+https://github.com/pop-os//cosmic-protocols?branch=main#bc4af9183e0967802d7fbe91ba811a29ca6a3b67" dependencies = [ "bitflags 2.8.0", "cosmic-protocols", @@ -840,7 +840,7 @@ dependencies = [ "ordered-float", "png", "profiling", - "rand", + "rand 0.9.0", "regex", "reis", "ron", @@ -874,6 +874,7 @@ version = "0.1.0" dependencies = [ "cosmic-config", "input", + "libdisplay-info", "serde", ] @@ -921,7 +922,7 @@ dependencies = [ [[package]] name = "cosmic-protocols" version = "0.1.0" -source = "git+https://github.com/pop-os//cosmic-protocols?rev=67df697#67df697105486fa4c9dd6ce00889c8b0526c9bb4" +source = "git+https://github.com/pop-os//cosmic-protocols?branch=main#bc4af9183e0967802d7fbe91ba811a29ca6a3b67" dependencies = [ "bitflags 2.8.0", "wayland-backend", @@ -1907,7 +1908,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -3248,7 +3261,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -3872,7 +3885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -4010,7 +4023,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -4112,6 +4125,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -4119,8 +4138,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.24", ] [[package]] @@ -4130,7 +4160,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -4139,7 +4179,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", ] [[package]] @@ -4209,7 +4258,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] @@ -4714,7 +4763,7 @@ dependencies = [ "pixman", "pkg-config", "profiling", - "rand", + "rand 0.8.5", "rustix", "scopeguard", "smallvec", @@ -4999,7 +5048,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "windows-sys 0.52.0", @@ -5582,6 +5631,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -6356,6 +6414,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -6548,7 +6615,7 @@ dependencies = [ "hex", "nix 0.29.0", "ordered-stream", - "rand", + "rand 0.8.5", "serde", "serde_repr", "sha1", @@ -6599,7 +6666,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", ] [[package]] @@ -6613,6 +6689,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 0a73f9f7..53154e9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ anyhow = {version = "1.0.51", features = ["backtrace"]} bitflags = "2.4" bytemuck = "1.12" calloop = {version = "0.14.1", features = ["executor"]} -cosmic-comp-config = {path = "cosmic-comp-config"} +cosmic-comp-config = {path = "cosmic-comp-config", features = ["libdisplay-info"]} cosmic-config = {git = "https://github.com/pop-os/libcosmic/", features = ["calloop", "macro"]} cosmic-protocols = {git = "https://github.com/pop-os/cosmic-protocols", rev = "e706814", default-features = false, features = ["server"]} cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon" } @@ -59,7 +59,7 @@ zbus = "4.4.0" profiling = { version = "1.0" } rustix = { version = "0.38.32", features = ["process"] } smallvec = "1.13.2" -rand = "0.8.5" +rand = "0.9.0" reis = { version = "0.4", features = ["calloop"] } # CLI arguments clap_lex = "0.7" @@ -119,8 +119,8 @@ inherits = "release" lto = "fat" [patch."https://github.com/pop-os/cosmic-protocols"] -cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", rev = "67df697" } -cosmic-client-toolkit = { git = "https://github.com/pop-os//cosmic-protocols", rev = "67df697" } +cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" } +cosmic-client-toolkit = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" } [patch."https://github.com/smithay/smithay"] smithay = { git = "https://github.com/smithay/smithay//", rev = "ce61c9b" } diff --git a/cosmic-comp-config/Cargo.toml b/cosmic-comp-config/Cargo.toml index 835adc86..426e6250 100644 --- a/cosmic-comp-config/Cargo.toml +++ b/cosmic-comp-config/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" [dependencies] cosmic-config = { git = "https://github.com/pop-os/libcosmic/" } input = "0.9.0" +libdisplay-info = { version = "0.2.0", optional = true } serde = { version = "1", features = ["derive"] } diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index 9db89fe2..04638076 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; pub mod input; +pub mod output; pub mod workspace; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -25,6 +26,7 @@ pub enum NumlockState { #[version = 1] pub struct CosmicCompConfig { pub workspaces: workspace::WorkspaceConfig, + pub pinned_workspaces: Vec, pub input_default: input::InputConfig, pub input_touchpad: input::InputConfig, pub input_devices: HashMap, @@ -57,6 +59,7 @@ impl Default for CosmicCompConfig { fn default() -> Self { Self { workspaces: Default::default(), + pinned_workspaces: Vec::new(), input_default: Default::default(), // By default, enable tap-to-click and disable-while-typing. input_touchpad: input::InputConfig { diff --git a/cosmic-comp-config/src/output.rs b/cosmic-comp-config/src/output.rs new file mode 100644 index 00000000..fe131563 --- /dev/null +++ b/cosmic-comp-config/src/output.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EdidProduct { + pub manufacturer: [char; 3], + pub product: u16, + pub serial: Option, + pub manufacture_week: i32, + pub manufacture_year: i32, + pub model_year: Option, +} + +#[cfg(feature = "libdisplay-info")] +impl From for EdidProduct { + fn from(vp: libdisplay_info::edid::VendorProduct) -> Self { + Self { + manufacturer: vp.manufacturer, + product: vp.product, + serial: vp.serial, + manufacture_week: vp.manufacture_week, + manufacture_year: vp.manufacture_year, + model_year: vp.model_year, + } + } +} diff --git a/cosmic-comp-config/src/workspace.rs b/cosmic-comp-config/src/workspace.rs index d72ed1c9..0e8288c6 100644 --- a/cosmic-comp-config/src/workspace.rs +++ b/cosmic-comp-config/src/workspace.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; +use crate::output::EdidProduct; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WorkspaceConfig { pub workspace_mode: WorkspaceMode, @@ -30,3 +32,16 @@ pub enum WorkspaceLayout { Vertical, Horizontal, } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OutputMatch { + pub name: String, + pub edid: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PinnedWorkspace { + pub output: OutputMatch, + pub tiling_enabled: bool, + // TODO: name, id +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 61dc42ac..e7ddf409 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -43,6 +43,7 @@ pub use key_bindings::{Action, PrivateAction}; mod types; pub use self::types::*; use cosmic::config::CosmicTk; +pub use cosmic_comp_config::output::EdidProduct; use cosmic_comp_config::{ input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, XkbConfig, XwaylandDescaling, XwaylandEavesdropping, ZoomConfig, @@ -94,29 +95,6 @@ impl From for OutputInfo { } } -#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct EdidProduct { - pub manufacturer: [char; 3], - pub product: u16, - pub serial: Option, - pub manufacture_week: i32, - pub manufacture_year: i32, - pub model_year: Option, -} - -impl From for EdidProduct { - fn from(vp: libdisplay_info::edid::VendorProduct) -> Self { - Self { - manufacturer: vp.manufacturer, - product: vp.product, - serial: vp.serial, - manufacture_week: vp.manufacture_week, - manufacture_year: vp.manufacture_year, - model_year: vp.model_year, - } - } -} - #[derive(Default, Debug, Deserialize, Serialize)] pub struct NumlockStateConfig { pub last_state: bool, diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 56ba101f..d59c77ac 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -6,15 +6,20 @@ use layout::TilingExceptions; use std::{ collections::HashMap, sync::{atomic::Ordering, Mutex}, + thread, time::{Duration, Instant}, }; use wayland_backend::server::ClientId; -use crate::wayland::{handlers::data_device, protocols::workspace::WorkspaceCapabilities}; +use crate::wayland::{ + handlers::data_device, + protocols::workspace::{State as WState, WorkspaceCapabilities}, +}; use cosmic_comp_config::{ - workspace::{WorkspaceLayout, WorkspaceMode}, + workspace::{PinnedWorkspace, WorkspaceLayout, WorkspaceMode}, TileBehavior, ZoomConfig, ZoomMovement, }; +use cosmic_config::ConfigSet; use cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::TilingState; use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection, ResizeDirection}; use cosmic_settings_config::{shortcuts, window_rules::ApplicationException}; @@ -38,10 +43,7 @@ use smithay::{ }, output::{Output, WeakOutput}, reexports::{ - wayland_protocols::ext::{ - session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1, - workspace::v1::server::ext_workspace_handle_v1::State as WState, - }, + wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1, wayland_server::{protocol::wl_surface::WlSurface, Client}, }, utils::{IsAlive, Logical, Point, Rectangle, Serial, Size}, @@ -54,6 +56,7 @@ use smithay::{ }, xwayland::X11Surface, }; +use tracing::error; use crate::{ backend::render::animations::spring::{Spring, SpringParams}, @@ -368,11 +371,48 @@ fn create_workspace( } state.set_workspace_capabilities( &workspace_handle, - WorkspaceCapabilities::Activate | WorkspaceCapabilities::SetTilingState, + WorkspaceCapabilities::Activate + | WorkspaceCapabilities::SetTilingState + | WorkspaceCapabilities::Pin + | WorkspaceCapabilities::Move, ); Workspace::new(workspace_handle, output.clone(), tiling, theme.clone()) } +fn create_workspace_from_pinned( + pinned: &PinnedWorkspace, + state: &mut WorkspaceUpdateGuard<'_, State>, + output: &Output, + group_handle: &WorkspaceGroupHandle, + active: bool, + theme: cosmic::Theme, +) -> Workspace { + let workspace_handle = state + .create_workspace( + &group_handle, + if pinned.tiling_enabled { + TilingState::TilingEnabled + } else { + TilingState::FloatingOnly + }, + // TODO Set id for persistent workspaces + None, + ) + .unwrap(); + state.add_workspace_state(&workspace_handle, WState::Pinned); + if active { + state.add_workspace_state(&workspace_handle, WState::Active); + } + state.set_workspace_capabilities( + &workspace_handle, + WorkspaceCapabilities::Activate + | WorkspaceCapabilities::SetTilingState + | WorkspaceCapabilities::Pin + | WorkspaceCapabilities::Move, + ); + Workspace::from_pinned(pinned, workspace_handle, output.clone(), theme.clone()) +} + /* We will probably need this again at some point fn merge_workspaces( mut workspace: Workspace, @@ -545,7 +585,11 @@ impl WorkspaceSet { xdg_activation_state: &XdgActivationState, ) { // add empty at the end, if necessary - if self.workspaces.last().map_or(true, |last| !last.is_empty()) { + if self + .workspaces + .last() + .map_or(true, |last| !last.is_empty() || last.pinned) + { self.add_empty_workspace(state); } @@ -556,8 +600,11 @@ impl WorkspaceSet { .iter() .enumerate() .map(|(i, workspace)| { - let previous_is_empty = - i > 0 && self.workspaces.get(i - 1).map_or(false, |w| w.is_empty()); + let previous_is_empty = i > 0 + && self + .workspaces + .get(i - 1) + .map_or(false, |w| w.is_empty() && !w.pinned); let keep = if workspace.can_auto_remove(xdg_activation_state) { // Keep empty workspace if it's active, or it's the last workspace, // and the previous worspace is not both active and empty. @@ -650,6 +697,8 @@ pub struct Workspaces { autotile: bool, autotile_behavior: TileBehavior, theme: cosmic::Theme, + // Persisted workspace to add on first `output_add` + persisted_workspaces: Vec, } impl Workspaces { @@ -662,6 +711,7 @@ impl Workspaces { autotile: config.cosmic_conf.autotile, autotile_behavior: config.cosmic_conf.autotile_behavior, theme, + persisted_workspaces: config.cosmic_conf.pinned_workspaces.clone(), } } @@ -686,6 +736,20 @@ impl Workspaces { }); workspace_state.add_group_output(&set.group, &output); + // If this is the first output added, create workspaces for pinned workspaces from config + for pinned in std::mem::take(&mut self.persisted_workspaces) { + tracing::error!("pinned workspace: {:?}", pinned); + let workspace = create_workspace_from_pinned( + &pinned, + workspace_state, + output, + &set.group, + false, + self.theme.clone(), + ); + set.workspaces.push(workspace); + } + // Remove workspaces that prefer this output from other sets let mut moved_workspaces = self .sets @@ -836,6 +900,73 @@ impl Workspaces { } } + // Move a workspace before/after a different workspace + pub fn move_workspace( + &mut self, + handle: &WorkspaceHandle, + other_handle: &WorkspaceHandle, + workspace_state: &mut WorkspaceUpdateGuard<'_, State>, + after: bool, + ) { + if handle == other_handle { + return; + } + + let (Some(old_output), Some(new_output)) = ( + self.space_for_handle(handle).map(|w| w.output.clone()), + self.space_for_handle(other_handle) + .map(|w| w.output.clone()), + ) else { + return; + }; + + // Check which workspace is active on the new set; before removing from the + // old set in cause we're moving an active workspace within the same set. + let new_set = &mut self.sets[&new_output]; + let previous_active_handle = new_set.workspaces[new_set.active].handle; + + // Remove workspace from old set + let old_set = &mut self.sets[&old_output]; + let mut workspace = if new_output != old_output { + old_set.remove_workspace(workspace_state, handle).unwrap() + } else { + // If set is the same, just remove it here without adding empty workspace, + // updating `active`, etc. + let idx = old_set + .workspaces + .iter() + .position(|w| w.handle == *handle) + .unwrap(); + old_set.workspaces.remove(idx) + }; + + let new_set = &mut self.sets[&new_output]; + + if new_output != old_output { + workspace_state.remove_workspace_state(&workspace.handle, WState::Active); + workspace_state.move_workspace_to_group(new_set.group, workspace.handle); + workspace.set_output(&new_output, true); + workspace.refresh(); + } + + // Insert workspace into new set, relative to `other_handle` + let idx = new_set + .workspaces + .iter() + .position(|w| w.handle == *other_handle) + .unwrap(); + let insert_idx = if after { idx + 1 } else { idx }; + new_set.workspaces.insert(insert_idx, workspace); + + new_set.active = new_set + .workspaces + .iter() + .position(|w| w.handle == previous_active_handle) + .unwrap(); + + new_set.update_workspace_idxs(workspace_state); + } + pub fn update_config( &mut self, config: &Config, @@ -937,7 +1068,7 @@ impl Workspaces { .sets .values() .flat_map(|set| set.workspaces.last()) - .any(|w| w.mapped().next().is_some()) + .any(|w| !w.is_empty() || w.pinned) { for set in self.sets.values_mut() { set.add_empty_workspace(workspace_state); @@ -1156,6 +1287,21 @@ impl Workspaces { self.autotile = autotile; self.apply_tile_change(guard, seats); } + + pub fn persist(&self, config: &Config) { + let pinned_workspaces: Vec = self + .sets + .values() + .flat_map(|set| &set.workspaces) + .flat_map(|w| w.to_pinned()) + .collect(); + let config = config.cosmic_helper.clone(); + thread::spawn(move || { + if let Err(err) = config.set("pinned_workspaces", pinned_workspaces) { + error!(?err, "Failed to update pinned_workspaces key"); + } + }); + } } #[derive(Debug)] diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index f69ee73b..96526d0c 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -4,7 +4,6 @@ use crate::{ element::{AsGlowRenderer, FromGlesError}, BackdropShader, }, - config::EdidProduct, shell::{ layout::{floating::FloatingLayout, tiling::TilingLayout}, OverviewMode, ANIMATION_DURATION, @@ -19,6 +18,7 @@ use crate::{ }, }, }; +use cosmic_comp_config::workspace::{OutputMatch, PinnedWorkspace}; use cosmic::theme::CosmicTheme; use cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::TilingState; @@ -74,30 +74,30 @@ use super::{ const FULLSCREEN_ANIMATION_DURATION: Duration = Duration::from_millis(200); -#[derive(Debug, Clone, PartialEq, Eq)] -struct OutputMatch { - name: String, - edid: Option, +// For stable workspace id, generate random 24-bit integer, as a hex string +// Must be compared with existing workspaces work uniqueness. +// TODO: Assign an id to any workspace that is pinned +pub fn random_id() -> String { + let id = rand::random_range(0..(2 << 24)); + format!("{:x}", id) } -impl OutputMatch { - fn for_output(output: &Output) -> Self { - Self { - name: output.name(), - edid: output.edid().cloned(), - } +fn output_match_for_output(output: &Output) -> OutputMatch { + OutputMatch { + name: output.name(), + edid: output.edid().cloned(), } +} - // If `disambguate` is true, check that edid *and* connector name match. - // Otherwise, match only edid (if it exists) - fn matches(&self, output: &Output, disambiguate: bool) -> bool { - if self.edid.as_ref() != output.edid() { - false - } else if disambiguate || self.edid.is_none() { - self.name == output.name() - } else { - true - } +// If `disambguate` is true, check that edid *and* connector name match. +// Otherwise, match only edid (if it exists) +fn output_matches(output_match: &OutputMatch, output: &Output, disambiguate: bool) -> bool { + if output_match.edid.as_ref() != output.edid() { + false + } else if disambiguate || output_match.edid.is_none() { + output_match.name == output.name() + } else { + true } } @@ -109,6 +109,7 @@ pub struct Workspace { pub minimized_windows: Vec, pub tiling_enabled: bool, pub fullscreen: Option, + pub pinned: bool, pub handle: WorkspaceHandle, pub focus_stack: FocusStacks, @@ -269,7 +270,7 @@ impl Workspace { ) -> Workspace { let tiling_layer = TilingLayout::new(theme.clone(), &output); let floating_layer = FloatingLayout::new(theme, &output); - let output_match = OutputMatch::for_output(&output); + let output_match = output_match_for_output(&output); Workspace { output, @@ -278,6 +279,7 @@ impl Workspace { tiling_enabled, minimized_windows: Vec::new(), fullscreen: None, + pinned: false, handle, focus_stack: FocusStacks::default(), screencopy: ScreencopySessions::default(), @@ -291,6 +293,55 @@ impl Workspace { } } + pub fn from_pinned( + pinned: &PinnedWorkspace, + handle: WorkspaceHandle, + output: Output, + theme: cosmic::Theme, + ) -> Self { + let tiling_layer = TilingLayout::new(theme.clone(), &output); + let floating_layer = FloatingLayout::new(theme, &output); + let output_match = output_match_for_output(&output); + + Workspace { + output, + tiling_layer, + floating_layer, + tiling_enabled: pinned.tiling_enabled, + minimized_windows: Vec::new(), + fullscreen: None, + pinned: true, + handle, + focus_stack: FocusStacks::default(), + screencopy: ScreencopySessions::default(), + output_stack: { + let mut queue = VecDeque::new(); + queue.push_back(pinned.output.clone()); + if output_match != pinned.output { + queue.push_back(output_match); + } + queue + }, + backdrop_id: Id::new(), + dirty: AtomicBool::new(false), + } + } + + pub fn to_pinned(&self) -> Option { + let output = self.explicit_output().clone(); + if self.pinned { + Some(PinnedWorkspace { + output: cosmic_comp_config::workspace::OutputMatch { + name: output.name, + edid: output.edid, + }, + tiling_enabled: self.tiling_enabled, + }) + } else { + None + } + } + #[profiling::function] pub fn refresh(&mut self) { // TODO: `Option::take_if` once stabilitized @@ -316,9 +367,9 @@ impl Workspace { } // Auto-removal of workspaces is allowed if empty, unless blocked by an - // unused and unexpired activation token. + // unused and unexpired activation token, or pinned. pub fn can_auto_remove(&self, xdg_activation_state: &XdgActivationState) -> bool { - self.is_empty() && !self.has_activation_token(xdg_activation_state) + self.is_empty() && !self.has_activation_token(xdg_activation_state) && !self.pinned } pub fn refresh_focus_stack(&mut self) { @@ -389,6 +440,11 @@ impl Workspace { &self.output } + /// Output workspace was originally created on, or explicitly moved to by the user + fn explicit_output(&self) -> &OutputMatch { + self.output_stack.front().unwrap() + } + // Set output the workspace is on // // If `explicit` is `true`, the user has explicitly moved the workspace @@ -414,21 +470,21 @@ impl Workspace { if let Some(pos) = self .output_stack .iter() - .position(|i| i.matches(output, true)) + .position(|i| output_matches(i, output, true)) { // Matched edid and connector name self.output_stack.truncate(pos + 1); } else if let Some(pos) = self .output_stack .iter() - .position(|i| i.matches(output, false)) + .position(|i| output_matches(i, output, false)) { // Matched edid but not connector name; truncate entries that don't match edid, // but keep old entry in case we see two outputs with the same edid. self.output_stack.truncate(pos + 1); - self.output_stack.push_back(OutputMatch::for_output(output)); + self.output_stack.push_back(output_match_for_output(output)); } else { - self.output_stack.push_back(OutputMatch::for_output(output)); + self.output_stack.push_back(output_match_for_output(output)); } self.output = output.clone(); } @@ -440,7 +496,7 @@ impl Workspace { .is_some_and(|edid| self.output().edid() == Some(edid)); self.output_stack .iter() - .any(|i| i.matches(output, disambiguate)) + .any(|i| output_matches(i, output, disambiguate)) } pub fn unmap(&mut self, mapped: &CosmicMapped) -> Option { diff --git a/src/wayland/handlers/workspace.rs b/src/wayland/handlers/workspace.rs index a839970f..b78343dd 100644 --- a/src/wayland/handlers/workspace.rs +++ b/src/wayland/handlers/workspace.rs @@ -4,7 +4,7 @@ use crate::{ shell::WorkspaceDelta, utils::prelude::*, wayland::protocols::workspace::{ - delegate_workspace, Request, WorkspaceHandler, WorkspaceState, + delegate_workspace, Request, State as WState, WorkspaceHandler, WorkspaceState, }, }; use cosmic_protocols::workspace::v2::server::zcosmic_workspace_handle_v2::TilingState; @@ -55,6 +55,55 @@ impl WorkspaceHandler for State { ); } } + Request::SetPin { workspace, pinned } => { + let mut shell = self.common.shell.write().unwrap(); + if let Some(workspace) = shell.workspaces.space_for_handle_mut(&workspace) { + workspace.pinned = pinned; + let mut update = self.common.workspace_state.update(); + if pinned { + update.add_workspace_state(&workspace.handle, WState::Pinned); + // TODO: Also need to update on changing other properties that are saved + shell.workspaces.persist(&self.common.config); + } else { + update.remove_workspace_state(&workspace.handle, WState::Pinned); + shell.workspaces.persist(&self.common.config); + } + } + } + Request::MoveBefore { + workspace, + other_workspace, + axis, + } => { + if axis != 0 { + continue; + } + let mut shell = self.common.shell.write().unwrap(); + let mut update = self.common.workspace_state.update(); + shell.workspaces.move_workspace( + &workspace, + &other_workspace, + &mut update, + false, + ); + } + Request::MoveAfter { + workspace, + other_workspace, + axis, + } => { + if axis != 0 { + continue; + } + let mut shell = self.common.shell.write().unwrap(); + let mut update = self.common.workspace_state.update(); + shell.workspaces.move_workspace( + &workspace, + &other_workspace, + &mut update, + true, + ); + } _ => {} } } diff --git a/src/wayland/handlers/xdg_activation.rs b/src/wayland/handlers/xdg_activation.rs index 9494bcc6..62509952 100644 --- a/src/wayland/handlers/xdg_activation.rs +++ b/src/wayland/handlers/xdg_activation.rs @@ -1,12 +1,12 @@ use crate::{shell::ActivationKey, state::ClientState, utils::prelude::*}; -use crate::{state::State, wayland::protocols::workspace::WorkspaceHandle}; +use crate::{ + state::State, + wayland::protocols::workspace::{State as WState, WorkspaceHandle}, +}; use smithay::{ delegate_xdg_activation, input::Seat, - reexports::{ - wayland_protocols::ext::workspace::v1::server::ext_workspace_handle_v1::State as WState, - wayland_server::protocol::wl_surface::WlSurface, - }, + reexports::wayland_server::protocol::wl_surface::WlSurface, wayland::xdg_activation::{ XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, }, diff --git a/src/wayland/protocols/workspace/cosmic_v2.rs b/src/wayland/protocols/workspace/cosmic_v2.rs index fa6b4396..3ae13c43 100644 --- a/src/wayland/protocols/workspace/cosmic_v2.rs +++ b/src/wayland/protocols/workspace/cosmic_v2.rs @@ -13,7 +13,7 @@ use smithay::reexports::{ use std::sync::Mutex; use super::{ - Request, Workspace, WorkspaceCapabilities, WorkspaceData, WorkspaceGlobalData, + Request, State, Workspace, WorkspaceCapabilities, WorkspaceData, WorkspaceGlobalData, WorkspaceHandler, WorkspaceManagerData, WorkspaceState, }; @@ -21,6 +21,7 @@ use super::{ pub struct CosmicWorkspaceV2DataInner { capabilities: Option, tiling: Option, + states: Option, } pub struct CosmicWorkspaceV2Data { @@ -164,6 +165,100 @@ where } } } + zcosmic_workspace_handle_v2::Request::Pin => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + { + if let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::SetPin { + workspace: workspace_handle, + pinned: true, + }); + } + } + } + zcosmic_workspace_handle_v2::Request::Unpin => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + { + if let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::SetPin { + workspace: workspace_handle, + pinned: false, + }); + } + } + } + zcosmic_workspace_handle_v2::Request::MoveBefore { + other_workspace, + axis, + } => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + { + if let Some(other_workspace) = state + .workspace_state() + .get_ext_workspace_handle(&other_workspace) + { + if let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::MoveBefore { + workspace: workspace_handle, + other_workspace, + axis, + }); + } + } + } + } + zcosmic_workspace_handle_v2::Request::MoveAfter { + other_workspace, + axis, + } => { + if let Some(workspace_handle) = + state.workspace_state().get_ext_workspace_handle(&workspace) + { + if let Some(other_workspace) = state + .workspace_state() + .get_ext_workspace_handle(&other_workspace) + { + if let Ok(manager) = + workspace.data::().unwrap().manager.upgrade() + { + let mut state = manager + .data::() + .unwrap() + .lock() + .unwrap(); + state.requests.push(Request::MoveAfter { + workspace: workspace_handle, + other_workspace, + axis, + }); + } + } + } + } zcosmic_workspace_handle_v2::Request::Destroy => {} _ => unreachable!(), } @@ -193,6 +288,12 @@ pub fn send_workspace_to_client( WorkspaceCapabilities::SetTilingState => { Some(zcosmic_workspace_handle_v2::WorkspaceCapabilities::SetTilingState) } + WorkspaceCapabilities::Pin => { + Some(zcosmic_workspace_handle_v2::WorkspaceCapabilities::Pin) + } + WorkspaceCapabilities::Move => { + Some(zcosmic_workspace_handle_v2::WorkspaceCapabilities::Move) + } _ => None, }) .collect::(); @@ -212,5 +313,21 @@ pub fn send_workspace_to_client( changed = true; } + if instance.version() >= zcosmic_workspace_handle_v2::EVT_STATE_SINCE { + let states = workspace + .states + .iter() + .filter_map(|state| match state { + State::Pinned => Some(zcosmic_workspace_handle_v2::State::Pinned), + _ => None, + }) + .collect::(); + if handle_state.states != Some(states) { + instance.state(states); + handle_state.states = Some(states); + changed = true; + } + } + changed } diff --git a/src/wayland/protocols/workspace/ext.rs b/src/wayland/protocols/workspace/ext.rs index bc33560b..80844d6c 100644 --- a/src/wayland/protocols/workspace/ext.rs +++ b/src/wayland/protocols/workspace/ext.rs @@ -20,7 +20,7 @@ use smithay::{ use std::{collections::HashSet, sync::Mutex}; use super::{ - Request, Workspace, WorkspaceCapabilities, WorkspaceGlobalData, WorkspaceGroup, + Request, State, Workspace, WorkspaceCapabilities, WorkspaceGlobalData, WorkspaceGroup, WorkspaceGroupHandle, WorkspaceHandler, WorkspaceState, }; @@ -469,12 +469,23 @@ where changed = true; } - if handle_state.states != Some(workspace.states) { - instance.state(workspace.states); - handle_state.states = Some(workspace.states.clone()); + let states = workspace + .states + .iter() + .filter_map(|state| match state { + State::Active => Some(ext_workspace_handle_v1::State::Active), + State::Urgent => Some(ext_workspace_handle_v1::State::Urgent), + State::Hidden => Some(ext_workspace_handle_v1::State::Hidden), + _ => None, + }) + .collect(); + if handle_state.states != Some(states) { + instance.state(states); + handle_state.states = Some(states); changed = true; } // TODO ext_workspace_handle_v1::id + // TODO send id if pinned if let Some(cosmic_v2_handle) = handle_state .cosmic_v2_handle diff --git a/src/wayland/protocols/workspace/mod.rs b/src/wayland/protocols/workspace/mod.rs index c8fb2f5d..2ea3a939 100644 --- a/src/wayland/protocols/workspace/mod.rs +++ b/src/wayland/protocols/workspace/mod.rs @@ -5,7 +5,7 @@ use smithay::{ reexports::{ wayland_protocols::ext::workspace::v1::server::{ ext_workspace_group_handle_v1::{ExtWorkspaceGroupHandleV1, GroupCapabilities}, - ext_workspace_handle_v1::{self, ExtWorkspaceHandleV1}, + ext_workspace_handle_v1::ExtWorkspaceHandleV1, ext_workspace_manager_v1::ExtWorkspaceManagerV1, }, wayland_server::{ @@ -38,6 +38,19 @@ bitflags::bitflags! { const Rename = 16; /// cosmic specific const SetTilingState = 32; + const Pin = 64; + const Move = 128; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct State: u32 { + const Active = 1; + const Urgent = 2; + const Hidden = 4; + /// cosmic specific + const Pinned = 8; } } @@ -96,7 +109,7 @@ pub struct Workspace { name: String, capabilities: WorkspaceCapabilities, coordinates: Vec, - states: ext_workspace_handle_v1::State, + states: State, tiling: zcosmic_workspace_handle_v2::TilingState, ext_id: Option, } @@ -148,6 +161,20 @@ pub enum Request { workspace: WorkspaceHandle, group: WorkspaceGroupHandle, }, + SetPin { + workspace: WorkspaceHandle, + pinned: bool, + }, + MoveBefore { + workspace: WorkspaceHandle, + other_workspace: WorkspaceHandle, + axis: u32, + }, + MoveAfter { + workspace: WorkspaceHandle, + other_workspace: WorkspaceHandle, + axis: u32, + }, } impl WorkspaceState @@ -166,7 +193,7 @@ where ); let cosmic_v2_global = dh.create_global::( - 1, + 2, WorkspaceGlobalData { filter: Box::new(client_filter.clone()), }, @@ -240,10 +267,7 @@ where }) } - pub fn workspace_states( - &self, - workspace: &WorkspaceHandle, - ) -> Option { + pub fn workspace_states(&self, workspace: &WorkspaceHandle) -> Option { self.groups .iter() .find_map(|g| Some(g.workspaces.iter().find(|w| w.id == workspace.id)?.states)) @@ -332,6 +356,7 @@ where &mut self, group: &WorkspaceGroupHandle, tiling: zcosmic_workspace_handle_v2::TilingState, + // TODO way to add id to workspace that doesn't have it ext_id: Option, ) -> Option { if let Some(group) = self.0.groups.iter_mut().find(|g| g.id == group.id) { @@ -343,7 +368,7 @@ where name: Default::default(), capabilities: WorkspaceCapabilities::empty(), coordinates: Default::default(), - states: ext_workspace_handle_v1::State::empty(), + states: State::empty(), ext_id, }; group.workspaces.push(workspace); @@ -538,18 +563,11 @@ where } } - pub fn workspace_states( - &self, - workspace: &WorkspaceHandle, - ) -> Option { + pub fn workspace_states(&self, workspace: &WorkspaceHandle) -> Option { self.0.workspace_states(workspace) } - pub fn add_workspace_state( - &mut self, - workspace: &WorkspaceHandle, - state: ext_workspace_handle_v1::State, - ) { + pub fn add_workspace_state(&mut self, workspace: &WorkspaceHandle, state: State) { if let Some(workspace) = self .0 .groups @@ -560,11 +578,7 @@ where } } - pub fn remove_workspace_state( - &mut self, - workspace: &WorkspaceHandle, - state: ext_workspace_handle_v1::State, - ) { + pub fn remove_workspace_state(&mut self, workspace: &WorkspaceHandle, state: State) { if let Some(workspace) = self .0 .groups