diff --git a/docs/modules/Workspaces.md b/docs/modules/Workspaces.md index ea25d9b..b740936 100644 --- a/docs/modules/Workspaces.md +++ b/docs/modules/Workspaces.md @@ -103,6 +103,7 @@ end: | `.workspaces` | Workspaces widget box | | `.workspaces .item` | Workspace button | | `.workspaces .item.focused` | Workspace button (workspace focused) | +| `.workspaces .item.visible` | Workspace button (workspace visible, including focused) | | `.workspaces .item.inactive` | Workspace button (favourite, not currently open) | `.workspaces .item .icon` | Workspace button icon (any type) | | `.workspaces .item .text-icon` | Workspace button icon (textual only) | diff --git a/src/clients/compositor/hyprland.rs b/src/clients/compositor/hyprland.rs index 54329f3..699a2f7 100644 --- a/src/clients/compositor/hyprland.rs +++ b/src/clients/compositor/hyprland.rs @@ -1,4 +1,4 @@ -use super::{Workspace, WorkspaceClient, WorkspaceUpdate}; +use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate}; use crate::{arc_mut, lock, send}; use color_eyre::Result; use hyprland::data::{Workspace as HWorkspace, Workspaces}; @@ -52,11 +52,8 @@ impl EventClient { let workspace_name = get_workspace_name(workspace_type); let prev_workspace = lock!(active); - let focused = prev_workspace - .as_ref() - .map_or(false, |w| w.name == workspace_name); - let workspace = Self::get_workspace(&workspace_name, focused); + let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); if let Some(workspace) = workspace { send!(tx, WorkspaceUpdate::Add(workspace)); @@ -80,10 +77,7 @@ impl EventClient { ); let workspace_name = get_workspace_name(workspace_type); - let focused = prev_workspace - .as_ref() - .map_or(false, |w| w.name == workspace_name); - let workspace = Self::get_workspace(&workspace_name, focused); + let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); workspace.map_or_else( || { @@ -92,7 +86,7 @@ impl EventClient { |workspace| { // there may be another type of update so dispatch that regardless of focus change send!(tx, WorkspaceUpdate::Update(workspace.clone())); - if !focused { + if !workspace.visibility.is_focused() { Self::send_focus_change(&mut prev_workspace, workspace, &tx); } }, @@ -117,12 +111,11 @@ impl EventClient { ); let workspace_name = get_workspace_name(workspace_type); - let focused = prev_workspace - .as_ref() - .map_or(false, |w| w.name == workspace_name); - let workspace = Self::get_workspace(&workspace_name, focused); + let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); - if let (Some(workspace), false) = (workspace, focused) { + if let Some((false, workspace)) = + workspace.map(|w| (w.visibility.is_focused(), w)) + { Self::send_focus_change(&mut prev_workspace, workspace, &tx); } else { error!("Unable to locate workspace"); @@ -142,15 +135,12 @@ impl EventClient { let mut prev_workspace = lock!(active); let workspace_name = get_workspace_name(workspace_type); - let focused = prev_workspace - .as_ref() - .map_or(false, |w| w.name == workspace_name); - let workspace = Self::get_workspace(&workspace_name, focused); + let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref()); if let Some(workspace) = workspace { send!(tx, WorkspaceUpdate::Move(workspace.clone())); - if !focused { + if !workspace.visibility.is_focused() { Self::send_focus_change(&mut prev_workspace, workspace, &tx); } } @@ -180,32 +170,28 @@ impl EventClient { workspace: Workspace, tx: &Sender, ) { - let old = prev_workspace - .as_ref() - .map(|w| w.name.clone()) - .unwrap_or_default(); - send!( tx, WorkspaceUpdate::Focus { - old, - new: workspace.name.clone(), + old: prev_workspace.take(), + new: workspace.clone(), } ); prev_workspace.replace(workspace); } - /// Gets a workspace by name from the server. - /// - /// Use `focused` to manually mark the workspace as focused, - /// as this is not automatically checked. - fn get_workspace(name: &str, focused: bool) -> Option { + /// Gets a workspace by name from the server, given the active workspace if known. + fn get_workspace(name: &str, active: Option<&Workspace>) -> Option { Workspaces::get() .expect("Failed to get workspaces") .find_map(|w| { if w.name == name { - Some(Workspace::from((focused, w))) + let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| { + create_is_visible()(w) + })); + + Some(Workspace::from((vis, w))) } else { None } @@ -214,7 +200,7 @@ impl EventClient { /// Gets the active workspace from the server. fn get_active_workspace() -> Result { - let w = HWorkspace::get_active().map(|w| Workspace::from((true, w)))?; + let w = HWorkspace::get_active().map(|w| Workspace::from((Visibility::focused(), w)))?; Ok(w) } } @@ -236,13 +222,16 @@ impl WorkspaceClient for EventClient { { let tx = self.workspace_tx.clone(); - let active_name = HWorkspace::get_active() - .map(|active| active.name) - .unwrap_or_default(); + let active_id = HWorkspace::get_active().ok().map(|active| active.name); + let is_visible = create_is_visible(); let workspaces = Workspaces::get() .expect("Failed to get workspaces") - .map(|w| Workspace::from((w.name == active_name, w))) + .map(|w| { + let vis = Visibility::from((&w, active_id.as_deref(), &is_visible)); + + Workspace::from((vis, w)) + }) .collect(); send!(tx, WorkspaceUpdate::Init(workspaces)); @@ -271,13 +260,36 @@ fn get_workspace_name(name: WorkspaceType) -> String { } } -impl From<(bool, hyprland::data::Workspace)> for Workspace { - fn from((focused, workspace): (bool, hyprland::data::Workspace)) -> Self { +/// Creates a function which determines if a workspace is visible. This function makes a Hyprland call that allocates so it should be cached when possible, but it is only valid so long as workspaces do not change so it should not be stored long term +fn create_is_visible() -> impl Fn(&HWorkspace) -> bool { + let monitors = hyprland::data::Monitors::get().map_or(Vec::new(), |ms| ms.to_vec()); + + move |w| monitors.iter().any(|m| m.active_workspace.id == w.id) +} + +impl From<(Visibility, HWorkspace)> for Workspace { + fn from((visibility, workspace): (Visibility, HWorkspace)) -> Self { Self { id: workspace.id.to_string(), name: workspace.name, monitor: workspace.monitor, - focused, + visibility, + } + } +} + +impl<'a, 'f, F> From<(&'a HWorkspace, Option<&str>, F)> for Visibility +where + F: FnOnce(&'f HWorkspace) -> bool, + 'a: 'f, +{ + fn from((workspace, active_name, is_visible): (&'a HWorkspace, Option<&str>, F)) -> Self { + if Some(workspace.name.as_str()) == active_name { + Self::focused() + } else if is_visible(workspace) { + Self::visible() + } else { + Self::Hidden } } } diff --git a/src/clients/compositor/mod.rs b/src/clients/compositor/mod.rs index 953f683..484d360 100644 --- a/src/clients/compositor/mod.rs +++ b/src/clients/compositor/mod.rs @@ -75,8 +75,38 @@ pub struct Workspace { pub name: String, /// Name of the monitor (output) the workspace is located on pub monitor: String, - /// Whether the workspace is in focus - pub focused: bool, + /// How visible the workspace is + pub visibility: Visibility, +} + +/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused. +/// Yes, this is the same signature as Option, but it's impl is a lot more suited for our case. +#[derive(Debug, Copy, Clone)] +pub enum Visibility { + Visible(bool), + Hidden, +} + +impl Visibility { + pub fn visible() -> Self { + Self::Visible(false) + } + + pub fn focused() -> Self { + Self::Visible(true) + } + + pub fn is_visible(self) -> bool { + matches!(self, Self::Visible(_)) + } + + pub fn is_focused(self) -> bool { + if let Self::Visible(focused) = self { + focused + } else { + false + } + } } #[derive(Debug, Clone)] @@ -90,8 +120,8 @@ pub enum WorkspaceUpdate { Move(Workspace), /// Declares focus moved from the old workspace to the new. Focus { - old: String, - new: String, + old: Option, + new: Workspace, }, } diff --git a/src/clients/compositor/sway.rs b/src/clients/compositor/sway.rs index 1f6f0b3..cde04e5 100644 --- a/src/clients/compositor/sway.rs +++ b/src/clients/compositor/sway.rs @@ -1,4 +1,4 @@ -use super::{Workspace, WorkspaceClient, WorkspaceUpdate}; +use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate}; use crate::{await_sync, send}; use async_once::AsyncOnce; use color_eyre::Report; @@ -105,22 +105,50 @@ pub fn get_sub_client() -> &'static SwayEventClient { impl From for Workspace { fn from(node: Node) -> Self { + let visibility = Visibility::from(&node); + Self { id: node.id.to_string(), name: node.name.unwrap_or_default(), monitor: node.output.unwrap_or_default(), - focused: node.focused, + visibility, } } } impl From for Workspace { fn from(workspace: swayipc_async::Workspace) -> Self { + let visibility = Visibility::from(&workspace); + Self { id: workspace.id.to_string(), name: workspace.name, monitor: workspace.output, - focused: workspace.focused, + visibility, + } + } +} + +impl From<&Node> for Visibility { + fn from(node: &Node) -> Self { + if node.focused { + Self::focused() + } else if node.visible.unwrap_or(false) { + Self::visible() + } else { + Self::Hidden + } + } +} + +impl From<&swayipc_async::Workspace> for Visibility { + fn from(workspace: &swayipc_async::Workspace) -> Self { + if workspace.focused { + Self::focused() + } else if workspace.visible { + Self::visible() + } else { + Self::Hidden } } } @@ -139,16 +167,8 @@ impl From for WorkspaceUpdate { .unwrap_or_default(), ), WorkspaceChange::Focus => Self::Focus { - old: event - .old - .expect("Missing old workspace") - .name - .unwrap_or_default(), - new: event - .current - .expect("Missing current workspace") - .name - .unwrap_or_default(), + old: event.old.map(Workspace::from), + new: Workspace::from(event.current.expect("Missing current workspace")), }, WorkspaceChange::Move => { Self::Move(event.current.expect("Missing current workspace").into()) diff --git a/src/modules/workspaces.rs b/src/modules/workspaces.rs index 2d415e7..0b5effb 100644 --- a/src/modules/workspaces.rs +++ b/src/modules/workspaces.rs @@ -1,4 +1,4 @@ -use crate::clients::compositor::{Compositor, Workspace, WorkspaceUpdate}; +use crate::clients::compositor::{Compositor, Visibility, Workspace, WorkspaceUpdate}; use crate::config::CommonConfig; use crate::image::new_icon_button; use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; @@ -76,8 +76,7 @@ const fn default_icon_size() -> i32 { /// Creates a button from a workspace fn create_button( name: &str, - focused: bool, - inactive: bool, + visibility: Visibility, name_map: &HashMap, icon_theme: &IconTheme, icon_size: i32, @@ -91,10 +90,12 @@ fn create_button( let style_context = button.style_context(); style_context.add_class("item"); - if focused { + if visibility.is_visible() { + style_context.add_class("visible"); + } + + if visibility.is_focused() { style_context.add_class("focused"); - } else if inactive { - style_context.add_class("inactive"); } { @@ -131,7 +132,7 @@ fn reorder_workspaces(container: >k::Box) { impl WorkspacesModule { fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool { - (work.focused || !self.hidden.contains(&work.name)) + (work.visibility.is_focused() || !self.hidden.contains(&work.name)) && (self.all_monitors || output == &work.monitor) } } @@ -212,11 +213,10 @@ impl Module for WorkspacesModule { let mut added = HashSet::new(); - let mut add_workspace = |name: &str, focused: bool| { + let mut add_workspace = |name: &str, visibility: Visibility| { let item = create_button( name, - focused, - false, + visibility, &name_map, &icon_theme, icon_size, @@ -230,7 +230,7 @@ impl Module for WorkspacesModule { // add workspaces from client for workspace in &workspaces { if self.show_workspace_check(&output_name, workspace) { - add_workspace(&workspace.name, workspace.focused); + add_workspace(&workspace.name, workspace.visibility); added.insert(workspace.name.to_string()); } } @@ -238,7 +238,7 @@ impl Module for WorkspacesModule { let mut add_favourites = |names: &Vec| { for name in names { if !added.contains(name) { - add_workspace(name, false); + add_workspace(name, Visibility::Hidden); added.insert(name.to_string()); fav_names.push(name.to_string()); } @@ -264,14 +264,20 @@ impl Module for WorkspacesModule { } } WorkspaceUpdate::Focus { old, new } => { - let old = button_map.get(&old); - if let Some(old) = old { - old.style_context().remove_class("focused"); + if let Some(btn) = old.as_ref().and_then(|w| button_map.get(&w.name)) { + if Some(new.monitor) == old.map(|w| w.monitor) { + btn.style_context().remove_class("visible"); + } + + btn.style_context().remove_class("focused"); } - let new = button_map.get(&new); - if let Some(new) = new { - new.style_context().add_class("focused"); + let new = button_map.get(&new.name); + if let Some(btn) = new { + let style = btn.style_context(); + + style.add_class("visible"); + style.add_class("focused"); } } WorkspaceUpdate::Add(workspace) => { @@ -284,8 +290,7 @@ impl Module for WorkspacesModule { let name = workspace.name; let item = create_button( &name, - workspace.focused, - false, + workspace.visibility, &name_map, &icon_theme, icon_size, @@ -310,8 +315,7 @@ impl Module for WorkspacesModule { let name = workspace.name; let item = create_button( &name, - workspace.focused, - false, + workspace.visibility, &name_map, &icon_theme, icon_size,