1mod icon;
6mod util;
7use std::ptr;
8
9use once_cell::sync::Lazy;
10use windows_sys::{
11 s,
12 Win32::{
13 Foundation::{FALSE, HWND, LPARAM, LRESULT, POINT, RECT, S_OK, TRUE, WPARAM},
14 UI::{
15 Shell::{
16 Shell_NotifyIconGetRect, Shell_NotifyIconW, NIF_ICON, NIF_MESSAGE, NIF_TIP,
17 NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW, NOTIFYICONIDENTIFIER,
18 },
19 WindowsAndMessaging::{
20 CreateWindowExW, DefWindowProcW, DestroyWindow, GetCursorPos, KillTimer,
21 RegisterClassW, RegisterWindowMessageA, SendMessageW, SetForegroundWindow,
22 SetTimer, TrackPopupMenu, CREATESTRUCTW, CW_USEDEFAULT, GWL_USERDATA, HICON, HMENU,
23 TPM_BOTTOMALIGN, TPM_LEFTALIGN, WM_CREATE, WM_DESTROY, WM_LBUTTONDBLCLK,
24 WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDBLCLK, WM_MBUTTONDOWN, WM_MBUTTONUP,
25 WM_MOUSEMOVE, WM_NCCREATE, WM_RBUTTONDBLCLK, WM_RBUTTONDOWN, WM_RBUTTONUP,
26 WM_TIMER, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
27 WS_EX_TRANSPARENT, WS_OVERLAPPED,
28 },
29 },
30 },
31};
32
33use crate::{
34 dpi::PhysicalPosition, icon::Icon, menu, MouseButton, MouseButtonState, Rect,
35 TrayIconAttributes, TrayIconEvent, TrayIconId, COUNTER,
36};
37
38pub(crate) use self::icon::WinIcon as PlatformIcon;
39
40const WM_USER_TRAYICON: u32 = 6002;
41const WM_USER_UPDATE_TRAYMENU: u32 = 6003;
42const WM_USER_UPDATE_TRAYICON: u32 = 6004;
43const WM_USER_SHOW_TRAYICON: u32 = 6005;
44const WM_USER_HIDE_TRAYICON: u32 = 6006;
45const WM_USER_UPDATE_TRAYTOOLTIP: u32 = 6007;
46const WM_USER_LEAVE_TIMER_ID: u32 = 6008;
47const WM_USER_SHOW_MENU_ON_LEFT_CLICK: u32 = 6009;
48static S_U_TASKBAR_RESTART: Lazy<u32> =
51 Lazy::new(|| unsafe { RegisterWindowMessageA(s!("TaskbarCreated")) });
52
53struct TrayUserData {
54 internal_id: u32,
55 id: TrayIconId,
56 hwnd: HWND,
57 hpopupmenu: Option<HMENU>,
58 icon: Option<Icon>,
59 tooltip: Option<String>,
60 entered: bool,
61 last_position: Option<PhysicalPosition<f64>>,
62 menu_on_left_click: bool,
63}
64
65pub struct TrayIcon {
66 hwnd: HWND,
67 menu: Option<Box<dyn menu::ContextMenu>>,
68 internal_id: u32,
69}
70
71impl TrayIcon {
72 pub fn new(id: TrayIconId, attrs: TrayIconAttributes) -> crate::Result<Self> {
73 let internal_id = COUNTER.next();
74
75 let class_name = util::encode_wide("tray_icon_app");
76 unsafe {
77 let hinstance = util::get_instance_handle();
78
79 let wnd_class = WNDCLASSW {
80 lpfnWndProc: Some(tray_proc),
81 lpszClassName: class_name.as_ptr(),
82 hInstance: hinstance,
83 ..std::mem::zeroed()
84 };
85
86 RegisterClassW(&wnd_class);
87
88 let traydata = TrayUserData {
89 id,
90 internal_id,
91 hwnd: std::ptr::null_mut(),
92 hpopupmenu: attrs.menu.as_ref().map(|m| m.hpopupmenu() as _),
93 icon: attrs.icon.clone(),
94 tooltip: attrs.tooltip.clone(),
95 entered: false,
96 last_position: None,
97 menu_on_left_click: attrs.menu_on_left_click,
98 };
99
100 let hwnd = CreateWindowExW(
101 WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED |
102 WS_EX_TOOLWINDOW,
110 class_name.as_ptr(),
111 ptr::null(),
112 WS_OVERLAPPED,
113 CW_USEDEFAULT,
114 0,
115 CW_USEDEFAULT,
116 0,
117 std::ptr::null_mut(),
118 std::ptr::null_mut(),
119 hinstance,
120 Box::into_raw(Box::new(traydata)) as _,
121 );
122 if hwnd.is_null() {
123 return Err(crate::Error::OsError(std::io::Error::last_os_error()));
124 }
125
126 let hicon = attrs.icon.as_ref().map(|i| i.inner.as_raw_handle());
127
128 if !register_tray_icon(hwnd, internal_id, &hicon, &attrs.tooltip) {
129 return Err(crate::Error::OsError(std::io::Error::last_os_error()));
130 }
131
132 if let Some(menu) = &attrs.menu {
133 menu.attach_menu_subclass_for_hwnd(hwnd as _);
134 }
135
136 Ok(Self {
137 hwnd,
138 internal_id,
139 menu: attrs.menu,
140 })
141 }
142 }
143
144 pub fn set_icon(&mut self, icon: Option<Icon>) -> crate::Result<()> {
145 unsafe {
146 let mut nid = NOTIFYICONDATAW {
147 uFlags: NIF_ICON,
148 hWnd: self.hwnd,
149 uID: self.internal_id,
150 ..std::mem::zeroed()
151 };
152
153 if let Some(hicon) = icon.as_ref().map(|i| i.inner.as_raw_handle()) {
154 nid.hIcon = hicon;
155 }
156
157 if Shell_NotifyIconW(NIM_MODIFY, &mut nid as _) == 0 {
158 return Err(crate::Error::OsError(std::io::Error::last_os_error()));
159 }
160
161 SendMessageW(
163 self.hwnd,
164 WM_USER_UPDATE_TRAYICON,
165 Box::into_raw(Box::new(icon)) as _,
166 0,
167 );
168 }
169
170 Ok(())
171 }
172
173 pub fn set_menu(&mut self, menu: Option<Box<dyn menu::ContextMenu>>) {
174 if let Some(menu) = &self.menu {
176 unsafe { menu.detach_menu_subclass_from_hwnd(self.hwnd as _) };
177 }
178 if let Some(menu) = &menu {
179 unsafe { menu.attach_menu_subclass_for_hwnd(self.hwnd as _) };
180 }
181
182 unsafe {
183 SendMessageW(
185 self.hwnd,
186 WM_USER_UPDATE_TRAYMENU,
187 Box::into_raw(Box::new(menu.as_ref().map(|m| m.hpopupmenu()))) as _,
188 0,
189 );
190 }
191
192 self.menu = menu;
193 }
194
195 pub fn set_tooltip<S: AsRef<str>>(&mut self, tooltip: Option<S>) -> crate::Result<()> {
196 unsafe {
197 let mut nid = NOTIFYICONDATAW {
198 uFlags: NIF_TIP,
199 hWnd: self.hwnd,
200 uID: self.internal_id,
201 ..std::mem::zeroed()
202 };
203 if let Some(tooltip) = &tooltip {
204 let tip = util::encode_wide(tooltip.as_ref());
205 #[allow(clippy::manual_memcpy)]
206 for i in 0..tip.len().min(128) {
207 nid.szTip[i] = tip[i];
208 }
209 }
210
211 if Shell_NotifyIconW(NIM_MODIFY, &mut nid as _) == 0 {
212 return Err(crate::Error::OsError(std::io::Error::last_os_error()));
213 }
214
215 SendMessageW(
217 self.hwnd,
218 WM_USER_UPDATE_TRAYTOOLTIP,
219 Box::into_raw(Box::new(tooltip.map(|t| t.as_ref().to_string()))) as _,
220 0,
221 );
222 }
223
224 Ok(())
225 }
226
227 pub fn set_show_menu_on_left_click(&mut self, enable: bool) {
228 unsafe {
229 SendMessageW(
230 self.hwnd,
231 WM_USER_SHOW_MENU_ON_LEFT_CLICK,
232 enable as usize,
233 0,
234 );
235 }
236 }
237
238 pub fn set_title<S: AsRef<str>>(&mut self, _title: Option<S>) {}
239
240 pub fn set_visible(&mut self, visible: bool) -> crate::Result<()> {
241 unsafe {
242 SendMessageW(
243 self.hwnd,
244 if visible {
245 WM_USER_SHOW_TRAYICON
246 } else {
247 WM_USER_HIDE_TRAYICON
248 },
249 0,
250 0,
251 );
252 }
253
254 Ok(())
255 }
256
257 pub fn rect(&self) -> Option<Rect> {
258 get_tray_rect(self.internal_id, self.hwnd).map(Into::into)
259 }
260}
261
262impl Drop for TrayIcon {
263 fn drop(&mut self) {
264 unsafe {
265 remove_tray_icon(self.hwnd, self.internal_id);
266
267 if let Some(menu) = &self.menu {
268 menu.detach_menu_subclass_from_hwnd(self.hwnd as _);
269 }
270
271 DestroyWindow(self.hwnd);
273 }
274 }
275}
276
277unsafe extern "system" fn tray_proc(
278 hwnd: HWND,
279 msg: u32,
280 wparam: WPARAM,
281 lparam: LPARAM,
282) -> LRESULT {
283 let userdata_ptr = unsafe { util::get_window_long(hwnd, GWL_USERDATA) };
284 let userdata_ptr = match (userdata_ptr, msg) {
285 (0, WM_NCCREATE) => {
286 let createstruct = unsafe { &mut *(lparam as *mut CREATESTRUCTW) };
287 let userdata = unsafe { &mut *(createstruct.lpCreateParams as *mut TrayUserData) };
288 userdata.hwnd = hwnd;
289 util::set_window_long(hwnd, GWL_USERDATA, createstruct.lpCreateParams as _);
290 return DefWindowProcW(hwnd, msg, wparam, lparam);
291 }
292 (0, WM_CREATE) => return -1,
295 (_, WM_CREATE) => return DefWindowProcW(hwnd, msg, wparam, lparam),
296 (0, _) => return DefWindowProcW(hwnd, msg, wparam, lparam),
297 _ => userdata_ptr as *mut TrayUserData,
298 };
299
300 let userdata = &mut *(userdata_ptr);
301
302 match msg {
303 WM_DESTROY => {
304 drop(Box::from_raw(userdata_ptr));
305 return 0;
306 }
307 WM_USER_UPDATE_TRAYMENU => {
308 let hpopupmenu = Box::from_raw(wparam as *mut Option<isize>);
309 userdata.hpopupmenu = (*hpopupmenu).map(|h| h as *mut _);
310 }
311 WM_USER_UPDATE_TRAYICON => {
312 let icon = Box::from_raw(wparam as *mut Option<Icon>);
313 userdata.icon = *icon;
314 }
315 WM_USER_SHOW_TRAYICON => {
316 register_tray_icon(
317 userdata.hwnd,
318 userdata.internal_id,
319 &userdata.icon.as_ref().map(|i| i.inner.as_raw_handle()),
320 &userdata.tooltip,
321 );
322 }
323 WM_USER_HIDE_TRAYICON => {
324 remove_tray_icon(userdata.hwnd, userdata.internal_id);
325 }
326 WM_USER_UPDATE_TRAYTOOLTIP => {
327 let tooltip = Box::from_raw(wparam as *mut Option<String>);
328 userdata.tooltip = *tooltip;
329 }
330 _ if msg == *S_U_TASKBAR_RESTART => {
331 remove_tray_icon(userdata.hwnd, userdata.internal_id);
332 register_tray_icon(
333 userdata.hwnd,
334 userdata.internal_id,
335 &userdata.icon.as_ref().map(|i| i.inner.as_raw_handle()),
336 &userdata.tooltip,
337 );
338 }
339 WM_USER_SHOW_MENU_ON_LEFT_CLICK => {
340 userdata.menu_on_left_click = wparam != 0;
341 }
342
343 WM_USER_TRAYICON
344 if matches!(
345 lparam as u32,
346 WM_LBUTTONDOWN
347 | WM_RBUTTONDOWN
348 | WM_MBUTTONDOWN
349 | WM_LBUTTONUP
350 | WM_RBUTTONUP
351 | WM_MBUTTONUP
352 | WM_LBUTTONDBLCLK
353 | WM_RBUTTONDBLCLK
354 | WM_MBUTTONDBLCLK
355 | WM_MOUSEMOVE
356 ) =>
357 {
358 let mut cursor = POINT { x: 0, y: 0 };
359 if GetCursorPos(&mut cursor as _) == 0 {
360 return 0;
361 }
362
363 let id = userdata.id.clone();
364 let position = PhysicalPosition::new(cursor.x as f64, cursor.y as f64);
365
366 let rect = match get_tray_rect(userdata.internal_id, hwnd) {
367 Some(rect) => Rect::from(rect),
368 None => return 0,
369 };
370
371 let event = match lparam as u32 {
372 WM_LBUTTONDOWN => TrayIconEvent::Click {
373 id,
374 rect,
375 position,
376 button: MouseButton::Left,
377 button_state: MouseButtonState::Down,
378 },
379 WM_RBUTTONDOWN => TrayIconEvent::Click {
380 id,
381 rect,
382 position,
383 button: MouseButton::Right,
384 button_state: MouseButtonState::Down,
385 },
386 WM_MBUTTONDOWN => TrayIconEvent::Click {
387 id,
388 rect,
389 position,
390 button: MouseButton::Middle,
391 button_state: MouseButtonState::Down,
392 },
393 WM_LBUTTONUP => TrayIconEvent::Click {
394 id,
395 rect,
396 position,
397 button: MouseButton::Left,
398 button_state: MouseButtonState::Up,
399 },
400 WM_RBUTTONUP => TrayIconEvent::Click {
401 id,
402 rect,
403 position,
404 button: MouseButton::Right,
405 button_state: MouseButtonState::Up,
406 },
407 WM_MBUTTONUP => TrayIconEvent::Click {
408 id,
409 rect,
410 position,
411 button: MouseButton::Middle,
412 button_state: MouseButtonState::Up,
413 },
414 WM_LBUTTONDBLCLK => TrayIconEvent::DoubleClick {
415 id,
416 rect,
417 position,
418 button: MouseButton::Left,
419 },
420 WM_RBUTTONDBLCLK => TrayIconEvent::DoubleClick {
421 id,
422 rect,
423 position,
424 button: MouseButton::Right,
425 },
426 WM_MBUTTONDBLCLK => TrayIconEvent::DoubleClick {
427 id,
428 rect,
429 position,
430 button: MouseButton::Middle,
431 },
432 WM_MOUSEMOVE if !userdata.entered => {
433 userdata.entered = true;
434 TrayIconEvent::Enter { id, rect, position }
435 }
436 WM_MOUSEMOVE if userdata.entered => {
437 let cursor_moved = userdata.last_position != Some(position);
439 userdata.last_position = Some(position);
440 if cursor_moved {
441 SetTimer(hwnd, WM_USER_LEAVE_TIMER_ID as _, 15, Some(tray_timer_proc));
443
444 TrayIconEvent::Move { id, rect, position }
445 } else {
446 return 0;
447 }
448 }
449
450 _ => unreachable!(),
451 };
452
453 TrayIconEvent::send(event);
454
455 if lparam as u32 == WM_RBUTTONDOWN
456 || (userdata.menu_on_left_click && lparam as u32 == WM_LBUTTONDOWN)
457 {
458 if let Some(menu) = userdata.hpopupmenu {
459 show_tray_menu(hwnd, menu, cursor.x, cursor.y);
460 }
461 }
462 }
463
464 WM_TIMER if wparam as u32 == WM_USER_LEAVE_TIMER_ID => {
465 if let Some(position) = userdata.last_position.take() {
466 let mut cursor = POINT { x: 0, y: 0 };
467 if GetCursorPos(&mut cursor as _) == 0 {
468 return 0;
469 }
470
471 let rect = match get_tray_rect(userdata.internal_id, hwnd) {
472 Some(r) => r,
473 None => return 0,
474 };
475
476 let in_x = (rect.left..rect.right).contains(&cursor.x);
477 let in_y = (rect.top..rect.bottom).contains(&cursor.y);
478
479 if !in_x || !in_y {
480 KillTimer(hwnd, WM_USER_LEAVE_TIMER_ID as _);
481 userdata.entered = false;
482
483 TrayIconEvent::send(TrayIconEvent::Leave {
484 id: userdata.id.clone(),
485 rect: rect.into(),
486 position,
487 });
488 }
489 }
490
491 return 0;
492 }
493
494 _ => {}
495 }
496
497 DefWindowProcW(hwnd, msg, wparam, lparam)
498}
499
500unsafe extern "system" fn tray_timer_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: u32) {
501 tray_proc(hwnd, msg, wparam, lparam as _);
502}
503
504#[inline]
505unsafe fn show_tray_menu(hwnd: HWND, menu: HMENU, x: i32, y: i32) {
506 SetForegroundWindow(hwnd);
509 TrackPopupMenu(
510 menu,
511 TPM_BOTTOMALIGN | TPM_LEFTALIGN,
513 x,
514 y,
515 0,
516 hwnd,
517 std::ptr::null_mut(),
518 );
519}
520
521#[inline]
522unsafe fn register_tray_icon(
523 hwnd: HWND,
524 tray_id: u32,
525 hicon: &Option<HICON>,
526 tooltip: &Option<String>,
527) -> bool {
528 let mut h_icon = std::ptr::null_mut();
529 let mut flags = NIF_MESSAGE;
530 let mut sz_tip: [u16; 128] = [0; 128];
531
532 if let Some(hicon) = hicon {
533 flags |= NIF_ICON;
534 h_icon = *hicon;
535 }
536
537 if let Some(tooltip) = tooltip {
538 flags |= NIF_TIP;
539 let tip = util::encode_wide(tooltip);
540 #[allow(clippy::manual_memcpy)]
541 for i in 0..tip.len().min(128) {
542 sz_tip[i] = tip[i];
543 }
544 }
545
546 let mut nid = NOTIFYICONDATAW {
547 uFlags: flags,
548 hWnd: hwnd,
549 uID: tray_id,
550 uCallbackMessage: WM_USER_TRAYICON,
551 hIcon: h_icon,
552 szTip: sz_tip,
553 ..std::mem::zeroed()
554 };
555
556 Shell_NotifyIconW(NIM_ADD, &mut nid as _) == TRUE
557}
558
559#[inline]
560unsafe fn remove_tray_icon(hwnd: HWND, id: u32) {
561 let mut nid = NOTIFYICONDATAW {
562 uFlags: NIF_ICON,
563 hWnd: hwnd,
564 uID: id,
565 ..std::mem::zeroed()
566 };
567
568 if Shell_NotifyIconW(NIM_DELETE, &mut nid as _) == FALSE {
569 eprintln!("Error removing system tray icon");
570 }
571}
572
573#[inline]
574fn get_tray_rect(id: u32, hwnd: HWND) -> Option<RECT> {
575 let nid = NOTIFYICONIDENTIFIER {
576 hWnd: hwnd,
577 cbSize: std::mem::size_of::<NOTIFYICONIDENTIFIER>() as _,
578 uID: id,
579 ..unsafe { std::mem::zeroed() }
580 };
581
582 let mut rect = RECT {
583 left: 0,
584 bottom: 0,
585 right: 0,
586 top: 0,
587 };
588 if unsafe { Shell_NotifyIconGetRect(&nid, &mut rect) } == S_OK {
589 Some(rect)
590 } else {
591 None
592 }
593}
594
595impl From<RECT> for Rect {
596 fn from(rect: RECT) -> Self {
597 Self {
598 position: crate::dpi::PhysicalPosition::new(rect.left.into(), rect.top.into()),
599 size: crate::dpi::PhysicalSize::new(
600 rect.right.saturating_sub(rect.left) as u32,
601 rect.bottom.saturating_sub(rect.top) as u32,
602 ),
603 }
604 }
605}