mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-08-16 22:31:03 +02:00
Merge pull request #748 from JakeStanger/fix/workspaces
Workspaces module rewrite
This commit is contained in:
commit
df55cdfa9f
9 changed files with 611 additions and 471 deletions
|
@ -8,14 +8,14 @@ Shows all current workspaces. Clicking a workspace changes focus to it.
|
||||||
|
|
||||||
> Type: `workspaces`
|
> Type: `workspaces`
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|----------------|---------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------|---------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
| `name_map` | `Map<string, string or image>` | `{}` | A map of actual workspace names to their display labels/images. Workspaces use their actual name if not present in the map. See [here](images) for information on images. |
|
||||||
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
|
| `favorites` | `Map<string, string[]>` or `string[]` | `[]` | Workspaces to always show. This can be for all monitors, or a map to set per monitor. |
|
||||||
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
|
| `hidden` | `string[]` | `[]` | A list of workspace names to never show |
|
||||||
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
|
||||||
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
| `all_monitors` | `boolean` | `false` | Whether to display workspaces from all monitors. When `false`, only shows workspaces on the current monitor. |
|
||||||
| `sort` | `'added'` or `'alphanumeric'` | `alphanumeric` | The method used for sorting workspaces. `added` always appends to the end, `alphanumeric` sorts by number/name. |
|
| `sort` | `'added'` or `'label'` or `'name'` | `label` | The method used for sorting workspaces. `added` always appends to the end, `label` sorts by displayed value, and `name` sorts by workspace name. |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>JSON</summary>
|
<summary>JSON</summary>
|
||||||
|
|
|
@ -119,7 +119,7 @@ impl Client {
|
||||||
{
|
{
|
||||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||||
} else {
|
} else {
|
||||||
error!("Unable to locate workspace");
|
error!("unable to locate workspace: {workspace_name}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,7 @@ impl Client {
|
||||||
|
|
||||||
event_listener.add_workspace_rename_handler(move |data| {
|
event_listener.add_workspace_rename_handler(move |data| {
|
||||||
let _lock = lock!(lock);
|
let _lock = lock!(lock);
|
||||||
|
debug!("Received workspace rename: {data:?}");
|
||||||
|
|
||||||
send!(
|
send!(
|
||||||
tx,
|
tx,
|
||||||
|
|
|
@ -86,29 +86,25 @@ pub struct Workspace {
|
||||||
pub visibility: Visibility,
|
pub visibility: Visibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused.
|
/// Indicates workspace visibility.
|
||||||
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
|
/// Visible workspaces have a boolean flag to indicate if they are also focused.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum Visibility {
|
pub enum Visibility {
|
||||||
Visible(bool),
|
Visible { focused: bool },
|
||||||
Hidden,
|
Hidden,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visibility {
|
impl Visibility {
|
||||||
pub fn visible() -> Self {
|
pub fn visible() -> Self {
|
||||||
Self::Visible(false)
|
Self::Visible { focused: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn focused() -> Self {
|
pub fn focused() -> Self {
|
||||||
Self::Visible(true)
|
Self::Visible { focused: true }
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_visible(self) -> bool {
|
|
||||||
matches!(self, Self::Visible(_))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_focused(self) -> bool {
|
pub fn is_focused(self) -> bool {
|
||||||
if let Self::Visible(focused) = self {
|
if let Self::Visible { focused } = self {
|
||||||
focused
|
focused
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub struct WidgetGeometry {
|
||||||
pub trait IronbarGtkExt {
|
pub trait IronbarGtkExt {
|
||||||
/// Adds a new CSS class to the widget.
|
/// Adds a new CSS class to the widget.
|
||||||
fn add_class(&self, class: &str);
|
fn add_class(&self, class: &str);
|
||||||
/// Removes a CSS class to the widget.
|
/// Removes a CSS class from the widget
|
||||||
fn remove_class(&self, class: &str);
|
fn remove_class(&self, class: &str);
|
||||||
/// Gets the geometry for the widget
|
/// Gets the geometry for the widget
|
||||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
|
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
|
||||||
|
|
|
@ -1,451 +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);
|
|
||||||
|
|
||||||
button.add_class("item");
|
|
||||||
|
|
||||||
if visibility.is_visible() {
|
|
||||||
button.add_class("visible");
|
|
||||||
}
|
|
||||||
|
|
||||||
if visibility.is_focused() {
|
|
||||||
button.add_class("focused");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visibility.is_visible() {
|
|
||||||
button.add_class("inactive");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let tx = tx.clone();
|
|
||||||
let name = name.to_string();
|
|
||||||
button.connect_clicked(move |button| {
|
|
||||||
if !button.style_context().has_class("focused") {
|
|
||||||
try_send!(tx, name.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
button
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reorder_workspaces(container: >k::Box) {
|
|
||||||
let mut buttons = container
|
|
||||||
.children()
|
|
||||||
.into_iter()
|
|
||||||
.map(|child| {
|
|
||||||
let label = child
|
|
||||||
.downcast_ref::<Button>()
|
|
||||||
.and_then(|button| button.label())
|
|
||||||
.unwrap_or_else(|| child.widget_name())
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
(label, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.sort == SortOrder::Alphanumeric {
|
|
||||||
reorder_workspaces(&container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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::Urgent { id, urgent } => {
|
|
||||||
let button = button_map.get(&id);
|
|
||||||
if let Some(item) = button {
|
|
||||||
if urgent {
|
|
||||||
item.add_class("urgent");
|
|
||||||
} else {
|
|
||||||
item.style_context().remove_class("urgent");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WorkspaceUpdate::Unknown => warn!("Received unknown type workspace event")
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ModuleParts {
|
|
||||||
widget: container,
|
|
||||||
popup: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
78
src/modules/workspaces/button.rs
Normal file
78
src/modules/workspaces/button.rs
Normal 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
btn.set_open_state(open_state);
|
||||||
|
btn
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button(&self) -> &GtkButton {
|
||||||
|
&self.button
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_open_state(&self, open_state: OpenState) {
|
||||||
|
if open_state.is_visible() {
|
||||||
|
self.button.add_class("visible");
|
||||||
|
} else {
|
||||||
|
self.button.remove_class("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
if open_state == OpenState::Focused {
|
||||||
|
self.button.add_class("focused");
|
||||||
|
} else {
|
||||||
|
self.button.remove_class("focused");
|
||||||
|
}
|
||||||
|
|
||||||
|
if open_state == OpenState::Closed {
|
||||||
|
self.button.add_class("inactive");
|
||||||
|
} else {
|
||||||
|
self.button.remove_class("inactive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_urgent(&self, urgent: bool) {
|
||||||
|
if urgent {
|
||||||
|
self.button.add_class("urgent");
|
||||||
|
} else {
|
||||||
|
self.button.remove_class("urgent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_id(&self) -> i64 {
|
||||||
|
self.workspace_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_workspace_id(&mut self, id: i64) {
|
||||||
|
self.workspace_id = id;
|
||||||
|
}
|
||||||
|
}
|
77
src/modules/workspaces/button_map.rs
Normal file
77
src/modules/workspaces/button_map.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
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(&self, id: i64) -> Option<&Button> {
|
||||||
|
self.map.iter().find_map(|(_, button)| {
|
||||||
|
if button.workspace_id() == id {
|
||||||
|
Some(button)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
409
src/modules/workspaces/mod.rs
Normal file
409
src/modules/workspaces/mod.rs
Normal file
|
@ -0,0 +1,409 @@
|
||||||
|
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, Default, 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 the order of their displayed labels,
|
||||||
|
/// accounting for any mappings supplied in `name_map`.
|
||||||
|
/// In most cases, this is likely their number.
|
||||||
|
///
|
||||||
|
/// Workspaces are sorted numerically first,
|
||||||
|
/// and named workspaces are added to the end in alphabetical order.
|
||||||
|
#[default]
|
||||||
|
Label,
|
||||||
|
|
||||||
|
/// Shows workspaces in the order of their real names,
|
||||||
|
/// as supplied by the compositor.
|
||||||
|
/// In most cases, this is likely their number.
|
||||||
|
///
|
||||||
|
/// Workspaces are sorted numerically first,
|
||||||
|
/// and named workspaces are added to the end in alphabetical order.
|
||||||
|
Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.
|
||||||
|
/// - `label` sorts by displayed value.
|
||||||
|
/// - `name` sorts by workspace name.
|
||||||
|
///
|
||||||
|
/// **Valid options**: `added`, `label`, `name`.
|
||||||
|
/// <br>
|
||||||
|
/// **Default**: `label`
|
||||||
|
#[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: >k::Box, sort_order: SortOrder) {
|
||||||
|
let mut buttons = container
|
||||||
|
.children()
|
||||||
|
.into_iter()
|
||||||
|
.map(|child| {
|
||||||
|
let label = if sort_order == SortOrder::Label {
|
||||||
|
child
|
||||||
|
.downcast_ref::<gtk::Button>()
|
||||||
|
.and_then(|button| button.label())
|
||||||
|
.unwrap_or_else(|| child.widget_name())
|
||||||
|
} else {
|
||||||
|
child.widget_name()
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
(label, 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::Added {
|
||||||
|
reorder_workspaces(&container, self.sort);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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::Urgent { id, urgent } => {
|
||||||
|
if let Some(button) = button_map
|
||||||
|
.get(&Identifier::Id(id))
|
||||||
|
.or_else(|| button_map.find_button_by_id(id))
|
||||||
|
{
|
||||||
|
button.set_urgent(urgent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorkspaceUpdate::Unknown => warn!("received unknown type workspace event"),
|
||||||
|
};
|
||||||
|
|
||||||
|
glib_recv!(context.subscribe(), handle_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ModuleParts {
|
||||||
|
widget: container,
|
||||||
|
popup: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
30
src/modules/workspaces/open_state.rs
Normal file
30
src/modules/workspaces/open_state.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue