Update to workspace v2, based on ext-workspace

In the workspace applet, this now uses `Workspace` in the front-end code
instead of a tuple with unnamed fields. Handling of scrolling is also
moved to the frontend, which uses less code and seems more natural. It
would be good to have a helper in libcosmic for this. It also changes
`ObjectId` to `ExtWorkspaceHandleV1`, which is a little simpler and I
see no reason here to avoid the more strongly typed object.

At some point we may want a shared subscription for workspaces in
multiple applets. As well as a higher-level abstraction for screen
capture.
This commit is contained in:
Ian Douglas Scott 2025-03-06 14:13:01 -08:00 committed by Ian Douglas Scott
parent f08d80a891
commit 7ba2ed0c53
12 changed files with 196 additions and 269 deletions

15
Cargo.lock generated
View file

@ -1395,8 +1395,9 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-client-toolkit" name = "cosmic-client-toolkit"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os//cosmic-protocols?rev=ed2a481#ed2a48143cd6dc63274aa04461de3d341e50b16b" source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#6b05c2a157118979cb472a38455ba78ca9729196"
dependencies = [ dependencies = [
"bitflags 2.8.0",
"cosmic-protocols", "cosmic-protocols",
"libc", "libc",
"smithay-client-toolkit", "smithay-client-toolkit",
@ -1534,7 +1535,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-protocols" name = "cosmic-protocols"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os//cosmic-protocols?rev=ed2a481#ed2a48143cd6dc63274aa04461de3d341e50b16b" source = "git+https://github.com/pop-os/cosmic-protocols//?branch=main#6b05c2a157118979cb472a38455ba78ca9729196"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"wayland-backend", "wayland-backend",
@ -2148,7 +2149,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3946,7 +3947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@ -5530,7 +5531,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -6055,7 +6056,7 @@ dependencies = [
"getrandom 0.3.1", "getrandom 0.3.1",
"once_cell", "once_cell",
"rustix 0.38.44", "rustix 0.38.44",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -6988,7 +6989,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View file

@ -86,5 +86,5 @@ ignored = ["libcosmic"]
sctk = { package = "smithay-client-toolkit", version = "=0.19.2" } sctk = { package = "smithay-client-toolkit", version = "=0.19.2" }
[patch."https://github.com/pop-os/cosmic-protocols"] [patch."https://github.com/pop-os/cosmic-protocols"]
cctk = { git = "https://github.com/pop-os//cosmic-protocols", package = "cosmic-client-toolkit", rev = "ed2a481" } cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", rev = "ed2a481" } cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }

View file

@ -14,7 +14,10 @@ use cctk::{
wayland_client::protocol::{ wayland_client::protocol::{
wl_data_device_manager::DndAction, wl_output::WlOutput, wl_seat::WlSeat, wl_data_device_manager::DndAction, wl_output::WlOutput, wl_seat::WlSeat,
}, },
wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, wayland_protocols::ext::{
foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
workspace::v1::client::ext_workspace_handle_v1::ExtWorkspaceHandleV1,
},
}; };
use cosmic::{ use cosmic::{
applet::{ applet::{
@ -44,10 +47,7 @@ use cosmic::{
Apply, Element, Task, Apply, Element, Task,
}; };
use cosmic_app_list_config::{AppListConfig, APP_ID}; use cosmic_app_list_config::{AppListConfig, APP_ID};
use cosmic_protocols::{ use cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::State;
toplevel_info::v1::client::zcosmic_toplevel_handle_v1::State,
workspace::v1::client::zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1,
};
use freedesktop_desktop_entry as fde; use freedesktop_desktop_entry as fde;
use freedesktop_desktop_entry::{get_languages_from_env, DesktopEntry}; use freedesktop_desktop_entry::{get_languages_from_env, DesktopEntry};
use futures::future::pending; use futures::future::pending;
@ -327,7 +327,7 @@ struct CosmicAppList {
dnd_offer: Option<DndOffer>, dnd_offer: Option<DndOffer>,
is_listening_for_dnd: bool, is_listening_for_dnd: bool,
gpus: Option<Vec<Gpu>>, gpus: Option<Vec<Gpu>>,
active_workspaces: Vec<ZcosmicWorkspaceHandleV1>, active_workspaces: Vec<ExtWorkspaceHandleV1>,
output_list: HashMap<WlOutput, OutputInfo>, output_list: HashMap<WlOutput, OutputInfo>,
locales: Vec<String>, locales: Vec<String>,
overflow_favorites_popup: Option<window::Id>, overflow_favorites_popup: Option<window::Id>,

View file

@ -39,13 +39,15 @@ use cctk::{
}, },
Connection, Dispatch, QueueHandle, WEnum, Connection, Dispatch, QueueHandle, WEnum,
}, },
wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, wayland_protocols::ext::{
foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
workspace::v1::client::ext_workspace_handle_v1::State as WorkspaceUpdateState,
},
workspace::{WorkspaceHandler, WorkspaceState}, workspace::{WorkspaceHandler, WorkspaceState},
}; };
use cosmic_protocols::{ use cosmic_protocols::{
toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
toplevel_management::v1::client::zcosmic_toplevel_manager_v1, toplevel_management::v1::client::zcosmic_toplevel_manager_v1,
workspace::v1::client::zcosmic_workspace_handle_v1::State as WorkspaceUpdateState,
}; };
use futures::channel::mpsc::UnboundedSender; use futures::channel::mpsc::UnboundedSender;
use sctk::{ use sctk::{
@ -129,12 +131,11 @@ impl WorkspaceHandler for AppData {
let active_workspaces = self let active_workspaces = self
.workspace_state .workspace_state
.workspace_groups() .workspace_groups()
.iter()
.filter_map(|x| { .filter_map(|x| {
x.workspaces.iter().find(|w| { x.workspaces
w.state .iter()
.contains(&WEnum::Value(WorkspaceUpdateState::Active)) .filter_map(|handle| self.workspace_state.workspace_info(handle))
}) .find(|w| w.state.contains(WorkspaceUpdateState::Active))
}) })
.map(|workspace| workspace.handle.clone()) .map(|workspace| workspace.handle.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -364,7 +365,7 @@ impl CaptureData {
pub fn capture_source_shm_fd<Fd: AsFd>( pub fn capture_source_shm_fd<Fd: AsFd>(
&self, &self,
overlay_cursor: bool, overlay_cursor: bool,
source: ZcosmicToplevelHandleV1, source: &ExtForeignToplevelHandleV1,
fd: Fd, fd: Fd,
len: Option<u32>, len: Option<u32>,
) -> Option<ShmImage<Fd>> { ) -> Option<ShmImage<Fd>> {
@ -379,7 +380,7 @@ impl CaptureData {
let capture_session = self let capture_session = self
.capturer .capturer
.create_session( .create_session(
&CaptureSource::CosmicToplevel(source), &CaptureSource::Toplevel(source.clone()),
CaptureOptions::empty(), CaptureOptions::empty(),
&self.qh, &self.qh,
SessionData { SessionData {
@ -494,9 +495,6 @@ impl AppData {
wl_shm: self.shm_state.wl_shm().clone(), wl_shm: self.shm_state.wl_shm().clone(),
capturer: self.screencopy_state.capturer().clone(), capturer: self.screencopy_state.capturer().clone(),
}; };
let Some(cosmic_toplevel) = self.cosmic_toplevel(&handle) else {
return;
};
std::thread::spawn(move || { std::thread::spawn(move || {
use std::ffi::CStr; use std::ffi::CStr;
let name = unsafe { CStr::from_bytes_with_nul_unchecked(b"app-list-screencopy\0") }; let name = unsafe { CStr::from_bytes_with_nul_unchecked(b"app-list-screencopy\0") };
@ -506,7 +504,7 @@ impl AppData {
}; };
// XXX is this going to use to much memory? // XXX is this going to use to much memory?
let img = capture_data.capture_source_shm_fd(false, cosmic_toplevel, fd, None); let img = capture_data.capture_source_shm_fd(false, &handle, fd, None);
if let Some(img) = img { if let Some(img) = img {
let Ok(img) = img.image() else { let Ok(img) = img.image() else {
tracing::error!("Failed to get RgbaImage"); tracing::error!("Failed to get RgbaImage");

View file

@ -6,13 +6,15 @@ use cctk::{
sctk::{output::OutputInfo, reexports::calloop}, sctk::{output::OutputInfo, reexports::calloop},
toplevel_info::ToplevelInfo, toplevel_info::ToplevelInfo,
wayland_client::protocol::wl_output::WlOutput, wayland_client::protocol::wl_output::WlOutput,
wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, wayland_protocols::ext::{
foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
workspace::v1::client::ext_workspace_handle_v1::ExtWorkspaceHandleV1,
},
}; };
use cosmic::{ use cosmic::{
iced::{self, stream, Subscription}, iced::{self, stream, Subscription},
iced_core::image::Bytes, iced_core::image::Bytes,
}; };
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1;
use image::EncodableLayout; use image::EncodableLayout;
use futures::{ use futures::{
@ -110,7 +112,7 @@ pub enum WaylandUpdate {
Init(calloop::channel::Sender<WaylandRequest>), Init(calloop::channel::Sender<WaylandRequest>),
Finished, Finished,
Toplevel(ToplevelUpdate), Toplevel(ToplevelUpdate),
Workspace(Vec<ZcosmicWorkspaceHandleV1>), Workspace(Vec<ExtWorkspaceHandleV1>),
Output(OutputUpdate), Output(OutputUpdate),
ActivationToken { ActivationToken {
token: Option<String>, token: Option<String>,

View file

@ -131,7 +131,7 @@ impl CaptureData {
pub fn capture_source_shm_fd<Fd: AsFd>( pub fn capture_source_shm_fd<Fd: AsFd>(
&self, &self,
overlay_cursor: bool, overlay_cursor: bool,
source: ZcosmicToplevelHandleV1, source: ExtForeignToplevelHandleV1,
fd: Fd, fd: Fd,
len: Option<u32>, len: Option<u32>,
) -> Option<ShmImage<Fd>> { ) -> Option<ShmImage<Fd>> {
@ -145,7 +145,7 @@ impl CaptureData {
let capture_session = self let capture_session = self
.capturer .capturer
.create_session( .create_session(
&CaptureSource::CosmicToplevel(source), &CaptureSource::Toplevel(source),
CaptureOptions::empty(), CaptureOptions::empty(),
&self.qh, &self.qh,
SessionData { SessionData {
@ -310,9 +310,6 @@ impl AppData {
wl_shm: self.shm_state.wl_shm().clone(), wl_shm: self.shm_state.wl_shm().clone(),
capturer: self.screencopy_state.capturer().clone(), capturer: self.screencopy_state.capturer().clone(),
}; };
let Some(cosmic_toplevel) = self.cosmic_toplevel(&handle) else {
return;
};
std::thread::spawn(move || { std::thread::spawn(move || {
use std::ffi::CStr; use std::ffi::CStr;
let name = let name =
@ -323,7 +320,7 @@ impl AppData {
}; };
// XXX is this going to use to much memory? // XXX is this going to use to much memory?
let img = capure_data.capture_source_shm_fd(false, cosmic_toplevel, fd, None); let img = capure_data.capture_source_shm_fd(false, handle.clone(), fd, None);
if let Some(img) = img { if let Some(img) = img {
let Ok(mut img) = img.image() else { let Ok(mut img) = img.image() else {
tracing::error!("Failed to get RgbaImage"); tracing::error!("Failed to get RgbaImage");

View file

@ -6,7 +6,10 @@ use cctk::{
sctk::{ sctk::{
self, self,
output::{OutputHandler, OutputState}, output::{OutputHandler, OutputState},
reexports::{calloop, calloop_wayland_source::WaylandSource, client as wayland_client}, reexports::{
calloop, calloop_wayland_source::WaylandSource, client as wayland_client,
protocols::ext::workspace::v1::client::ext_workspace_handle_v1,
},
registry::{ProvidesRegistryState, RegistryState}, registry::{ProvidesRegistryState, RegistryState},
}, },
toplevel_info::{ToplevelInfoHandler, ToplevelInfoState}, toplevel_info::{ToplevelInfoHandler, ToplevelInfoState},
@ -15,7 +18,7 @@ use cctk::{
workspace::{WorkspaceHandler, WorkspaceState}, workspace::{WorkspaceHandler, WorkspaceState},
}; };
use cosmic::iced::futures; use cosmic::iced::futures;
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1::{self, TilingState}; use cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::TilingState;
use futures::{channel::mpsc, executor::block_on, SinkExt}; use futures::{channel::mpsc, executor::block_on, SinkExt};
use std::{ use std::{
collections::HashSet, collections::HashSet,
@ -87,44 +90,41 @@ pub fn spawn_workspaces(tx: mpsc::Sender<TilingState>) -> SyncSender<AppRequest>
loop_handle loop_handle
.insert_source(workspaces_rx, |e, _, state| match e { .insert_source(workspaces_rx, |e, _, state| match e {
Event::Msg(AppRequest::TilingState(autotile)) => { Event::Msg(AppRequest::TilingState(autotile)) => {
if let Some(w) = if let Some(w) = state.workspace_state.workspace_groups().find_map(|g| {
state if let Some(o) = state.expected_output.as_ref() {
.workspace_state if !g.outputs.contains(o) {
.workspace_groups() return None;
}
}
g.workspaces
.iter() .iter()
.find_map(|g| { .filter_map(|handle| state.workspace_state.workspace_info(handle))
if let Some(o) = state.expected_output.as_ref() { .find(|w| w.state.contains(ext_workspace_handle_v1::State::Active))
if !g.outputs.contains(o) { }) {
return None; if let Some(cosmic_handle) = &w.cosmic_handle {
} cosmic_handle.set_tiling_state(autotile);
} state
g.workspaces.iter().find(|w| { .workspace_state
w.state.contains(&WEnum::Value( .workspace_manager()
zcosmic_workspace_handle_v1::State::Active, .get()
)) .unwrap()
}) .commit();
}) }
{
w.handle.set_tiling_state(autotile);
state
.workspace_state
.workspace_manager()
.get()
.unwrap()
.commit();
} }
} }
Event::Msg(AppRequest::DefaultBehavior(tiling)) => { Event::Msg(AppRequest::DefaultBehavior(tiling)) => {
for w in state for w in state
.workspace_state .workspace_state
.workspace_groups() .workspace_groups()
.iter()
.flat_map(|g| g.workspaces.iter()) .flat_map(|g| g.workspaces.iter())
.filter_map(|handle| state.workspace_state.workspace_info(handle))
.filter(|w| { .filter(|w| {
!state.workspaces_with_previous_toplevel.contains(&w.handle) !state.workspaces_with_previous_toplevel.contains(&w.handle)
}) })
{ {
w.handle.set_tiling_state(tiling); if let Some(cosmic_handle) = &w.cosmic_handle {
cosmic_handle.set_tiling_state(tiling);
}
} }
state state
.workspace_state .workspace_state
@ -167,25 +167,22 @@ pub struct State {
registry_state: RegistryState, registry_state: RegistryState,
workspace_state: WorkspaceState, workspace_state: WorkspaceState,
toplevel_info_state: ToplevelInfoState, toplevel_info_state: ToplevelInfoState,
workspaces_with_previous_toplevel: workspaces_with_previous_toplevel: HashSet<ext_workspace_handle_v1::ExtWorkspaceHandleV1>,
HashSet<zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1>,
have_workspaces: bool, have_workspaces: bool,
} }
impl State { impl State {
pub fn tiling_state(&self) -> Option<TilingState> { pub fn tiling_state(&self) -> Option<TilingState> {
self.workspace_state self.workspace_state.workspace_groups().find_map(|g| {
.workspace_groups() if g.outputs
.iter() .iter()
.find_map(|g| { .any(|o| Some(o) == self.expected_output.as_ref())
if g.outputs {
g.workspaces
.iter() .iter()
.any(|o| Some(o) == self.expected_output.as_ref()) .filter_map(|handle| self.workspace_state.workspace_info(handle))
{ .find_map(|w| {
g.workspaces.iter().find_map(|w| { if w.state.contains(ext_workspace_handle_v1::State::Active) {
if w.state
.contains(&WEnum::Value(zcosmic_workspace_handle_v1::State::Active))
{
w.tiling.and_then(|e| match e { w.tiling.and_then(|e| match e {
WEnum::Value(v) => Some(v), WEnum::Value(v) => Some(v),
_ => { _ => {
@ -197,10 +194,10 @@ impl State {
None None
} }
}) })
} else { } else {
None None
} }
}) })
} }
} }

View file

@ -8,7 +8,7 @@ use cosmic::iced::{
futures::{self, channel::mpsc, SinkExt, StreamExt}, futures::{self, channel::mpsc, SinkExt, StreamExt},
stream, Subscription, stream, Subscription,
}; };
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1::TilingState; use cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::TilingState;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tokio::sync::Mutex; use tokio::sync::Mutex;

View file

@ -25,7 +25,7 @@ use cosmic::{
Element, Element,
}; };
use cosmic_comp_config::{CosmicCompConfig, TileBehavior}; use cosmic_comp_config::{CosmicCompConfig, TileBehavior};
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1::TilingState; use cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::TilingState;
use cosmic_time::{anim, chain, id, Timeline}; use cosmic_time::{anim, chain, id, Timeline};
use std::{thread, time::Instant}; use std::{thread, time::Instant};
use tracing::error; use tracing::error;

View file

@ -1,7 +1,15 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cctk::sctk::reexports::{calloop::channel::SyncSender, client::backend::ObjectId}; use cctk::{
sctk::reexports::{
calloop::channel::SyncSender,
protocols::ext::workspace::v1::client::ext_workspace_handle_v1::{
self, ExtWorkspaceHandleV1,
},
},
workspace::Workspace,
};
use cosmic::{ use cosmic::{
applet::cosmic_panel_config::PanelAnchor, applet::cosmic_panel_config::PanelAnchor,
iced::{ iced::{
@ -17,17 +25,18 @@ use cosmic::{
Element, Task, Theme, Element, Task, Theme,
}; };
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::cmp::Ordering;
use crate::{ use crate::{
config, config,
wayland::{WorkspaceEvent, WorkspaceList}, wayland::WorkspaceEvent,
wayland_subscription::{workspaces, WorkspacesUpdate}, wayland_subscription::{workspaces, WorkspacesUpdate},
}; };
use std::process::Command as ShellCommand; use std::{
process::Command as ShellCommand,
time::{Duration, Instant},
};
static AUTOSIZE_MAIN_ID: Lazy<Id> = Lazy::new(|| Id::new("autosize-main")); static AUTOSIZE_MAIN_ID: Lazy<Id> = Lazy::new(|| Id::new("autosize-main"));
@ -43,9 +52,12 @@ pub enum Layout {
struct IcedWorkspacesApplet { struct IcedWorkspacesApplet {
core: cosmic::app::Core, core: cosmic::app::Core,
workspaces: WorkspaceList, workspaces: Vec<Workspace>,
workspace_tx: Option<SyncSender<WorkspaceEvent>>, workspace_tx: Option<SyncSender<WorkspaceEvent>>,
layout: Layout, layout: Layout,
scroll: f64,
next_scroll: Option<Instant>,
last_scroll: Instant,
} }
impl IcedWorkspacesApplet { impl IcedWorkspacesApplet {
@ -78,7 +90,7 @@ impl IcedWorkspacesApplet {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
WorkspaceUpdate(WorkspacesUpdate), WorkspaceUpdate(WorkspacesUpdate),
WorkspacePressed(ObjectId), WorkspacePressed(ExtWorkspaceHandleV1),
WheelScrolled(ScrollDelta), WheelScrolled(ScrollDelta),
WorkspaceOverview, WorkspaceOverview,
} }
@ -105,6 +117,9 @@ impl cosmic::Application for IcedWorkspacesApplet {
core, core,
workspaces: Vec::new(), workspaces: Vec::new(),
workspace_tx: Default::default(), workspace_tx: Default::default(),
scroll: 0.0,
next_scroll: None,
last_scroll: Instant::now(),
}, },
Task::none(), Task::none(),
) )
@ -125,14 +140,8 @@ impl cosmic::Application for IcedWorkspacesApplet {
match message { match message {
Message::WorkspaceUpdate(msg) => match msg { Message::WorkspaceUpdate(msg) => match msg {
WorkspacesUpdate::Workspaces(mut list) => { WorkspacesUpdate::Workspaces(mut list) => {
list.retain(|w| { list.retain(|w| !w.state.contains(ext_workspace_handle_v1::State::Hidden));
!matches!(w.1, Some(zcosmic_workspace_handle_v1::State::Hidden)) list.sort_by(|w1, w2| w1.coordinates.cmp(&w2.coordinates));
});
list.sort_by(|a, b| match a.0.len().cmp(&b.0.len()) {
Ordering::Equal => a.0.cmp(&b.0),
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
});
self.workspaces = list; self.workspaces = list;
} }
WorkspacesUpdate::Started(tx) => { WorkspacesUpdate::Started(tx) => {
@ -152,8 +161,55 @@ impl cosmic::Application for IcedWorkspacesApplet {
ScrollDelta::Lines { x, y } => ((x + y) as f64, false), ScrollDelta::Lines { x, y } => ((x + y) as f64, false),
ScrollDelta::Pixels { x, y } => ((x + y) as f64, true), ScrollDelta::Pixels { x, y } => ((x + y) as f64, true),
}; };
if let Some(tx) = self.workspace_tx.as_mut() {
let _ = tx.try_send(WorkspaceEvent::Scroll(delta, debounce)); let dur = if debounce {
Duration::from_millis(350)
} else {
Duration::from_millis(200)
};
if self.last_scroll.elapsed() > Duration::from_millis(100)
|| self.scroll * delta < 0.0
{
self.next_scroll = None;
self.scroll = 0.0;
}
self.last_scroll = Instant::now();
self.scroll += delta;
if let Some(next) = self.next_scroll {
if next > Instant::now() {
return cosmic::iced::Task::none();
}
self.next_scroll = None;
}
if self.scroll.abs() < 1.0 {
return cosmic::iced::Task::none();
}
self.next_scroll = Some(Instant::now() + dur);
if let Some(w_i) = self
.workspaces
.iter()
.position(|w| w.state.contains(ext_workspace_handle_v1::State::Active))
{
let max_w = self.workspaces.len().wrapping_sub(1);
let d_i = if self.scroll > 0.0 {
if w_i == 0 {
max_w
} else {
w_i.wrapping_sub(1)
}
} else if w_i == max_w {
0
} else {
w_i.wrapping_add(1)
};
self.scroll = 0.0;
if let Some(w) = self.workspaces.get(d_i) {
if let Some(tx) = self.workspace_tx.as_mut() {
let _ = tx.try_send(WorkspaceEvent::Activate(w.handle.clone()));
}
}
} }
} }
Message::WorkspaceOverview => { Message::WorkspaceOverview => {
@ -177,11 +233,7 @@ impl cosmic::Application for IcedWorkspacesApplet {
let popup_index = self.popup_index().unwrap_or(self.workspaces.len()); let popup_index = self.popup_index().unwrap_or(self.workspaces.len());
let buttons = self.workspaces[..popup_index].iter().filter_map(|w| { let buttons = self.workspaces[..popup_index].iter().filter_map(|w| {
let content = self let content = self.core.applet.text(&w.name).font(cosmic::font::bold());
.core
.applet
.text(w.0.clone())
.font(cosmic::font::bold());
let (width, height) = if self.core.applet.is_horizontal() { let (width, height) = if self.core.applet.is_horizontal() {
(suggested_total as f32, suggested_window_size.1.get() as f32) (suggested_total as f32, suggested_window_size.1.get() as f32)
@ -205,18 +257,20 @@ impl cosmic::Application for IcedWorkspacesApplet {
} else { } else {
[self.core.applet.suggested_padding(true), 0] [self.core.applet.suggested_padding(true), 0]
}) })
.on_press(match w.1 { .on_press(
Some(zcosmic_workspace_handle_v1::State::Active) => Message::WorkspaceOverview, if w.state.contains(ext_workspace_handle_v1::State::Active) {
_ => Message::WorkspacePressed(w.2.clone()), Message::WorkspaceOverview
}) } else {
Message::WorkspacePressed(w.handle.clone())
},
)
.padding(0); .padding(0);
Some( Some(
btn.class(match w.1 { btn.class(
Some(zcosmic_workspace_handle_v1::State::Active) => { if w.state.contains(ext_workspace_handle_v1::State::Active) {
cosmic::theme::iced::Button::Primary cosmic::theme::iced::Button::Primary
} } else if w.state.contains(ext_workspace_handle_v1::State::Urgent) {
Some(zcosmic_workspace_handle_v1::State::Urgent) => {
let appearance = |theme: &Theme| { let appearance = |theme: &Theme| {
let cosmic = theme.cosmic(); let cosmic = theme.cosmic();
button::Style { button::Style {
@ -249,8 +303,7 @@ impl cosmic::Application for IcedWorkspacesApplet {
button::Status::Disabled => appearance(theme), button::Status::Disabled => appearance(theme),
} }
})) }))
} } else {
None => {
let appearance = |theme: &Theme| { let appearance = |theme: &Theme| {
let cosmic = theme.cosmic(); let cosmic = theme.cosmic();
button::Style { button::Style {
@ -282,9 +335,8 @@ impl cosmic::Application for IcedWorkspacesApplet {
} }
} }
})) }))
} },
_ => return None, )
})
.into(), .into(),
) )
}); });

View file

@ -10,35 +10,29 @@ use cctk::{
calloop, calloop,
calloop_wayland_source::WaylandSource, calloop_wayland_source::WaylandSource,
client::{self as wayland_client}, client::{self as wayland_client},
protocols::ext::workspace::v1::client::ext_workspace_handle_v1::ExtWorkspaceHandleV1,
}, },
registry::{ProvidesRegistryState, RegistryState}, registry::{ProvidesRegistryState, RegistryState},
}, },
workspace::{WorkspaceHandler, WorkspaceState}, workspace::{Workspace, WorkspaceHandler, WorkspaceState},
}; };
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use futures::{channel::mpsc, executor::block_on, SinkExt}; use futures::{channel::mpsc, executor::block_on, SinkExt};
use std::{ use std::os::{
os::{ fd::{FromRawFd, RawFd},
fd::{FromRawFd, RawFd}, unix::net::UnixStream,
unix::net::UnixStream,
},
time::{Duration, Instant},
}; };
use wayland_client::{ use wayland_client::{
backend::ObjectId,
globals::registry_queue_init, globals::registry_queue_init,
protocol::wl_output::{self, WlOutput}, protocol::wl_output::{self, WlOutput},
Connection, Proxy, QueueHandle, WEnum, Connection, QueueHandle,
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum WorkspaceEvent { pub enum WorkspaceEvent {
Activate(ObjectId), Activate(ExtWorkspaceHandleV1),
Scroll(f64, bool),
} }
pub type WorkspaceList = Vec<(String, Option<zcosmic_workspace_handle_v1::State>, ObjectId)>;
pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<WorkspaceEvent> { pub fn spawn_workspaces(tx: mpsc::Sender<Vec<Workspace>>) -> SyncSender<WorkspaceEvent> {
let (workspaces_tx, workspaces_rx) = calloop::channel::sync_channel(100); let (workspaces_tx, workspaces_rx) = calloop::channel::sync_channel(100);
let socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET") let socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET")
@ -81,100 +75,18 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
tx, tx,
running: true, running: true,
have_workspaces: false, have_workspaces: false,
scroll: 0.0,
next_scroll: None,
last_scroll: Instant::now(),
}; };
let loop_handle = event_loop.handle(); let loop_handle = event_loop.handle();
loop_handle loop_handle
.insert_source(workspaces_rx, |e, _, state| match e { .insert_source(workspaces_rx, |e, _, state| match e {
Event::Msg(WorkspaceEvent::Activate(id)) => { Event::Msg(WorkspaceEvent::Activate(handle)) => {
if let Some(w) = state handle.activate();
state
.workspace_state .workspace_state
.workspace_groups() .workspace_manager()
.iter() .get()
.find_map(|g| g.workspaces.iter().find(|w| w.handle.id() == id)) .unwrap()
{ .commit();
w.handle.activate();
state
.workspace_state
.workspace_manager()
.get()
.unwrap()
.commit();
}
}
Event::Msg(WorkspaceEvent::Scroll(v, debounce)) => {
let dur = if debounce {
Duration::from_millis(350)
} else {
Duration::from_millis(200)
};
if state.last_scroll.elapsed() > Duration::from_millis(100)
|| state.scroll * v < 0.0
{
state.next_scroll = None;
state.scroll = 0.0;
}
state.last_scroll = Instant::now();
state.scroll += v;
if let Some(next) = state.next_scroll {
if next > Instant::now() {
return;
}
state.next_scroll = None;
}
if state.scroll.abs() < 1.0 {
return;
}
state.next_scroll = Some(Instant::now() + dur);
if let Some((w_g, w_i)) = state
.workspace_state
.workspace_groups()
.iter()
.find_map(|g| {
if !g
.outputs
.iter()
.any(|o| Some(o) == state.expected_output.as_ref())
{
return None;
}
g.workspaces
.iter()
.position(|w| {
w.state.contains(&WEnum::Value(
zcosmic_workspace_handle_v1::State::Active,
))
})
.map(|w_i| (g, w_i))
})
{
let max_w = w_g.workspaces.len().wrapping_sub(1);
let d_i = if state.scroll > 0.0 {
if w_i == 0 {
max_w
} else {
w_i.wrapping_sub(1)
}
} else if w_i == max_w {
0
} else {
w_i.wrapping_add(1)
};
state.scroll = 0.0;
if let Some(w) = w_g.workspaces.get(d_i) {
w.handle.activate();
state
.workspace_state
.workspace_manager()
.get()
.unwrap()
.commit();
}
}
} }
Event::Closed => { Event::Closed => {
if let Ok(workspace_manager) = if let Ok(workspace_manager) =
@ -203,62 +115,30 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
#[derive(Debug)] #[derive(Debug)]
pub struct State { pub struct State {
running: bool, running: bool,
tx: mpsc::Sender<WorkspaceList>, tx: mpsc::Sender<Vec<Workspace>>,
configured_output: String, configured_output: String,
expected_output: Option<WlOutput>, expected_output: Option<WlOutput>,
output_state: OutputState, output_state: OutputState,
registry_state: RegistryState, registry_state: RegistryState,
workspace_state: WorkspaceState, workspace_state: WorkspaceState,
have_workspaces: bool, have_workspaces: bool,
scroll: f64,
next_scroll: Option<Instant>,
last_scroll: Instant,
} }
impl State { impl State {
pub fn workspace_list( pub fn workspace_list(&self) -> Vec<Workspace> {
&self,
) -> Vec<(String, Option<zcosmic_workspace_handle_v1::State>, ObjectId)> {
self.workspace_state self.workspace_state
.workspace_groups() .workspace_groups()
.iter() .filter(|g| {
.filter_map(|g| { g.outputs
if g.outputs
.iter() .iter()
.any(|o| Some(o) == self.expected_output.as_ref()) .any(|o| Some(o) == self.expected_output.as_ref())
{
Some(g.workspaces.iter().map(|w| {
(
w.name.clone(),
match &w.state {
x if x.contains(&WEnum::Value(
zcosmic_workspace_handle_v1::State::Active,
)) =>
{
Some(zcosmic_workspace_handle_v1::State::Active)
}
x if x.contains(&WEnum::Value(
zcosmic_workspace_handle_v1::State::Urgent,
)) =>
{
Some(zcosmic_workspace_handle_v1::State::Urgent)
}
x if x.contains(&WEnum::Value(
zcosmic_workspace_handle_v1::State::Hidden,
)) =>
{
Some(zcosmic_workspace_handle_v1::State::Hidden)
}
_ => None,
},
w.handle.id(),
)
}))
} else {
None
}
}) })
.flatten() .flat_map(|g| {
g.workspaces
.iter()
.filter_map(|handle| self.workspace_state.workspace_info(handle))
})
.cloned()
.collect() .collect()
} }
} }

View file

@ -1,8 +1,8 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use crate::wayland::{self, WorkspaceEvent, WorkspaceList}; use crate::wayland::{self, WorkspaceEvent};
use cctk::sctk::reexports::calloop::channel::SyncSender; use cctk::{sctk::reexports::calloop::channel::SyncSender, workspace::Workspace};
use cosmic::iced::{ use cosmic::iced::{
self, self,
futures::{channel::mpsc, SinkExt, StreamExt}, futures::{channel::mpsc, SinkExt, StreamExt},
@ -11,12 +11,12 @@ use cosmic::iced::{
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tokio::sync::Mutex; use tokio::sync::Mutex;
pub static WAYLAND_RX: Lazy<Mutex<Option<mpsc::Receiver<WorkspaceList>>>> = pub static WAYLAND_RX: Lazy<Mutex<Option<mpsc::Receiver<Vec<Workspace>>>>> =
Lazy::new(|| Mutex::new(None)); Lazy::new(|| Mutex::new(None));
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum WorkspacesUpdate { pub enum WorkspacesUpdate {
Workspaces(WorkspaceList), Workspaces(Vec<Workspace>),
Started(SyncSender<WorkspaceEvent>), Started(SyncSender<WorkspaceEvent>),
Errored, Errored,
} }
@ -71,7 +71,7 @@ pub enum State {
} }
pub struct WorkspacesWatcher { pub struct WorkspacesWatcher {
rx: mpsc::Receiver<WorkspaceList>, rx: mpsc::Receiver<Vec<Workspace>>,
tx: SyncSender<WorkspaceEvent>, tx: SyncSender<WorkspaceEvent>,
} }