From 8016ec256de0c3d2290d1446cda45a769a3c5284 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sat, 27 Jan 2024 00:35:16 +0000 Subject: [PATCH 1/5] fix(tray): crash caused by excess updates Some icons seem to absolutely spam updates in some circumstances. Increasing the channel size by 4x seems sufficient to prevent this. --- src/clients/system_tray.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clients/system_tray.rs b/src/clients/system_tray.rs index 60c8b129..aec5c0a6 100644 --- a/src/clients/system_tray.rs +++ b/src/clients/system_tray.rs @@ -25,7 +25,7 @@ impl TrayEventReceiver { let id = format!("ironbar-{}", Ironbar::unique_id()); let (tx, rx) = mpsc::channel(16); - let (b_tx, b_rx) = broadcast::channel(16); + let (b_tx, b_rx) = broadcast::channel(64); let tray = StatusNotifierWatcher::new(rx).await?; let mut host = Box::pin(tray.create_notifier_host(&id)).await?; @@ -48,7 +48,7 @@ impl TrayEventReceiver { item, menu, } => { - debug!("Adding item with address '{address}'"); + debug!("Adding/updating item with address '{address}'"); tray.insert(address, (item, menu)); } NotifierItemMessage::Remove { address } => { From b3a70ce8fa76b0ae8b06f423e7d5955c6d5d6920 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sat, 27 Jan 2024 01:01:10 +0000 Subject: [PATCH 2/5] fix(tray): not handling checkbox items --- src/modules/tray.rs | 88 ++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/src/modules/tray.rs b/src/modules/tray.rs index 35a8b57c..63f248e1 100644 --- a/src/modules/tray.rs +++ b/src/modules/tray.rs @@ -9,7 +9,7 @@ use gtk::ffi::gtk_icon_theme_get_search_path; use gtk::gdk_pixbuf::{Colorspace, InterpType}; use gtk::prelude::*; use gtk::{ - gdk_pixbuf, IconLookupFlags, IconTheme, Image, Label, Menu, MenuBar, MenuItem, + gdk_pixbuf, CheckMenuItem, IconLookupFlags, IconTheme, Image, Label, Menu, MenuBar, MenuItem, SeparatorMenuItem, }; use serde::Deserialize; @@ -17,7 +17,7 @@ use std::collections::{HashMap, HashSet}; use std::ffi::CStr; use std::os::raw::{c_char, c_int}; use std::ptr; -use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType}; +use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType}; use system_tray::message::tray::StatusNotifierItem; use system_tray::message::{NotifierItemCommand, NotifierItemMessage}; use tokio::sync::mpsc; @@ -75,7 +75,7 @@ fn get_image_from_icon_name(item: &StatusNotifierItem, icon_theme: &IconTheme) - /// The pixmap is supplied in ARGB32 format, /// which has 8 bits per sample and a bit stride of `4*width`. fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option { - const BITS_PER_SAMPLE: i32 = 8; // + const BITS_PER_SAMPLE: i32 = 8; let pixmap = item .icon_pixmap @@ -109,46 +109,62 @@ fn get_menu_items( id: &str, path: &str, ) -> Vec { + macro_rules! setup_menu_item { + ($builder:expr, $item_info:expr) => {{ + if !$item_info.submenu.is_empty() { + let menu = Menu::new(); + get_menu_items(&$item_info.submenu, &tx.clone(), id, path) + .iter() + .for_each(|item| menu.add(item)); + + $builder = $builder.submenu(&menu); + } + + let item = $builder.build(); + + let info = $item_info.clone(); + let id = id.to_string(); + let path = path.to_string(); + + { + let tx = tx.clone(); + item.connect_activate(move |_item| { + try_send!( + tx, + NotifierItemCommand::MenuItemClicked { + submenu_id: info.id, + menu_path: path.clone(), + notifier_address: id.clone(), + } + ); + }); + } + + Box::new(item) + }}; + } + menu.iter() .map(|item_info| { - let item: Box> = match item_info.menu_type { - MenuType::Separator => Box::new(SeparatorMenuItem::new()), - MenuType::Standard => { + let item: Box> = match (item_info.menu_type, item_info.toggle_type) + { + (MenuType::Separator, _) => Box::new(SeparatorMenuItem::new()), + (MenuType::Standard, ToggleType::Checkmark) => { + let mut builder = CheckMenuItem::builder() + .label(item_info.label.as_str()) + .visible(item_info.visible) + .sensitive(item_info.enabled) + .active(item_info.toggle_state == ToggleState::On); + + setup_menu_item!(builder, item_info) + } + (MenuType::Standard, _) => { let mut builder = MenuItem::builder() .label(item_info.label.as_str()) .visible(item_info.visible) .sensitive(item_info.enabled); - if !item_info.submenu.is_empty() { - let menu = Menu::new(); - get_menu_items(&item_info.submenu, &tx.clone(), id, path) - .iter() - .for_each(|item| menu.add(item)); - - builder = builder.submenu(&menu); - } - - let item = builder.build(); - - let info = item_info.clone(); - let id = id.to_string(); - let path = path.to_string(); - - { - let tx = tx.clone(); - item.connect_activate(move |_item| { - try_send!( - tx, - NotifierItemCommand::MenuItemClicked { - submenu_id: info.id, - menu_path: path.clone(), - notifier_address: id.clone(), - } - ); - }); - } - - Box::new(item) + setup_menu_item!(builder, item_info) } }; From cc39896181fd317440d94a054457ba01c649f0d7 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sat, 27 Jan 2024 01:03:46 +0000 Subject: [PATCH 3/5] build: update `system-tray` crate --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b75a7fbe..d5bb9bb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3002,9 +3002,9 @@ dependencies = [ [[package]] name = "system-tray" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8f7f19237d2149a8e4b56409c579b26a98748e3cfadba93762c4746c4c7ae2" +checksum = "a456e3e6cbd396f1a3a91f8f74d1fdcf2bde85c97afe174442c367f4749fc09b" dependencies = [ "anyhow", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index 17972274..d3aa9fd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ mpris = { version = "2.0.1", optional = true } sysinfo = { version = "0.29.11", optional = true } # tray -system-tray = { version = "0.1.4", optional = true } +system-tray = { version = "0.1.5", optional = true } # upower upower_dbus = { version = "0.3.2", optional = true } From 061663392e01503448fb44a064d172dbf10dc770 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Mon, 29 Jan 2024 23:30:25 +0000 Subject: [PATCH 4/5] fix: do not panic on full channels --- src/macros.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index e71a5934..028fd196 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -62,8 +62,17 @@ macro_rules! glib_recv { glib::spawn_future_local(async move { // re-delcare in case ie `context.subscribe()` is passed directly let mut rx = $rx; - while let Ok($val) = rx.recv().await { - $expr + loop { + match rx.recv().await { + Ok($val) => $expr, + Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => { + tracing::warn!("Channel lagged behind by {count}, this may result in unexpected or broken behaviour"); + } + Err(err) => { + tracing::error!("{err:?}"); + break; + } + } } }); }}; From 133632d1ad0778bb93e398e6d2bacf28c364f6c4 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Thu, 1 Feb 2024 21:43:20 +0000 Subject: [PATCH 5/5] fix(tray): vastly improve rendering performance --- src/modules/tray.rs | 291 ------------------------------- src/modules/tray/diff.rs | 104 +++++++++++ src/modules/tray/icon.rs | 86 ++++++++++ src/modules/tray/interface.rs | 314 ++++++++++++++++++++++++++++++++++ src/modules/tray/mod.rs | 146 ++++++++++++++++ 5 files changed, 650 insertions(+), 291 deletions(-) delete mode 100644 src/modules/tray.rs create mode 100644 src/modules/tray/diff.rs create mode 100644 src/modules/tray/icon.rs create mode 100644 src/modules/tray/interface.rs create mode 100644 src/modules/tray/mod.rs diff --git a/src/modules/tray.rs b/src/modules/tray.rs deleted file mode 100644 index 63f248e1..00000000 --- a/src/modules/tray.rs +++ /dev/null @@ -1,291 +0,0 @@ -use crate::clients::system_tray::TrayEventReceiver; -use crate::config::CommonConfig; -use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext}; -use crate::{glib_recv, spawn, try_send}; -use color_eyre::Result; -use glib::ffi::g_strfreev; -use glib::translate::ToGlibPtr; -use gtk::ffi::gtk_icon_theme_get_search_path; -use gtk::gdk_pixbuf::{Colorspace, InterpType}; -use gtk::prelude::*; -use gtk::{ - gdk_pixbuf, CheckMenuItem, IconLookupFlags, IconTheme, Image, Label, Menu, MenuBar, MenuItem, - SeparatorMenuItem, -}; -use serde::Deserialize; -use std::collections::{HashMap, HashSet}; -use std::ffi::CStr; -use std::os::raw::{c_char, c_int}; -use std::ptr; -use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType}; -use system_tray::message::tray::StatusNotifierItem; -use system_tray::message::{NotifierItemCommand, NotifierItemMessage}; -use tokio::sync::mpsc; -use tokio::sync::mpsc::{Receiver, Sender}; - -#[derive(Debug, Deserialize, Clone)] -pub struct TrayModule { - #[serde(flatten)] - pub common: Option, -} - -/// Gets the GTK icon theme search paths by calling the FFI function. -/// Conveniently returns the result as a `HashSet`. -fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet { - let mut gtk_paths: *mut *mut c_char = ptr::null_mut(); - let mut n_elements: c_int = 0; - let mut paths = HashSet::new(); - unsafe { - gtk_icon_theme_get_search_path( - icon_theme.to_glib_none().0, - &mut gtk_paths, - &mut n_elements, - ); - // n_elements is never negative (that would be weird) - for i in 0..n_elements as usize { - let c_str = CStr::from_ptr(*gtk_paths.add(i)); - if let Ok(str) = c_str.to_str() { - paths.insert(str.to_owned()); - } - } - - g_strfreev(gtk_paths); - } - - paths -} - -/// Attempts to get a GTK `Image` component -/// for the status notifier item's icon. -fn get_image_from_icon_name(item: &StatusNotifierItem, icon_theme: &IconTheme) -> Option { - if let Some(path) = item.icon_theme_path.as_ref() { - if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) { - icon_theme.append_search_path(path); - } - } - - item.icon_name.as_ref().and_then(|icon_name| { - let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty()); - icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref())) - }) -} - -/// Attempts to get an image from the item pixmap. -/// -/// The pixmap is supplied in ARGB32 format, -/// which has 8 bits per sample and a bit stride of `4*width`. -fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option { - const BITS_PER_SAMPLE: i32 = 8; - - let pixmap = item - .icon_pixmap - .as_ref() - .and_then(|pixmap| pixmap.first())?; - - let bytes = glib::Bytes::from(&pixmap.pixels); - let row_stride = pixmap.width * 4; // - - let pixbuf = gdk_pixbuf::Pixbuf::from_bytes( - &bytes, - Colorspace::Rgb, - true, - BITS_PER_SAMPLE, - pixmap.width, - pixmap.height, - row_stride, - ); - - let pixbuf = pixbuf - .scale_simple(16, 16, InterpType::Bilinear) - .unwrap_or(pixbuf); - Some(Image::from_pixbuf(Some(&pixbuf))) -} - -/// Recursively gets GTK `MenuItem` components -/// for the provided submenu array. -fn get_menu_items( - menu: &[MenuItemInfo], - tx: &Sender, - id: &str, - path: &str, -) -> Vec { - macro_rules! setup_menu_item { - ($builder:expr, $item_info:expr) => {{ - if !$item_info.submenu.is_empty() { - let menu = Menu::new(); - get_menu_items(&$item_info.submenu, &tx.clone(), id, path) - .iter() - .for_each(|item| menu.add(item)); - - $builder = $builder.submenu(&menu); - } - - let item = $builder.build(); - - let info = $item_info.clone(); - let id = id.to_string(); - let path = path.to_string(); - - { - let tx = tx.clone(); - item.connect_activate(move |_item| { - try_send!( - tx, - NotifierItemCommand::MenuItemClicked { - submenu_id: info.id, - menu_path: path.clone(), - notifier_address: id.clone(), - } - ); - }); - } - - Box::new(item) - }}; - } - - menu.iter() - .map(|item_info| { - let item: Box> = match (item_info.menu_type, item_info.toggle_type) - { - (MenuType::Separator, _) => Box::new(SeparatorMenuItem::new()), - (MenuType::Standard, ToggleType::Checkmark) => { - let mut builder = CheckMenuItem::builder() - .label(item_info.label.as_str()) - .visible(item_info.visible) - .sensitive(item_info.enabled) - .active(item_info.toggle_state == ToggleState::On); - - setup_menu_item!(builder, item_info) - } - (MenuType::Standard, _) => { - let mut builder = MenuItem::builder() - .label(item_info.label.as_str()) - .visible(item_info.visible) - .sensitive(item_info.enabled); - - setup_menu_item!(builder, item_info) - } - }; - - (*item).as_ref().clone() - }) - .collect() -} - -impl Module for TrayModule { - type SendMessage = NotifierItemMessage; - type ReceiveMessage = NotifierItemCommand; - - fn name() -> &'static str { - "tray" - } - - fn spawn_controller( - &self, - _info: &ModuleInfo, - context: &WidgetContext, - mut rx: Receiver, - ) -> Result<()> { - let tx = context.tx.clone(); - - let client = context.client::(); - - let (tray_tx, mut tray_rx) = client.subscribe(); - - // listen to tray updates - spawn(async move { - while let Ok(message) = tray_rx.recv().await { - tx.send(ModuleUpdateEvent::Update(message)).await?; - } - - Ok::<(), mpsc::error::SendError>>(()) - }); - - // send tray commands - spawn(async move { - while let Some(cmd) = rx.recv().await { - tray_tx.send(cmd).await?; - } - - Ok::<(), mpsc::error::SendError>(()) - }); - - Ok(()) - } - - fn into_widget( - self, - context: WidgetContext, - info: &ModuleInfo, - ) -> Result> { - let container = MenuBar::new(); - - { - let container = container.clone(); - let mut widgets = HashMap::new(); - let icon_theme = info.icon_theme.clone(); - - // listen for UI updates - glib_recv!(context.subscribe(), update => { - match update { - NotifierItemMessage::Update { - item, - address, - menu, - } => { - let addr = &address; - let menu_item = widgets.remove(address.as_str()).unwrap_or_else(|| { - let menu_item = MenuItem::new(); - menu_item.style_context().add_class("item"); - - get_image_from_icon_name(&item, &icon_theme) - .or_else(|| get_image_from_pixmap(&item)) - .map_or_else( - || { - let label = - Label::new(Some(item.title.as_ref().unwrap_or(addr))); - menu_item.add(&label); - }, - |image| { - image.set_widget_name(address.as_str()); - menu_item.add(&image); - }, - ); - - container.add(&menu_item); - menu_item.show_all(); - menu_item - }); - if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) { - let submenus = menu_opts.submenus; - if !submenus.is_empty() { - let menu = Menu::new(); - get_menu_items( - &submenus, - &context.controller_tx.clone(), - &address, - &menu_path, - ) - .iter() - .for_each(|item| menu.add(item)); - menu_item.set_submenu(Some(&menu)); - } - } - widgets.insert(address, menu_item); - } - NotifierItemMessage::Remove { address } => { - if let Some(widget) = widgets.get(&address) { - container.remove(widget); - } - } - }; - }); - }; - - Ok(ModuleParts { - widget: container, - popup: None, - }) - } -} diff --git a/src/modules/tray/diff.rs b/src/modules/tray/diff.rs new file mode 100644 index 00000000..25e27b69 --- /dev/null +++ b/src/modules/tray/diff.rs @@ -0,0 +1,104 @@ +use system_tray::message::menu::{MenuItem as MenuItemInfo, ToggleState}; + +/// Diff change type and associated info. +#[derive(Debug, Clone)] +pub enum Diff { + Add(MenuItemInfo), + Update(i32, MenuItemDiff), + Remove(i32), +} + +/// Diff info to be applied to an existing menu item as an update. +#[derive(Debug, Clone)] +pub struct MenuItemDiff { + /// Text of the item, + pub label: Option, + /// Whether the item can be activated or not. + pub enabled: Option, + /// True if the item is visible in the menu. + pub visible: Option, + /// Icon name of the item, following the freedesktop.org icon spec. + // pub icon_name: Option>, + /// Describe the current state of a "togglable" item. Can be one of: + /// - Some(true): on + /// - Some(false): off + /// - None: indeterminate + pub toggle_state: Option, + /// A submenu for this item, typically this would ve revealed to the user by hovering the current item + pub submenu: Vec, +} + +impl MenuItemDiff { + fn new(old: &MenuItemInfo, new: &MenuItemInfo) -> Self { + macro_rules! diff { + ($field:ident) => { + if old.$field == new.$field { + None + } else { + Some(new.$field) + } + }; + + (&$field:ident) => { + if &old.$field == &new.$field { + None + } else { + Some(new.$field.clone()) + } + }; + } + + Self { + label: diff!(&label), + enabled: diff!(enabled), + visible: diff!(visible), + // icon_name: diff!(&icon_name), + toggle_state: diff!(toggle_state), + submenu: get_diffs(&old.submenu, &new.submenu), + } + } + + /// Whether this diff contains any changes + fn has_diff(&self) -> bool { + self.label.is_some() + || self.enabled.is_some() + || self.visible.is_some() + // || self.icon_name.is_some() + || self.toggle_state.is_some() + || !self.submenu.is_empty() + } +} + +/// Gets a diff set between old and new state. +pub fn get_diffs(old: &[MenuItemInfo], new: &[MenuItemInfo]) -> Vec { + let mut diffs = vec![]; + + for new_item in new { + let old_item = old.iter().find(|&item| item.id == new_item.id); + + let diff = match old_item { + Some(old_item) => { + let item_diff = MenuItemDiff::new(old_item, new_item); + if item_diff.has_diff() { + Some(Diff::Update(old_item.id, item_diff)) + } else { + None + } + } + None => Some(Diff::Add(new_item.clone())), + }; + + if let Some(diff) = diff { + diffs.push(diff); + } + } + + for old_item in old { + let new_item = new.iter().find(|&item| item.id == old_item.id); + if new_item.is_none() { + diffs.push(Diff::Remove(old_item.id)); + } + } + + diffs +} diff --git a/src/modules/tray/icon.rs b/src/modules/tray/icon.rs new file mode 100644 index 00000000..850137f6 --- /dev/null +++ b/src/modules/tray/icon.rs @@ -0,0 +1,86 @@ +use glib::ffi::g_strfreev; +use glib::translate::ToGlibPtr; +use gtk::ffi::gtk_icon_theme_get_search_path; +use gtk::gdk_pixbuf::{Colorspace, InterpType}; +use gtk::prelude::IconThemeExt; +use gtk::{gdk_pixbuf, IconLookupFlags, IconTheme, Image}; +use std::collections::HashSet; +use std::ffi::CStr; +use std::os::raw::{c_char, c_int}; +use std::ptr; +use system_tray::message::tray::StatusNotifierItem; + +/// Gets the GTK icon theme search paths by calling the FFI function. +/// Conveniently returns the result as a `HashSet`. +fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet { + let mut gtk_paths: *mut *mut c_char = ptr::null_mut(); + let mut n_elements: c_int = 0; + let mut paths = HashSet::new(); + unsafe { + gtk_icon_theme_get_search_path( + icon_theme.to_glib_none().0, + &mut gtk_paths, + &mut n_elements, + ); + // n_elements is never negative (that would be weird) + for i in 0..n_elements as usize { + let c_str = CStr::from_ptr(*gtk_paths.add(i)); + if let Ok(str) = c_str.to_str() { + paths.insert(str.to_owned()); + } + } + + g_strfreev(gtk_paths); + } + + paths +} + +/// Attempts to get a GTK `Image` component +/// for the status notifier item's icon. +pub(crate) fn get_image_from_icon_name( + item: &StatusNotifierItem, + icon_theme: &IconTheme, +) -> Option { + if let Some(path) = item.icon_theme_path.as_ref() { + if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) { + icon_theme.append_search_path(path); + } + } + + item.icon_name.as_ref().and_then(|icon_name| { + let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty()); + icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref())) + }) +} + +/// Attempts to get an image from the item pixmap. +/// +/// The pixmap is supplied in ARGB32 format, +/// which has 8 bits per sample and a bit stride of `4*width`. +pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option { + const BITS_PER_SAMPLE: i32 = 8; + + let pixmap = item + .icon_pixmap + .as_ref() + .and_then(|pixmap| pixmap.first())?; + + let bytes = glib::Bytes::from(&pixmap.pixels); + let row_stride = pixmap.width * 4; // + + let pixbuf = gdk_pixbuf::Pixbuf::from_bytes( + &bytes, + Colorspace::Rgb, + true, + BITS_PER_SAMPLE, + pixmap.width, + pixmap.height, + row_stride, + ); + + let pixbuf = pixbuf + .scale_simple(16, 16, InterpType::Bilinear) + .unwrap_or(pixbuf); + Some(Image::from_pixbuf(Some(&pixbuf))) +} diff --git a/src/modules/tray/interface.rs b/src/modules/tray/interface.rs new file mode 100644 index 00000000..3b9d988d --- /dev/null +++ b/src/modules/tray/interface.rs @@ -0,0 +1,314 @@ +use crate::modules::tray::diff::{Diff, MenuItemDiff}; +use crate::{spawn, try_send}; +use gtk::prelude::*; +use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem}; +use std::collections::HashMap; +use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType}; +use system_tray::message::NotifierItemCommand; +use tokio::sync::mpsc; + +/// Calls a method on the underlying widget, +/// passing in a single argument. +/// +/// This is useful to avoid matching on +/// `TrayMenuWidget` constantly. +/// +/// # Example +/// ```rust +/// call!(container, add, my_widget) +/// ``` +/// is the same as: +/// ``` +/// match &my_widget { +/// TrayMenuWidget::Separator(w) => { +/// container.add(w); +/// } +/// TrayMenuWidget::Standard(w) => { +/// container.add(w); +/// } +/// TrayMenuWidget::Checkbox(w) => { +/// container.add(w); +/// } +/// } +/// ``` +macro_rules! call { + ($parent:expr, $method:ident, $child:expr) => { + match &$child { + TrayMenuWidget::Separator(w) => { + $parent.$method(w); + } + TrayMenuWidget::Standard(w) => { + $parent.$method(w); + } + TrayMenuWidget::Checkbox(w) => { + $parent.$method(w); + } + } + }; +} + +/// Main tray icon to show on the bar +pub(crate) struct TrayMenu { + pub(crate) widget: MenuItem, + menu_widget: Menu, + image_widget: Option, + label_widget: Option