Skip to content

Commit

Permalink
Add compat menu items and menu update channel
Browse files Browse the repository at this point in the history
  • Loading branch information
dfaust committed Nov 16, 2024
1 parent 2f9afec commit fc73011
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 156 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ serde = ["muda/serde", "dep:serde"]
common-controls-v6 = ["muda/common-controls-v6"]

[dependencies]
muda = { version = "0.15", default-features = false }
muda = { version = "0.15", default-features = false, features = ["ksni"] }
crossbeam-channel = "0.5"
once_cell = "1"
thiserror = "1.0"
Expand All @@ -32,6 +32,7 @@ features = [
]

[target."cfg(target_os = \"linux\")".dependencies]
arc-swap = "1.7.1"
ksni = "0.2.2"
dirs = "5"

Expand Down
86 changes: 86 additions & 0 deletions examples/counter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

#![allow(unused)]

use tao::event_loop::{ControlFlow, EventLoopBuilder};
use tray_icon::{
menu::{
AboutMetadata, CheckMenuItem, IconMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem,
Submenu,
},
TrayIconBuilder, TrayIconEvent,
};

fn main() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png");

let event_loop = EventLoopBuilder::new().build();

let mut counter = 0;
let tray_menu = Menu::new();

let counter_i = MenuItem::new(format!("Counter: {counter}"), true, None);
tray_menu.append_items(&[&counter_i]);

let mut tray_icon = None;

let menu_channel = MenuEvent::receiver();
let tray_channel = TrayIconEvent::receiver();

event_loop.run(move |event, _, control_flow| {
// We add delay of 16 ms (60fps) to event_loop to reduce cpu load.
// Alternatively, you can set ControlFlow::Wait or use TrayIconEvent::set_event_handler,
// see https://github.com/tauri-apps/tray-icon/issues/83#issuecomment-1697773065
*control_flow = ControlFlow::Poll;
std::thread::sleep(std::time::Duration::from_millis(16));

if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event {
let icon = load_tray_icon(std::path::Path::new(path));

// We create the icon once the event loop is actually running
// to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90
tray_icon = Some(
TrayIconBuilder::new()
.with_menu(Box::new(tray_menu.clone()))
.with_tooltip("tao - awesome windowing lib")
.with_icon(icon)
.build()
.unwrap(),
);

// We have to request a redraw here to have the icon actually show up.
// Tao only exposes a redraw method on the Window so we use core-foundation directly.
#[cfg(target_os = "macos")]
unsafe {
use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp};

let rl = CFRunLoopGetMain();
CFRunLoopWakeUp(rl);
}
}

counter += 1;
counter_i.set_text(format!("Counter: {counter}"));
})
}

fn load_icon(path: &std::path::Path) -> (Vec<u8>, u32, u32) {
let image = image::open(path)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
}

fn load_tray_icon(path: &std::path::Path) -> tray_icon::Icon {
let (icon_rgba, icon_width, icon_height) = load_icon(path);
tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
}

fn load_menu_icon(path: &std::path::Path) -> muda::Icon {
let (icon_rgba, icon_width, icon_height) = load_icon(path);
muda::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
}
194 changes: 46 additions & 148 deletions src/platform_impl/linux/menu.rs
Original file line number Diff line number Diff line change
@@ -1,166 +1,64 @@
use std::sync::Arc;

use arc_swap::ArcSwap;
use muda::{AboutDialog, PredefinedMenuItemKind};

use super::tray::Tray;

#[derive(Debug, Clone)]
pub struct StandardItem {
id: String,
label: String,
enabled: bool,
icon: Option<Vec<u8>>,
predefined_menu_item_kind: Option<PredefinedMenuItemKind>,
}

#[derive(Debug, Clone)]
pub struct CheckmarkItem {
id: String,
label: String,
enabled: bool,
checked: bool,
}

#[derive(Debug, Clone)]
pub struct SubMenuItem {
label: String,
enabled: bool,
submenu: Vec<MenuItem>,
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone)]
pub enum MenuItem {
Standard(StandardItem),
Checkmark(CheckmarkItem),
SubMenu(SubMenuItem),
Separator,
}

impl From<StandardItem> for MenuItem {
fn from(item: StandardItem) -> Self {
Self::Standard(item)
}
}

impl From<CheckmarkItem> for MenuItem {
fn from(item: CheckmarkItem) -> Self {
Self::Checkmark(item)
}
}

impl From<SubMenuItem> for MenuItem {
fn from(item: SubMenuItem) -> Self {
Self::SubMenu(item)
}
}

