Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/modules/Tray.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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"]
}
```

<details>
<summary>JSON</summary>

Expand Down Expand Up @@ -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`

</details>

## Styling
Expand Down
3 changes: 3 additions & 0 deletions src/clients/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ impl Clients {
client
};

#[cfg(feature = "ipc")]
Ironbar::variable_manager().register_namespace("tray", client.clone());

Ok(client)
}

Expand Down
34 changes: 34 additions & 0 deletions src/clients/tray/ironvar.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
match key {
TRAY_ICONS_NAMES => {
let items = self.client.items();
let ids: Vec<String> = lock!(items)
.values()
.map(|(item, _)| item.id.clone())
.collect();

Some(ids.join(", "))
}
_ => None,
}
}

fn list(&self) -> Vec<String> {
tracing::info!("Test");
vec![TRAY_ICONS_NAMES.to_owned()]
}

fn namespaces(&self) -> Vec<String> {
vec![]
}

fn get_namespace(&self, _key: &str) -> Option<crate::ironvar::NamespaceTrait> {
None
}
}
3 changes: 3 additions & 0 deletions src/clients/tray.rs → src/clients/tray/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
2 changes: 2 additions & 0 deletions src/modules/tray/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub(crate) struct TrayMenu {
shortcut_controller: Option<ShortcutController>,

pub title: Option<String>,
pub id: String,
pub icon_name: Option<String>,
pub icon_theme_path: Option<PathBuf>,
pub icon_pixmap: Option<Vec<IconPixmap>>,
Expand Down Expand Up @@ -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,
Expand Down
81 changes: 78 additions & 3 deletions src/modules/tray/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct IconConfig {
provider: image::Provider,
size: u32,
prefer_theme: bool,
order: Vec<String>,
}

/// Reserved tray click actions
Expand Down Expand Up @@ -164,6 +165,12 @@ pub struct TrayModule {
/// **Default**: `horizontal` for horizontal bars, `vertical` for vertical bars
direction: Option<ModuleOrientation>,

/// 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<String>,

/// Click action handlers for tray icons
#[serde(flatten)]
click_handlers: TrayClickHandlers,
Expand All @@ -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()),
}
Expand Down Expand Up @@ -301,6 +309,7 @@ impl Module<gtk::Box> for TrayModule {
provider,
size: self.icon_size,
prefer_theme: self.prefer_theme_icons,
order: self.icon_order,
};

// listen for UI updates
Expand Down Expand Up @@ -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<Box<str>, 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<usize>, 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(
Expand All @@ -402,8 +462,20 @@ fn on_update(
let mut menu_item =
TrayMenu::new(&address, *item, activated_channel.clone(), click_handlers);

let x: Option<&gtk::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
Expand Down Expand Up @@ -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);
}
}
Expand Down
Loading