fix(workspaces): rewrite module to fix several small issues

Rewrites the module code to be better structured, in a similar pattern to the launcher. The code is now more robust and more maintainable, yay!

Fixes #705

Fixes an issue with moving favourite workspaces.

Fixes an issue with workspace visible state being incorrect.

Fixes an issue where the `inactive` class looked at hidden instead of closed favourites.
This commit is contained in:
Jake Stanger 2024-10-19 00:07:49 +01:00
parent c1e8b26749
commit 2cd2d255d4
No known key found for this signature in database
GPG Key ID: C51FC8F9CB0BEA61
7 changed files with 561 additions and 436 deletions

View File

@ -90,25 +90,21 @@ pub struct Workspace {
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
#[derive(Debug, Copy, Clone)]
pub enum Visibility {
Visible(bool),
Visible { focused: bool },
Hidden,
}
impl Visibility {
pub fn visible() -> Self {
Self::Visible(false)
Self::Visible { focused: false }
}
pub fn focused() -> Self {
Self::Visible(true)
}
pub fn is_visible(self) -> bool {
matches!(self, Self::Visible(_))
Self::Visible { focused: true }
}
pub fn is_focused(self) -> bool {
if let Self::Visible(focused) = self {
if let Self::Visible { focused } = self {
focused
} else {
false

View File

@ -18,6 +18,9 @@ pub struct WidgetGeometry {
pub trait IronbarGtkExt {
/// Adds a new CSS class to the widget.
fn add_class(&self, class: &str);
/// Removes a CSS class from the widget
fn remove_class(&self, class: &str);
/// Gets the geometry for the widget
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
@ -32,6 +35,10 @@ impl<W: IsA<Widget>> IronbarGtkExt for W {
self.style_context().add_class(class);
}
fn remove_class(&self, class: &str) {
self.style_context().remove_class(class);
}
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
let allocation = self.allocation();

View File

@ -1,428 +0,0 @@
use crate::clients::compositor::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, module_impl, send_async, spawn, try_send, Ironbar};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{Button, IconTheme};
use serde::Deserialize;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, trace, warn};
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum SortOrder {
/// Shows workspaces in the order they're added
Added,
/// Shows workspaces in numeric order.
/// Named workspaces are added to the end in alphabetical order.
Alphanumeric,
}
impl Default for SortOrder {
fn default() -> Self {
Self::Alphanumeric
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Favorites {
ByMonitor(HashMap<String, Vec<String>>),
Global(Vec<String>),
}
impl Default for Favorites {
fn default() -> Self {
Self::Global(vec![])
}
}
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WorkspacesModule {
/// Map of actual workspace names to custom names.
///
/// Custom names can be [images](images).
///
/// If a workspace is not present in the map,
/// it will fall back to using its actual name.
name_map: Option<HashMap<String, String>>,
/// Workspaces which should always be shown.
/// This can either be an array of workspace names,
/// or a map of monitor names to arrays of workspace names.
///
/// **Default**: `{}`
///
/// # Example
///
/// ```corn
/// // array format
/// {
/// type = "workspaces"
/// favorites = ["1", "2", "3"]
/// }
///
/// // map format
/// {
/// type = "workspaces"
/// favorites.DP-1 = ["1", "2", "3"]
/// favorites.DP-2 = ["4", "5", "6"]
/// }
/// ```
#[serde(default)]
favorites: Favorites,
/// A list of workspace names to never show.
///
/// This may be useful for scratchpad/special workspaces, for example.
///
/// **Default**: `[]`
#[serde(default)]
hidden: Vec<String>,
/// Whether to display workspaces from all monitors.
/// When false, only shows workspaces on the current monitor.
///
/// **Default**: `false`
#[serde(default = "crate::config::default_false")]
all_monitors: bool,
/// The method used for sorting workspaces.
/// `added` always appends to the end, `alphanumeric` sorts by number/name.
///
/// **Valid options**: `added`, `alphanumeric`
/// <br>
/// **Default**: `alphanumeric`
#[serde(default)]
sort: SortOrder,
/// The size to render icons at (image icons only).
///
/// **Default**: `32`
#[serde(default = "default_icon_size")]
icon_size: i32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_icon_size() -> i32 {
32
}
/// Creates a button from a workspace
fn create_button(
name: &str,
visibility: Visibility,
name_map: &HashMap<String, String>,
icon_theme: &IconTheme,
icon_size: i32,
tx: &Sender<String>,
) -> Button {
let label = name_map.get(name).map_or(name, String::as_str);
let button = new_icon_button(label, icon_theme, icon_size);
button.set_widget_name(name);
let style_context = button.style_context();
style_context.add_class("item");
if visibility.is_visible() {
style_context.add_class("visible");
}
if visibility.is_focused() {
style_context.add_class("focused");
}
if !visibility.is_visible() {
style_context.add_class("inactive");
}
{
let tx = tx.clone();
let name = name.to_string();
button.connect_clicked(move |_item| {
try_send!(tx, name.clone());
});
}
button
}
fn reorder_workspaces(container: &gtk::Box) {
let mut buttons = container
.children()
.into_iter()
.map(|child| (child.widget_name().to_string(), child))
.collect::<Vec<_>>();
buttons.sort_by(|(label_a, _), (label_b, _a)| {
match (label_a.parse::<i32>(), label_b.parse::<i32>()) {
(Ok(a), Ok(b)) => a.cmp(&b),
(Ok(_), Err(_)) => Ordering::Less,
(Err(_), Ok(_)) => Ordering::Greater,
(Err(_), Err(_)) => label_a.cmp(label_b),
}
});
for (i, (_, button)) in buttons.into_iter().enumerate() {
container.reorder_child(&button, i as i32);
}
}
fn find_btn(map: &HashMap<i64, Button>, workspace: &Workspace) -> Option<Button> {
map.get(&workspace.id)
.or_else(|| {
map.values()
.find(|&btn| btn.widget_name() == workspace.name)
})
.cloned()
}
impl WorkspacesModule {
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
(work.visibility.is_focused() || !self.hidden.contains(&work.name))
&& (self.all_monitors || output == &work.monitor)
}
}
impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String;
module_impl!("workspaces");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces()?;
// Subscribe & send events
spawn(async move {
let mut srx = client.subscribe_workspace_change();
trace!("Set up workspace subscription");
while let Ok(payload) = srx.recv().await {
debug!("Received update: {payload:?}");
send_async!(tx, ModuleUpdateEvent::Update(payload));
}
});
let client = context.try_client::<dyn WorkspaceClient>()?;
// Change workspace focus
spawn(async move {
trace!("Setting up UI event handler");
while let Some(name) = rx.recv().await {
if let Err(e) = client.focus(name.clone()) {
warn!("Couldn't focus workspace '{name}': {e:#}");
};
}
Ok::<(), Report>(())
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.orientation(), 0);
let name_map = self.name_map.clone().unwrap_or_default();
let favs = self.favorites.clone();
let mut fav_names: Vec<String> = vec![];
let mut button_map: HashMap<i64, Button> = HashMap::new();
{
let container = container.clone();
let output_name = info.output_name.to_string();
let icon_theme = info.icon_theme.clone();
let icon_size = self.icon_size;
// keep track of whether init event has fired previously
// since it fires for every workspace subscriber
let mut has_initialized = false;
glib_recv!(context.subscribe(), event => {
match event {
WorkspaceUpdate::Init(workspaces) => {
if !has_initialized {
trace!("Creating workspace buttons");
let mut added = HashSet::new();
let mut add_workspace = |id: i64, name: &str, visibility: Visibility| {
let item = create_button(
name,
visibility,
&name_map,
&icon_theme,
icon_size,
&context.controller_tx,
);
container.add(&item);
button_map.insert(id, item);
};
// add workspaces from client
for workspace in &workspaces {
if self.show_workspace_check(&output_name, workspace) {
add_workspace(workspace.id, &workspace.name, workspace.visibility);
added.insert(workspace.name.to_string());
}
}
let mut add_favourites = |names: &Vec<String>| {
for name in names {
fav_names.push(name.to_string());
if !added.contains(name) {
// Favourites are added with the same name and ID
// as Hyprland will initialize them this way.
// Since existing workspaces are added above,
// this means there shouldn't be any issues with renaming.
add_workspace(-(Ironbar::unique_id() as i64), name, Visibility::Hidden);
added.insert(name.to_string());
}
}
};
// add workspaces from favourites
match &favs {
Favorites::Global(names) => add_favourites(names),
Favorites::ByMonitor(map) => {
if let Some(to_add) = map.get(&output_name) {
add_favourites(to_add);
}
}
}
if self.sort == SortOrder::Alphanumeric {
reorder_workspaces(&container);
}
container.show_all();
has_initialized = true;
}
}
WorkspaceUpdate::Focus { old, new } => {
if let Some(btn) = old.as_ref().and_then(|w| find_btn(&button_map, w)) {
if Some(new.monitor.as_str()) == old.as_ref().map(|w| w.monitor.as_str()) {
btn.style_context().remove_class("visible");
}
btn.style_context().remove_class("focused");
}
if let Some(btn) = find_btn(&button_map, &new) {
btn.add_class("visible");
btn.add_class("focused");
}
}
WorkspaceUpdate::Rename { id, name } => {
if let Some(btn) = button_map.get(&id) {
let name = name_map.get(&name).unwrap_or(&name);
btn.set_label(name);
}
}
WorkspaceUpdate::Add(workspace) => {
if fav_names.contains(&workspace.name) {
let btn = button_map.get(&workspace.id);
if let Some(btn) = btn {
btn.style_context().remove_class("inactive");
}
} else if self.show_workspace_check(&output_name, &workspace) {
let name = workspace.name;
let item = create_button(
&name,
workspace.visibility,
&name_map,
&icon_theme,
icon_size,
&context.controller_tx,
);
container.add(&item);
if self.sort == SortOrder::Alphanumeric {
reorder_workspaces(&container);
}
item.show();
if !name.is_empty() {
button_map.insert(workspace.id, item);
}
}
}
WorkspaceUpdate::Move(workspace) => {
if !self.hidden.contains(&workspace.name) && !self.all_monitors {
if workspace.monitor == output_name {
let name = workspace.name;
let item = create_button(
&name,
workspace.visibility,
&name_map,
&icon_theme,
icon_size,
&context.controller_tx,
);
container.add(&item);
if self.sort == SortOrder::Alphanumeric {
reorder_workspaces(&container);
}
item.show();
if !name.is_empty() {
button_map.insert(workspace.id, item);
}
} else if let Some(item) = button_map.get(&workspace.id) {
container.remove(item);
}
}
}
WorkspaceUpdate::Remove(workspace) => {
let button = button_map.get(&workspace);
if let Some(item) = button {
if workspace < 0 {
// if fav_names.contains(&workspace) {
item.style_context().add_class("inactive");
} else {
container.remove(item);
}
}
}
WorkspaceUpdate::Unknown => warn!("Received unknown type workspace event")
};
});
}
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}

View File

@ -0,0 +1,78 @@
use super::open_state::OpenState;
use crate::gtk_helpers::IronbarGtkExt;
use crate::image::new_icon_button;
use crate::modules::workspaces::WorkspaceItemContext;
use crate::try_send;
use gtk::prelude::*;
use gtk::Button as GtkButton;
#[derive(Debug, Clone)]
pub struct Button {
button: GtkButton,
workspace_id: i64,
open_state: OpenState,
}
impl Button {
pub fn new(id: i64, name: &str, open_state: OpenState, context: &WorkspaceItemContext) -> Self {
let label = context.name_map.get(name).map_or(name, String::as_str);
let button = new_icon_button(label, &context.icon_theme, context.icon_size);
button.set_widget_name(name);
button.add_class("item");
let tx = context.tx.clone();
let name = name.to_string();
button.connect_clicked(move |_item| {
try_send!(tx, name.clone());
});
let btn = Self {
button,
workspace_id: id,
open_state,
};
btn.update_classes();
btn
}
pub fn button(&self) -> &GtkButton {
&self.button
}
pub fn set_open_state(&mut self, open_state: OpenState) {
self.open_state = open_state;
self.update_classes();
}
pub fn workspace_id(&self) -> i64 {
self.workspace_id
}
pub fn set_workspace_id(&mut self, id: i64) {
self.workspace_id = id;
}
/// Adds/removes styling classes according to the open state.
pub fn update_classes(&self) {
if self.open_state.is_visible() {
self.button.add_class("visible");
} else {
self.button.remove_class("visible");
}
if self.open_state == OpenState::Focused {
self.button.add_class("focused");
} else {
self.button.remove_class("focused");
}
if self.open_state == OpenState::Closed {
self.button.add_class("inactive");
} else {
self.button.remove_class("inactive");
}
}
}

View File

@ -0,0 +1,64 @@
use super::button::Button;
use crate::clients::compositor::Workspace;
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Identifier {
Id(i64),
Name(String),
}
/// Wrapper around a hashmap of workspace buttons,
/// which can be found using the workspace ID,
/// or their name for favourites.
pub struct ButtonMap {
map: HashMap<Identifier, Button>,
}
impl ButtonMap {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
/// Gets the button for a workspace,
/// checking the map for both its ID and name.
pub fn find_button_mut(&mut self, workspace: &Workspace) -> Option<&mut Button> {
let id = Identifier::Id(workspace.id);
if self.map.contains_key(&id) {
self.map.get_mut(&id)
} else {
self.map.get_mut(&Identifier::Name(workspace.name.clone()))
}
}
/// Gets the button for a workspace,
/// performing a search of all keys for the button
/// with the associated workspace ID.
pub fn find_button_by_id_mut(&mut self, id: i64) -> Option<&mut Button> {
self.map.iter_mut().find_map(|(_, button)| {
if button.workspace_id() == id {
Some(button)
} else {
None
}
})
}
}
impl Deref for ButtonMap {
type Target = HashMap<Identifier, Button>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl DerefMut for ButtonMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}

View File

@ -0,0 +1,378 @@
mod button;
mod button_map;
mod open_state;
use self::button::Button;
use crate::clients::compositor::{Workspace, WorkspaceClient, WorkspaceUpdate};
use crate::config::CommonConfig;
use crate::modules::workspaces::button_map::{ButtonMap, Identifier};
use crate::modules::workspaces::open_state::OpenState;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, module_impl, send_async, spawn};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::IconTheme;
use serde::Deserialize;
use std::cmp::Ordering;
use std::collections::HashMap;
use tokio::sync::mpsc;
use tracing::{debug, trace, warn};
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum SortOrder {
/// Shows workspaces in the order they're added
Added,
/// Shows workspaces in numeric order.
/// Named workspaces are added to the end in alphabetical order.
Alphanumeric,
}
impl Default for SortOrder {
fn default() -> Self {
Self::Alphanumeric
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Favorites {
ByMonitor(HashMap<String, Vec<String>>),
Global(Vec<String>),
}
impl Default for Favorites {
fn default() -> Self {
Self::Global(vec![])
}
}
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WorkspacesModule {
/// Map of actual workspace names to custom names.
///
/// Custom names can be [images](images).
///
/// If a workspace is not present in the map,
/// it will fall back to using its actual name.
name_map: Option<HashMap<String, String>>,
/// Workspaces which should always be shown.
/// This can either be an array of workspace names,
/// or a map of monitor names to arrays of workspace names.
///
/// **Default**: `{}`
///
/// # Example
///
/// ```corn
/// // array format
/// {
/// type = "workspaces"
/// favorites = ["1", "2", "3"]
/// }
///
/// // map format
/// {
/// type = "workspaces"
/// favorites.DP-1 = ["1", "2", "3"]
/// favorites.DP-2 = ["4", "5", "6"]
/// }
/// ```
#[serde(default)]
favorites: Favorites,
/// A list of workspace names to never show.
///
/// This may be useful for scratchpad/special workspaces, for example.
///
/// **Default**: `[]`
#[serde(default)]
hidden: Vec<String>,
/// Whether to display workspaces from all monitors.
/// When false, only shows workspaces on the current monitor.
///
/// **Default**: `false`
#[serde(default = "crate::config::default_false")]
all_monitors: bool,
/// The method used for sorting workspaces.
/// `added` always appends to the end, `alphanumeric` sorts by number/name.
///
/// **Valid options**: `added`, `alphanumeric`
/// <br>
/// **Default**: `alphanumeric`
#[serde(default)]
sort: SortOrder,
/// The size to render icons at (image icons only).
///
/// **Default**: `32`
#[serde(default = "default_icon_size")]
icon_size: i32,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
const fn default_icon_size() -> i32 {
32
}
#[derive(Debug, Clone)]
pub struct WorkspaceItemContext {
name_map: HashMap<String, String>,
icon_theme: IconTheme,
icon_size: i32,
tx: mpsc::Sender<String>,
}
/// Re-orders the container children alphabetically,
/// using their widget names.
///
/// Named workspaces are always sorted before numbered ones.
fn reorder_workspaces(container: &gtk::Box) {
let mut buttons = container
.children()
.into_iter()
.map(|child| (child.widget_name().to_string(), child))
.collect::<Vec<_>>();
buttons.sort_by(|(label_a, _), (label_b, _a)| {
match (label_a.parse::<i32>(), label_b.parse::<i32>()) {
(Ok(a), Ok(b)) => a.cmp(&b),
(Ok(_), Err(_)) => Ordering::Less,
(Err(_), Ok(_)) => Ordering::Greater,
(Err(_), Err(_)) => label_a.cmp(label_b),
}
});
for (i, (_, button)) in buttons.into_iter().enumerate() {
container.reorder_child(&button, i as i32);
}
}
impl Module<gtk::Box> for WorkspacesModule {
type SendMessage = WorkspaceUpdate;
type ReceiveMessage = String;
module_impl!("workspaces");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> Result<()> {
let tx = context.tx.clone();
let client = context.ironbar.clients.borrow_mut().workspaces()?;
// Subscribe & send events
spawn(async move {
let mut srx = client.subscribe_workspace_change();
trace!("Set up workspace subscription");
while let Ok(payload) = srx.recv().await {
debug!("Received update: {payload:?}");
send_async!(tx, ModuleUpdateEvent::Update(payload));
}
});
let client = context.try_client::<dyn WorkspaceClient>()?;
// Change workspace focus
spawn(async move {
trace!("Setting up UI event handler");
while let Some(name) = rx.recv().await {
if let Err(e) = client.focus(name.clone()) {
warn!("Couldn't focus workspace '{name}': {e:#}");
};
}
Ok::<(), Report>(())
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.orientation(), 0);
let name_map = self.name_map.clone().unwrap_or_default();
let mut button_map = ButtonMap::new();
let item_context = WorkspaceItemContext {
name_map,
icon_theme: info.icon_theme.clone(),
icon_size: self.icon_size,
tx: context.controller_tx.clone(),
};
// setup favorites
let favorites = match self.favorites {
Favorites::ByMonitor(map) => map.get(info.output_name).cloned(),
Favorites::Global(vec) => Some(vec),
}
.unwrap_or_default();
for favorite in &favorites {
let btn = Button::new(-1, favorite, OpenState::Closed, &item_context);
container.add(btn.button());
button_map.insert(Identifier::Name(favorite.clone()), btn);
}
{
let container = container.clone();
let output_name = info.output_name.to_string();
// keep track of whether init event has fired previously
// since it fires for every workspace subscriber
let mut has_initialized = false;
let add_workspace = {
let container = container.clone();
move |workspace: Workspace, button_map: &mut ButtonMap| {
if favorites.contains(&workspace.name) {
let btn = button_map
.get_mut(&Identifier::Name(workspace.name))
.expect("favorite to exist");
// set an ID to track the open workspace for the favourite
btn.set_workspace_id(workspace.id);
btn.set_open_state(workspace.visibility.into());
} else {
let btn = Button::new(
workspace.id,
&workspace.name,
workspace.visibility.into(),
&item_context,
);
container.add(btn.button());
btn.button().show();
button_map.insert(Identifier::Id(workspace.id), btn);
}
}
};
let remove_workspace = {
let container = container.clone();
move |id: i64, button_map: &mut ButtonMap| {
// since favourites use name identifiers,
// we can safely remove using ID here and favourites will remain
if let Some(button) = button_map.remove(&Identifier::Id(id)) {
container.remove(button.button());
} else {
// otherwise we do a deep search and use the button's cached ID
if let Some(button) = button_map.find_button_by_id_mut(id) {
button.set_workspace_id(-1);
button.set_open_state(OpenState::Closed);
}
}
}
};
macro_rules! reorder {
() => {
if self.sort == SortOrder::Alphanumeric {
reorder_workspaces(&container);
}
};
}
let mut handle_event = move |event: WorkspaceUpdate| match event {
WorkspaceUpdate::Init(workspaces) => {
if has_initialized {
return;
}
trace!("Creating workspace buttons");
for workspace in workspaces
.into_iter()
.filter(|w| self.all_monitors || w.monitor == output_name)
.filter(|w| !self.hidden.contains(&w.name))
{
add_workspace(workspace, &mut button_map);
}
reorder!();
has_initialized = true;
}
WorkspaceUpdate::Add(workspace) => {
if !self.hidden.contains(&workspace.name)
&& (self.all_monitors || workspace.monitor == output_name)
{
add_workspace(workspace, &mut button_map);
}
reorder!();
}
WorkspaceUpdate::Remove(id) => remove_workspace(id, &mut button_map),
WorkspaceUpdate::Move(workspace) => {
if self.all_monitors {
return;
}
if workspace.monitor == output_name && !self.hidden.contains(&workspace.name) {
add_workspace(workspace, &mut button_map);
reorder!();
} else {
remove_workspace(workspace.id, &mut button_map);
}
}
WorkspaceUpdate::Focus { old, new } => {
// Open states are calculated here rather than using the workspace visibility
// as that seems to come back wrong, at least on Hyprland.
// Likely a deeper issue that needs exploring.
if let Some(old) = old {
if let Some(button) = button_map.find_button_mut(&old) {
let open_state = if new.monitor == old.monitor {
OpenState::Hidden
} else {
OpenState::Visible
};
button.set_open_state(open_state);
}
}
if let Some(button) = button_map.find_button_mut(&new) {
button.set_open_state(OpenState::Focused);
}
}
WorkspaceUpdate::Rename { id, name } => {
if let Some(button) = button_map
.get(&Identifier::Id(id))
.or_else(|| button_map.get(&Identifier::Name(name.clone())))
.map(Button::button)
{
button.set_label(&name);
button.set_widget_name(&name);
}
}
WorkspaceUpdate::Unknown => warn!("received unknown type workspace event"),
};
glib_recv!(context.subscribe(), handle_event);
}
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}

View File

@ -0,0 +1,30 @@
use crate::clients::compositor::Visibility;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum OpenState {
/// A favourite workspace, which is not currently open
Closed,
/// A workspace which is open but not visible on any monitors.
Hidden,
/// A workspace which is visible, but not focused.
Visible,
/// The currently active workspace.
Focused,
}
impl From<Visibility> for OpenState {
fn from(value: Visibility) -> Self {
match value {
Visibility::Visible { focused: true } => Self::Focused,
Visibility::Visible { focused: false } => Self::Visible,
Visibility::Hidden => Self::Hidden,
}
}
}
impl OpenState {
/// Whether the workspace is visible, including focused state.
pub fn is_visible(self) -> bool {
matches!(self, OpenState::Visible | OpenState::Focused)
}
}