diff --git a/docs/modules/Tray.md b/docs/modules/Tray.md index e154d8bb..7d976f7a 100644 --- a/docs/modules/Tray.md +++ b/docs/modules/Tray.md @@ -11,6 +11,8 @@ Displays a fully interactive icon tray using the KDE `libappindicator` and `com. | `direction` | `'horizontal'` or `'vertical'` (shorthand: `'h'` or `'v'`) | Matches bar orientation | The direction in which to pack tray icons. | | `icon_size` | `integer` | `16` | Size in pixels to display tray icons as. | | `prefer_theme_icons` | `bool` | `true` | Requests that icons from the theme be used over the item-provided item. Most items only provide one or the other so this will have no effect in most circumstances. | +| `icon_order` | `string[]` | `[]` | Order of tray icons by id. The match is case insensitive and each entry must only be contained in the id received by the bar. Any icon that does not match will be added to the left or on top depending on orientation.| + | `on_click_left` | `string` | `'default'` | Action to perform on left-click. See [Click Actions](#click-actions) below. | | `on_click_right` | `string` | `'menu'` | Action to perform on right-click. See [Click Actions](#click-actions) below. | | `on_click_middle` | `string` | `'none'` | Action to perform on middle-click. See [Click Actions](#click-actions) below. | @@ -55,6 +57,14 @@ To run custom commands based on which tray item was clicked: } ``` +To order icons with matching all the proton apps at once (this will not guarantee order within proton apps): +```corn +{ + type = "tray" + icon_order = ["element", "proton", "keepass"] +} +``` +
JSON @@ -105,6 +115,20 @@ end: } ``` +**Icon Names** + +List of known `icons names`: + - Element: `Element_status_icon_1` + - OBS: `obs` + - Proton Mail: `Proton Mail Bridge` + - Proton VPN: `proton.vpn.app.gtk` + - Discord: `discord_status_icon_1` + - Vesktop: `vesktop_status_icon_1` + - Nextcloud: `Nextcloud` + - KeePassXC: `KeePassXC` + - Steam: `steam` + - Tidal: `tidal-hifi_status_icon_1` +
## Styling diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 83147aed..66983213 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -261,6 +261,9 @@ impl Clients { client }; + #[cfg(feature = "ipc")] + Ironbar::variable_manager().register_namespace("tray", client.clone()); + Ok(client) } diff --git a/src/clients/tray/ironvar.rs b/src/clients/tray/ironvar.rs new file mode 100644 index 00000000..6528a37e --- /dev/null +++ b/src/clients/tray/ironvar.rs @@ -0,0 +1,34 @@ +use crate::lock; + +const TRAY_ICONS_NAMES: &str = "icon_names"; + +#[cfg(feature = "ipc")] +impl crate::ironvar::Namespace for super::Client { + fn get(&self, key: &str) -> Option { + match key { + TRAY_ICONS_NAMES => { + let items = self.client.items(); + let ids: Vec = lock!(items) + .values() + .map(|(item, _)| item.id.clone()) + .collect(); + + Some(ids.join(", ")) + } + _ => None, + } + } + + fn list(&self) -> Vec { + tracing::info!("Test"); + vec![TRAY_ICONS_NAMES.to_owned()] + } + + fn namespaces(&self) -> Vec { + vec![] + } + + fn get_namespace(&self, _key: &str) -> Option { + None + } +} diff --git a/src/clients/tray.rs b/src/clients/tray/mod.rs similarity index 99% rename from src/clients/tray.rs rename to src/clients/tray/mod.rs index 64574b04..2526d02e 100644 --- a/src/clients/tray.rs +++ b/src/clients/tray/mod.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "ipc")] +mod ironvar; + use super::ClientResult; use crate::channels::SyncSenderExt; use crate::{arc_mut, lock, register_fallible_client, spawn}; diff --git a/src/modules/tray/interface.rs b/src/modules/tray/interface.rs index 89daba15..648f06c7 100644 --- a/src/modules/tray/interface.rs +++ b/src/modules/tray/interface.rs @@ -42,6 +42,7 @@ pub(crate) struct TrayMenu { shortcut_controller: Option, pub title: Option, + pub id: String, pub icon_name: Option, pub icon_theme_path: Option, pub icon_pixmap: Option>, @@ -237,6 +238,7 @@ impl TrayMenu { label_widget: None, tx, title: item.title, + id: item.id, icon_name: item.icon_name, icon_theme_path: item.icon_theme_path.map(PathBuf::from), icon_pixmap: item.icon_pixmap, diff --git a/src/modules/tray/mod.rs b/src/modules/tray/mod.rs index 3daebdfe..e1b4e368 100644 --- a/src/modules/tray/mod.rs +++ b/src/modules/tray/mod.rs @@ -26,6 +26,7 @@ struct IconConfig { provider: image::Provider, size: u32, prefer_theme: bool, + order: Vec, } /// Reserved tray click actions @@ -164,6 +165,12 @@ pub struct TrayModule { /// **Default**: `horizontal` for horizontal bars, `vertical` for vertical bars direction: Option, + /// Order in which icons are displayed from left to right or top to bottom. + /// The names are case insensitive and must be included in the id of tray entry. + /// + /// **Default**: icons are pre append when received by the bar. + icon_order: Vec, + /// Click action handlers for tray icons #[serde(flatten)] click_handlers: TrayClickHandlers, @@ -179,6 +186,7 @@ impl Default for TrayModule { prefer_theme_icons: true, icon_size: default::IconSize::Tiny as u32, direction: None, + icon_order: Vec::new(), click_handlers: TrayClickHandlers::default(), common: Some(CommonConfig::default()), } @@ -301,6 +309,7 @@ impl Module for TrayModule { provider, size: self.icon_size, prefer_theme: self.prefer_theme_icons, + order: self.icon_order, }; // listen for UI updates @@ -385,6 +394,57 @@ fn load_icon( } } +/// Tries to find the closet neighbor to `id`. +/// +/// If no match was found then returns [None]. +/// If a match is found then the `key` to that neighbor is returned along positional information. +/// Set to true for before and false for after. +fn find_closest_neighbor<'a>( + id: &str, + menus: &'a HashMap, TrayMenu>, + ordering: &[String], +) -> Option<(&'a str, bool)> { + fn match_name(a: &str, b: &str) -> bool { + a.to_lowercase().contains(&b.to_lowercase()) + } + + fn update<'a>(key: &mut Option<&'a str>, addr: &'a str, best: &mut Option, idx: usize) { + *best = Some(idx); + *key = Some(addr); + } + + let id_idx = ordering.iter().position(|s| match_name(id, s))?; + + let mut best = None; + let mut key = None; + + for (addr, menu) in menus { + let Some(idx) = ordering.iter().position(|s| match_name(&menu.id, s)) else { + continue; + }; + + if (id_idx as i32 - idx as i32).abs() == 1 { + update(&mut key, addr, &mut best, idx); + break; + } + + let Some(best) = &mut best else { + update(&mut key, addr, &mut best, idx); + continue; + }; + + if (id_idx as i32 - idx as i32).abs() < (id_idx as i32 - *best as i32).abs() { + *best = idx; + key = Some(addr); + } + } + + let key = key?; + let idx = best?; + + Some((key, id_idx < idx)) +} + /// Handles UI updates as callback, /// getting the diff since the previous update and applying it to the menu. fn on_update( @@ -402,8 +462,20 @@ fn on_update( let mut menu_item = TrayMenu::new(&address, *item, activated_channel.clone(), click_handlers); - let x: Option<>k::Widget> = None; - container.insert_child_after(&menu_item.widget, x); + let key_order = find_closest_neighbor(&menu_item.id, menus, &icon_config.order); + let widget = if let Some((key, _)) = key_order { + menus.get(key).map(|menu| &menu.widget) + } else { + None + }; + + container.insert_child_after(&menu_item.widget, widget); + if let Some((_, order)) = key_order + && let Some(widget) = widget + && order + { + container.reorder_child_after(widget, Some(&menu_item.widget)); + } // Register both a fallback label and a picture synchronously so // the widget tree is always consistent. The picture is populated @@ -516,7 +588,10 @@ fn on_update( Event::Remove(address) => { debug!("Removing tray item at '{address}'"); - if let Some(menu) = menus.get(address.as_str()) { + if let Some(menu) = menus.remove(address.as_str()) { + if let Some(child) = menu.widget.last_child() { + child.unparent(); // removing popover + } container.remove(&menu.widget); } }