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

View file

@ -1,7 +1,15 @@
// Copyright 2023 System76 <info@system76.com>
// 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::{
applet::cosmic_panel_config::PanelAnchor,
iced::{
@ -17,17 +25,18 @@ use cosmic::{
Element, Task, Theme,
};
use cosmic_protocols::workspace::v1::client::zcosmic_workspace_handle_v1;
use once_cell::sync::Lazy;
use std::cmp::Ordering;
use crate::{
config,
wayland::{WorkspaceEvent, WorkspaceList},
wayland::WorkspaceEvent,
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"));
@ -43,9 +52,12 @@ pub enum Layout {
struct IcedWorkspacesApplet {
core: cosmic::app::Core,
workspaces: WorkspaceList,
workspaces: Vec<Workspace>,
workspace_tx: Option<SyncSender<WorkspaceEvent>>,
layout: Layout,
scroll: f64,
next_scroll: Option<Instant>,
last_scroll: Instant,
}
impl IcedWorkspacesApplet {
@ -78,7 +90,7 @@ impl IcedWorkspacesApplet {
#[derive(Debug, Clone)]
enum Message {
WorkspaceUpdate(WorkspacesUpdate),
WorkspacePressed(ObjectId),
WorkspacePressed(ExtWorkspaceHandleV1),
WheelScrolled(ScrollDelta),
WorkspaceOverview,
}
@ -105,6 +117,9 @@ impl cosmic::Application for IcedWorkspacesApplet {
core,
workspaces: Vec::new(),
workspace_tx: Default::default(),
scroll: 0.0,
next_scroll: None,
last_scroll: Instant::now(),
},
Task::none(),
)
@ -125,14 +140,8 @@ impl cosmic::Application for IcedWorkspacesApplet {
match message {
Message::WorkspaceUpdate(msg) => match msg {
WorkspacesUpdate::Workspaces(mut list) => {
list.retain(|w| {
!matches!(w.1, Some(zcosmic_workspace_handle_v1::State::Hidden))
});
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,
});
list.retain(|w| !w.state.contains(ext_workspace_handle_v1::State::Hidden));
list.sort_by(|w1, w2| w1.coordinates.cmp(&w2.coordinates));
self.workspaces = list;
}
WorkspacesUpdate::Started(tx) => {
@ -152,8 +161,55 @@ impl cosmic::Application for IcedWorkspacesApplet {
ScrollDelta::Lines { x, y } => ((x + y) as f64, false),
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 => {
@ -177,11 +233,7 @@ impl cosmic::Application for IcedWorkspacesApplet {
let popup_index = self.popup_index().unwrap_or(self.workspaces.len());
let buttons = self.workspaces[..popup_index].iter().filter_map(|w| {
let content = self
.core
.applet
.text(w.0.clone())
.font(cosmic::font::bold());
let content = self.core.applet.text(&w.name).font(cosmic::font::bold());
let (width, height) = if self.core.applet.is_horizontal() {
(suggested_total as f32, suggested_window_size.1.get() as f32)
@ -205,18 +257,20 @@ impl cosmic::Application for IcedWorkspacesApplet {
} else {
[self.core.applet.suggested_padding(true), 0]
})
.on_press(match w.1 {
Some(zcosmic_workspace_handle_v1::State::Active) => Message::WorkspaceOverview,
_ => Message::WorkspacePressed(w.2.clone()),
})
.on_press(
if w.state.contains(ext_workspace_handle_v1::State::Active) {
Message::WorkspaceOverview
} else {
Message::WorkspacePressed(w.handle.clone())
},
)
.padding(0);
Some(
btn.class(match w.1 {
Some(zcosmic_workspace_handle_v1::State::Active) => {
btn.class(
if w.state.contains(ext_workspace_handle_v1::State::Active) {
cosmic::theme::iced::Button::Primary
}
Some(zcosmic_workspace_handle_v1::State::Urgent) => {
} else if w.state.contains(ext_workspace_handle_v1::State::Urgent) {
let appearance = |theme: &Theme| {
let cosmic = theme.cosmic();
button::Style {
@ -249,8 +303,7 @@ impl cosmic::Application for IcedWorkspacesApplet {
button::Status::Disabled => appearance(theme),
}
}))
}
None => {
} else {
let appearance = |theme: &Theme| {
let cosmic = theme.cosmic();
button::Style {
@ -282,9 +335,8 @@ impl cosmic::Application for IcedWorkspacesApplet {
}
}
}))
}
_ => return None,
})
},
)
.into(),
)
});

View file

@ -10,35 +10,29 @@ use cctk::{
calloop,
calloop_wayland_source::WaylandSource,
client::{self as wayland_client},
protocols::ext::workspace::v1::client::ext_workspace_handle_v1::ExtWorkspaceHandleV1,
},
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 std::{
os::{
fd::{FromRawFd, RawFd},
unix::net::UnixStream,
},
time::{Duration, Instant},
use std::os::{
fd::{FromRawFd, RawFd},
unix::net::UnixStream,
};
use wayland_client::{
backend::ObjectId,
globals::registry_queue_init,
protocol::wl_output::{self, WlOutput},
Connection, Proxy, QueueHandle, WEnum,
Connection, QueueHandle,
};
#[derive(Debug, Clone)]
pub enum WorkspaceEvent {
Activate(ObjectId),
Scroll(f64, bool),
Activate(ExtWorkspaceHandleV1),
}
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 socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET")
@ -81,100 +75,18 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
tx,
running: true,
have_workspaces: false,
scroll: 0.0,
next_scroll: None,
last_scroll: Instant::now(),
};
let loop_handle = event_loop.handle();
loop_handle
.insert_source(workspaces_rx, |e, _, state| match e {
Event::Msg(WorkspaceEvent::Activate(id)) => {
if let Some(w) = state
Event::Msg(WorkspaceEvent::Activate(handle)) => {
handle.activate();
state
.workspace_state
.workspace_groups()
.iter()
.find_map(|g| g.workspaces.iter().find(|w| w.handle.id() == id))
{
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();
}
}
.workspace_manager()
.get()
.unwrap()
.commit();
}
Event::Closed => {
if let Ok(workspace_manager) =
@ -203,62 +115,30 @@ pub fn spawn_workspaces(tx: mpsc::Sender<WorkspaceList>) -> SyncSender<Workspace
#[derive(Debug)]
pub struct State {
running: bool,
tx: mpsc::Sender<WorkspaceList>,
tx: mpsc::Sender<Vec<Workspace>>,
configured_output: String,
expected_output: Option<WlOutput>,
output_state: OutputState,
registry_state: RegistryState,
workspace_state: WorkspaceState,
have_workspaces: bool,
scroll: f64,
next_scroll: Option<Instant>,
last_scroll: Instant,
}
impl State {
pub fn workspace_list(
&self,
) -> Vec<(String, Option<zcosmic_workspace_handle_v1::State>, ObjectId)> {
pub fn workspace_list(&self) -> Vec<Workspace> {
self.workspace_state
.workspace_groups()
.iter()
.filter_map(|g| {
if g.outputs
.filter(|g| {
g.outputs
.iter()
.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()
}
}

View file

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