tray_icon/platform_impl/windows/
mod.rs

1// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5mod 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;
48/// When the taskbar is created, it registers a message with the "TaskbarCreated" string and then broadcasts this message to all top-level windows
49/// When the application receives this message, it should assume that any taskbar icons it added have been removed and add them again.
50static 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 prevents this window from ever showing up in the taskbar, which
103            // we want to avoid. If you remove this style, this window won't show up in the
104            // taskbar *initially*, but it can show up at some later point. This can sometimes
105            // happen on its own after several hours have passed, although this has proven
106            // difficult to reproduce. Alternatively, it can be manually triggered by killing
107            // `explorer.exe` and then starting the process back up.
108            // It is unclear why the bug is triggered by waiting for several hours.
109            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            // send the new icon to the subclass proc to store it in the tray data
162            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        // Safety: self.hwnd is valid as long as as the TrayIcon is
175        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            // send the new menu to the subclass proc where we will update there
184            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            // send the new tooltip to the subclass proc to store it in the tray data
216            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            // destroy the hidden window used by the tray
272            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        // Getting here should quite frankly be impossible,
293        // but we'll make window creation fail here just in case.
294        (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                    // handle extra WM_MOUSEMOVE events, ignore if position hasn't changed
438                    let cursor_moved = userdata.last_position != Some(position);
439                    userdata.last_position = Some(position);
440                    if cursor_moved {
441                        // Set or update existing timer, where we check if cursor left
442                        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    // bring the hidden window to the foreground so the pop up menu
507    // would automatically hide on click outside
508    SetForegroundWindow(hwnd);
509    TrackPopupMenu(
510        menu,
511        // align bottom / right, maybe we could expose this later..
512        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}