impl From<muda::MenuItemKind> for MenuItem {
fn from(item: muda::MenuItemKind) -> Self {
match item {
muda::MenuItemKind::MenuItem(menu_item) => StandardItem {
id: menu_item.id().0.clone(),
label: menu_item.text().replace('&', ""),
enabled: menu_item.is_enabled(),
icon: None,
predefined_menu_item_kind: None,
}
.into(),
muda::MenuItemKind::Submenu(submenu) => SubMenuItem {
label: submenu.text().replace('&', ""),
enabled: submenu.is_enabled(),
submenu: submenu.items().into_iter().map(Into::into).collect(),
}
.into(),
muda::MenuItemKind::Predefined(predefined_menu_item) => {
match predefined_menu_item.predefined_item_kind() {
Some(PredefinedMenuItemKind::Separator) => MenuItem::Separator,
Some(predefined_menu_item_kind) => StandardItem {
id: predefined_menu_item.id().0.clone(),
label: predefined_menu_item.text().replace('&', ""),
enabled: true,
icon: None,
predefined_menu_item_kind: Some(predefined_menu_item_kind),
}
.into(),
_ => StandardItem {
id: predefined_menu_item.id().0.clone(),
label: predefined_menu_item.text().replace('&', ""),
enabled: true,
icon: None,
predefined_menu_item_kind: None,
}
.into(),
}
}
muda::MenuItemKind::Check(check_menu_item) => CheckmarkItem {
id: check_menu_item.id().0.clone(),
label: check_menu_item.text().replace('&', ""),
enabled: check_menu_item.is_enabled(),
checked: check_menu_item.is_checked(),
}
.into(),
muda::MenuItemKind::Icon(icon_menu_item) => StandardItem {
id: icon_menu_item.id().0.clone(),
label: icon_menu_item.text().replace('&', ""),
enabled: icon_menu_item.is_enabled(),
icon: icon_menu_item
.icon()
.map(|icon| icon.to_pixbuf().save_to_bufferv("png", &[]).unwrap()),
predefined_menu_item_kind: None,
}
.into(),
}
}
}

impl From<MenuItem> for ksni::MenuItem<Tray> {
fn from(item: MenuItem) -> Self {
match item {
MenuItem::Standard(menu_item) => {
let id = menu_item.id;
match menu_item.predefined_menu_item_kind {
Some(PredefinedMenuItemKind::About(Some(metadata))) => {
let about_dialog = AboutDialog::new(metadata);
ksni::menu::StandardItem {
label: menu_item.label,
enabled: menu_item.enabled,
icon_data: menu_item.icon.unwrap_or_default(),
activate: Box::new(move |_| {
about_dialog.show();
}),
..Default::default()
}
.into()
}
_ => ksni::menu::StandardItem {
label: menu_item.label,
pub fn muda_to_ksni_menu_item(
item: Arc<ArcSwap<muda::CompatMenuItem>>,
) -> ksni::menu::MenuItem<Tray> {
match &**item.load() {
muda::CompatMenuItem::Standard(menu_item) => {
let id = menu_item.id.clone();
match &menu_item.predefined_menu_item_kind {
Some(PredefinedMenuItemKind::About(Some(metadata))) => {
let about_dialog = AboutDialog::new(metadata.clone());
ksni::menu::StandardItem {
label: menu_item.label.clone(),
enabled: menu_item.enabled,
icon_data: menu_item.icon.unwrap_or_default(),
activate: Box::new(move |_| send_menu_event(&id)),
icon_data: menu_item.icon.clone().unwrap_or_default(),
activate: Box::new(move |_| {
about_dialog.show();
}),
..Default::default()
}
.into(),
.into()
}
}
MenuItem::Checkmark(check_menu_item) => {
let id = check_menu_item.id;
ksni::menu::CheckmarkItem {
label: check_menu_item.label,
enabled: check_menu_item.enabled,
checked: check_menu_item.checked,
_ => ksni::menu::StandardItem {
label: menu_item.label.clone(),
enabled: menu_item.enabled,
icon_data: menu_item.icon.clone().unwrap_or_default(),
activate: Box::new(move |_| send_menu_event(&id)),
..Default::default()
}
.into()
.into(),
}
MenuItem::SubMenu(submenu) => ksni::menu::SubMenu {
label: submenu.label,
enabled: submenu.enabled,
submenu: submenu.submenu.into_iter().map(Into::into).collect(),
}
muda::CompatMenuItem::Checkmark(check_menu_item) => {
let id = check_menu_item.id.clone();
ksni::menu::CheckmarkItem {
label: check_menu_item.label.clone(),
enabled: check_menu_item.enabled,
checked: check_menu_item.checked,
activate: Box::new(move |_| send_menu_event(&id)),
..Default::default()
}
.into(),
MenuItem::Separator => ksni::menu::MenuItem::Separator,
.into()
}
muda::CompatMenuItem::SubMenu(submenu) => ksni::menu::SubMenu {
label: submenu.label.clone(),
enabled: submenu.enabled,
submenu: submenu
.submenu
.iter()
.cloned()
.map(muda_to_ksni_menu_item)
.collect(),
..Default::default()
}
.into(),
muda::CompatMenuItem::Separator => ksni::menu::MenuItem::Separator,
}
}

Expand Down
13 changes: 11 additions & 2 deletions src/platform_impl/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ mod icon;
mod menu;
mod tray;

use std::thread;

pub(crate) use icon::PlatformIcon;
use tray::Tray;

Expand All @@ -24,13 +26,20 @@ impl TrayIcon {
let menu = attrs
.menu
.as_ref()
.map(|menu| menu.items().into_iter().map(Into::into).collect())
.map(|menu| menu.compat_items())
.unwrap_or_default();

let tray_service = ksni::TrayService::new(Tray::new(id, icon, title, tooltip, menu));
let tray_handle = tray_service.handle();
tray_service.spawn();

let update_tray_handle = tray_handle.clone();
thread::spawn(move || {
while muda::recv_menu_update().is_ok() {
update_tray_handle.update(|_| {});
}
});

Ok(Self { tray_handle })
}

Expand All @@ -47,7 +56,7 @@ impl TrayIcon {
pub fn set_menu(&mut self, menu: Option<Box<dyn crate::menu::ContextMenu>>) {
let menu = menu
.as_ref()
.map(|menu| menu.items().into_iter().map(Into::into).collect())
.map(|menu| menu.compat_items())
.unwrap_or_default();

self.tray_handle.update(|tray| {
Expand Down
Loading

0 comments on commit fc73011

Please sign in to comment.