Skip to content

Commit 0a98eb3

Browse files
authored
refactor: system tray icons (#328)
1 parent 8af4d8f commit 0a98eb3

File tree

18 files changed

+201
-477
lines changed

18 files changed

+201
-477
lines changed

.changes/icons.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tao": "minor"
3+
---
4+
5+
**Breaking change** `SystemTrayBuilder::new` and `SystemTray::set_icon` now takes `system_tray::Icon` on all platforms.

Cargo.toml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,23 +62,25 @@ core-foundation = "0.9"
6262
core-graphics = "0.22"
6363
dispatch = "0.2"
6464
scopeguard = "1.1"
65+
png = "0.17"
6566

66-
[target."cfg(target_os = \"macos\")".dependencies.tao-core-video-sys]
67-
version = "0.2"
68-
default_features = false
69-
features = [ "display_link" ]
67+
[target."cfg(target_os = \"macos\")".dependencies.tao-core-video-sys]
68+
version = "0.2"
69+
default_features = false
70+
features = [ "display_link" ]
7071

7172
[target."cfg(target_os = \"macos\")".build-dependencies]
7273
cc = "1"
7374

7475
[target."cfg(target_os = \"windows\")".dependencies]
7576
parking_lot = "0.11"
7677
unicode-segmentation = "1.8.0"
78+
image = { version = "0.24", default-features = false }
7779
windows-implement = "0.37.0"
7880

79-
[target."cfg(target_os = \"windows\")".dependencies.windows]
80-
version = "0.37.0"
81-
features = [
81+
[target."cfg(target_os = \"windows\")".dependencies.windows]
82+
version = "0.37.0"
83+
features = [
8284
"alloc",
8385
"implement",
8486
"Win32_Devices_HumanInterfaceDevice",
@@ -120,3 +122,5 @@ gdkx11-sys = "0.15"
120122
gdk-pixbuf = { version = "0.15", features = [ "v2_36_8" ] }
121123
libappindicator = { version = "0.7", optional = true }
122124
x11-dl = "2.18"
125+
uuid = { version = "0.8", features = [ "v4" ] }
126+
png = "0.17"

examples/icon.ico

-4.06 KB
Binary file not shown.

examples/icon.png

215 Bytes
Loading

examples/icon_blue.ico

-4.31 KB
Binary file not shown.

examples/icon_dark.png

-1 KB
Binary file not shown.

examples/system_tray.rs

Lines changed: 30 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -4,183 +4,72 @@
44
// System tray is supported and availabled only if `tray` feature is enabled.
55
// Platform: Windows, Linux and macOS.
66
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
7-
#[cfg(feature = "tray")]
7+
#[cfg(any(feature = "tray", all(target_os = "linux", feature = "ayatana")))]
88
fn main() {
9-
use std::collections::HashMap;
10-
#[cfg(target_os = "linux")]
11-
use std::path::Path;
129
#[cfg(target_os = "macos")]
1310
use tao::platform::macos::{CustomMenuItemExtMacOS, NativeImage, SystemTrayBuilderExtMacOS};
1411
use tao::{
15-
event::{Event, WindowEvent},
12+
event::Event,
1613
event_loop::{ControlFlow, EventLoop},
1714
menu::{ContextMenu as Menu, MenuItemAttributes, MenuType},
1815
system_tray::SystemTrayBuilder,
19-
window::{Window, WindowId},
2016
};
2117

2218
env_logger::init();
2319
let event_loop = EventLoop::new();
24-
let mut windows: HashMap<WindowId, Window> = HashMap::new();
2520

2621
let mut tray_menu = Menu::new();
22+
let quit = tray_menu.add_item(MenuItemAttributes::new("Quit"));
2723

28-
let mut submenu = Menu::new();
24+
// You'll have to choose an icon size at your own discretion. On Linux, the icon should be
25+
// provided in whatever size it was naturally drawn; that is, don’t scale the image before passing
26+
// it to Tao. But on Windows, you will have to account for screen scaling. Here we use 32px,
27+
// since it seems to work well enough in most cases. Be careful about going too high, or
28+
// you'll be bitten by the low-quality downscaling built into the WM.
29+
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png");
2930

30-
// open new window menu item
31-
let open_new_window_element = submenu.add_item(MenuItemAttributes::new("Open new window"));
31+
let icon = load_icon(std::path::Path::new(path));
3232

33-
// set default icon
34-
#[cfg(target_os = "macos")]
35-
open_new_window_element
36-
.clone()
37-
.set_native_image(NativeImage::StatusAvailable);
38-
39-
// focus all window menu item
40-
let mut focus_all_window =
41-
tray_menu.add_item(MenuItemAttributes::new("Focus window").with_enabled(false));
42-
43-
let change_menu = tray_menu.add_item(MenuItemAttributes::new("Change menu"));
44-
45-
// inject submenu into tray_menu
46-
tray_menu.add_submenu("Sub menu", true, submenu);
47-
48-
// add quit button
49-
let quit_element = tray_menu.add_item(MenuItemAttributes::new("Quit"));
50-
51-
// Windows require Vec<u8> ICO file
52-
#[cfg(target_os = "windows")]
53-
let icon = include_bytes!("icon.ico").to_vec();
54-
// macOS require Vec<u8> PNG file
55-
#[cfg(target_os = "macos")]
56-
let icon = include_bytes!("icon.png").to_vec();
57-
// Linux require Pathbuf to PNG file
58-
#[cfg(target_os = "linux")]
59-
let icon = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/icon.png");
60-
61-
// Windows require Vec<u8> ICO file
62-
#[cfg(target_os = "windows")]
63-
let new_icon = include_bytes!("icon_blue.ico").to_vec();
64-
// macOS require Vec<u8> PNG file
65-
#[cfg(target_os = "macos")]
66-
let new_icon = include_bytes!("icon_dark.png").to_vec();
67-
// Linux require Pathbuf to PNG file
68-
#[cfg(target_os = "linux")]
69-
let new_icon = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/icon_dark.png");
70-
71-
// Menu is shown with left click on macOS and right click on Windows.
72-
#[cfg(target_os = "macos")]
73-
let mut system_tray = SystemTrayBuilder::new(icon.clone(), Some(tray_menu))
74-
.with_icon_as_template(true)
75-
.build(&event_loop)
76-
.unwrap();
77-
78-
#[cfg(not(target_os = "macos"))]
79-
let mut system_tray = SystemTrayBuilder::new(icon.clone(), Some(tray_menu))
33+
let system_tray = SystemTrayBuilder::new(icon, Some(tray_menu))
8034
.build(&event_loop)
8135
.unwrap();
8236

83-
event_loop.run(move |event, event_loop, control_flow| {
37+
event_loop.run(move |event, _event_loop, control_flow| {
8438
*control_flow = ControlFlow::Wait;
8539

86-
let mut create_window_or_focus = || {
87-
// if we already have one window, let's focus instead
88-
if !windows.is_empty() {
89-
for window in windows.values() {
90-
window.set_focus();
91-
}
92-
return;
93-
}
94-
95-
// create new window
96-
let mut open_new_window_element = open_new_window_element.clone();
97-
let mut focus_all_window = focus_all_window.clone();
98-
99-
let window = Window::new(event_loop).unwrap();
100-
windows.insert(window.id(), window);
101-
// disable button
102-
open_new_window_element.set_enabled(false);
103-
// change title (text)
104-
open_new_window_element.set_title("Window already open");
105-
println!(
106-
"Changed the menu item title: {}",
107-
open_new_window_element.title()
108-
);
109-
// set checked
110-
open_new_window_element.set_selected(true);
111-
// enable focus window
112-
focus_all_window.set_enabled(true);
113-
// update tray icon
114-
system_tray.set_icon(new_icon.clone());
115-
// add macOS Native red dot
116-
#[cfg(target_os = "macos")]
117-
open_new_window_element.set_native_image(NativeImage::StatusUnavailable);
118-
};
119-
12040
match event {
121-
Event::WindowEvent {
122-
event, window_id, ..
123-
} => {
124-
if event == WindowEvent::CloseRequested {
125-
let mut open_new_window_element = open_new_window_element.clone();
126-
// Remove window from our hashmap
127-
windows.remove(&window_id);
128-
// Modify our button's state
129-
open_new_window_element.set_enabled(true);
130-
focus_all_window.set_enabled(false);
131-
// Reset text
132-
open_new_window_element.set_title("Open new window");
133-
println!(
134-
"Reset the menu item title: {}",
135-
open_new_window_element.title()
136-
);
137-
// Set selected
138-
open_new_window_element.set_selected(false);
139-
// Change tray icon
140-
system_tray.set_icon(icon.clone());
141-
// macOS have native image available that we can use in our menu-items
142-
#[cfg(target_os = "macos")]
143-
open_new_window_element.set_native_image(NativeImage::StatusAvailable);
144-
}
145-
}
146-
// on Windows, habitually, we show the window with left click
147-
#[cfg(target_os = "windows")]
148-
Event::TrayEvent {
149-
event: tao::event::TrayEvent::LeftClick,
150-
..
151-
} => create_window_or_focus(),
152-
// left click on menu item
15341
Event::MenuEvent {
15442
menu_id,
15543
// specify only context menu's
15644
origin: MenuType::ContextMenu,
15745
..
15846
} => {
159-
// Click on Open new window or focus item
160-
if menu_id == open_new_window_element.clone().id()
161-
|| menu_id == focus_all_window.clone().id()
162-
{
163-
create_window_or_focus();
164-
}
165-
// click on `quit` item
166-
if menu_id == quit_element.clone().id() {
167-
// tell our app to close at the end of the loop.
47+
if menu_id == quit.clone().id() {
48+
// drop the system tray before exiting to remove the icon from system tray on Windows
49+
drop(&system_tray);
16850
*control_flow = ControlFlow::Exit;
16951
}
170-
171-
if menu_id == change_menu.clone().id() {
172-
let mut tray_menu = Menu::new();
173-
tray_menu.add_item(MenuItemAttributes::new("Quit"));
174-
system_tray.set_menu(&tray_menu);
175-
}
176-
177-
println!("Clicked on {:?}", menu_id);
17852
}
17953
_ => (),
18054
}
18155
});
18256
}
18357

58+
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
59+
#[cfg(any(feature = "tray", all(target_os = "linux", feature = "ayatana")))]
60+
fn load_icon(path: &std::path::Path) -> tao::system_tray::Icon {
61+
let (icon_rgba, icon_width, icon_height) = {
62+
let image = image::open(path)
63+
.expect("Failed to open icon path")
64+
.into_rgba8();
65+
let (width, height) = image.dimensions();
66+
let rgba = image.into_raw();
67+
(rgba, width, height)
68+
};
69+
tao::system_tray::Icon::from_rgba(icon_rgba, icon_width, icon_height)
70+
.expect("Failed to open icon")
71+
}
72+
18473
// System tray isn't supported on other's platforms.
18574
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
18675
fn main() {

0 commit comments

Comments
 (0)