1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-16 22:31:03 +02:00
ironbar/src/modules/tray/mod.rs

220 lines
7.3 KiB
Rust
Raw Normal View History

mod icon;
mod interface;
use crate::clients::tray;
use crate::config::{CommonConfig, ModuleOrientation};
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, lock, module_impl, send_async, spawn};
use color_eyre::{Report, Result};
use gtk::prelude::*;
use gtk::{IconTheme, Orientation};
use interface::TrayMenu;
use serde::Deserialize;
use std::collections::HashMap;
use system_tray::client::Event;
use system_tray::client::{ActivateRequest, UpdateEvent};
use tokio::sync::mpsc;
use tracing::{debug, error, trace, warn};
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TrayModule {
/// 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.
///
/// **Default**: `true`
#[serde(default = "crate::config::default_true")]
prefer_theme_icons: bool,
/// Size in pixels to display the tray icons as.
///
/// **Default**: `16`
2024-02-25 17:13:28 +00:00
#[serde(default = "default_icon_size")]
icon_size: u32,
/// The direction in which to pack tray icons.
///
/// **Valid options**: `horizontal`, `vertical`
/// <br>
/// **Default**: `horizontal` for horizontal bars, `vertical` for vertical bars
#[serde(default)]
direction: Option<ModuleOrientation>,
/// See [common options](module-level-options#common-options).
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
2024-02-25 17:13:28 +00:00
const fn default_icon_size() -> u32 {
16
}
impl Module<gtk::Box> for TrayModule {
type SendMessage = Event;
type ReceiveMessage = ActivateRequest;
module_impl!("tray");
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.try_client::<tray::Client>()?;
let mut tray_rx = client.subscribe();
let initial_items = {
let items = client.items();
lock!(items).clone()
};
// listen to tray updates
spawn(async move {
2024-04-01 16:33:38 +01:00
for (key, (item, menu)) in initial_items {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
);
if let Some(menu) = menu.clone() {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::Update(key, UpdateEvent::Menu(menu)))
);
}
}
while let Ok(message) = tray_rx.recv().await {
2024-04-01 16:33:38 +01:00
send_async!(tx, ModuleUpdateEvent::Update(message));
}
});
// send tray commands
spawn(async move {
while let Some(cmd) = rx.recv().await {
if let Err(err) = client.activate(cmd).await {
error!("{err:?}");
};
}
Ok::<_, Report>(())
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let orientation = self
.direction
.map_or(info.bar_position.orientation(), Orientation::from);
// We use a `Box` here instead of the (supposedly correct) `MenuBar`
// as the latter has issues on Sway with menus focus-stealing from the bar.
//
// Each widget is wrapped in an EventBox, copying what Waybar does here.
let container = gtk::Box::new(orientation, 10);
2024-02-12 23:57:27 +01:00
{
let container = container.clone();
let mut menus = HashMap::new();
let icon_theme = info.icon_theme.clone();
// listen for UI updates
glib_recv!(context.subscribe(), update =>
on_update(update, &container, &mut menus, &icon_theme, self.icon_size, self.prefer_theme_icons)
);
};
Ok(ModuleParts {
widget: container,
popup: None,
})
}
}
/// Handles UI updates as callback,
/// getting the diff since the previous update and applying it to the menu.
fn on_update(
update: Event,
container: &gtk::Box,
menus: &mut HashMap<Box<str>, TrayMenu>,
icon_theme: &IconTheme,
2024-02-25 17:13:28 +00:00
icon_size: u32,
prefer_icons: bool,
) {
match update {
Event::Add(address, item) => {
debug!("Received new tray item at '{address}': {item:?}");
let mut menu_item = TrayMenu::new(&address, *item);
container.pack_start(&menu_item.event_box, true, true, 0);
2024-04-01 16:33:38 +01:00
if let Ok(image) = icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
menu_item.set_image(&image);
} else {
let label = menu_item.title.clone().unwrap_or(address.clone());
menu_item.set_label(&label);
};
menu_item.event_box.show();
menus.insert(address.into(), menu_item);
}
Event::Update(address, update) => {
debug!("Received tray update for '{address}'");
trace!("Tray update for '{address}: {update:?}'");
let Some(menu_item) = menus.get_mut(address.as_str()) else {
error!("Attempted to update menu at '{address}' but could not find it");
return;
};
match update {
UpdateEvent::AttentionIcon(_icon) => {
warn!("received unimplemented NewAttentionIcon event");
}
UpdateEvent::Icon(icon) => {
if icon.as_ref() != menu_item.icon_name() {
2024-11-08 01:35:35 +00:00
menu_item.set_icon_name(icon);
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
Ok(image) => menu_item.set_image(&image),
Err(_) => menu_item.show_label(),
};
}
}
UpdateEvent::OverlayIcon(_icon) => {
warn!("received unimplemented NewOverlayIcon event");
}
UpdateEvent::Status(_status) => {
warn!("received unimplemented NewStatus event");
}
UpdateEvent::Title(title) => {
if let Some(label_widget) = menu_item.label_widget() {
label_widget.set_label(&title.unwrap_or_default());
}
}
UpdateEvent::Tooltip(tooltip) => {
menu_item.set_tooltip(tooltip);
}
UpdateEvent::MenuConnect(menu) => {
let menu = system_tray::gtk_menu::Menu::new(&address, &menu);
menu_item.set_menu_widget(menu);
}
UpdateEvent::Menu(_) | UpdateEvent::MenuDiff(_) => {}
}
}
Event::Remove(address) => {
debug!("Removing tray item at '{address}'");
if let Some(menu) = menus.get(address.as_str()) {
container.remove(&menu.event_box);
}
}
};
}