win7_notifications/
notification.rs

1// Copyright 2020-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use once_cell::sync::Lazy;
6use std::{ptr, sync::Mutex, thread, time::Duration};
7use windows_sys::{
8    w,
9    Win32::{
10        Foundation::*, Graphics::Gdi::*, Media::Audio::*, System::LibraryLoader::*,
11        UI::WindowsAndMessaging::*,
12    },
13};
14
15use crate::{
16    timeout::Timeout,
17    util::{self, GetWindowLongPtrW, SetWindowLongPtrW, GET_X_LPARAM, GET_Y_LPARAM, RGB},
18};
19
20/// notification width
21const NW: i32 = 360;
22/// notification height
23const NH: i32 = 170;
24/// notification margin
25const NM: i32 = 16;
26/// notification icon size (width or height)
27const NIS: i32 = 16;
28/// notification window bg color
29const WC: u32 = RGB(50, 57, 69);
30/// used for notification summary (title)
31const TC: u32 = RGB(255, 255, 255);
32/// used for notification body
33const SC: u32 = RGB(200, 200, 200);
34
35const CLOSE_BTN_RECT: RECT = RECT {
36    left: NW - NM - NM / 2,
37    top: NM,
38    right: (NW - NM - NM / 2) + 8,
39    bottom: NM + 8,
40};
41const CLOSE_BTN_RECT_EXTRA: RECT = RECT {
42    left: CLOSE_BTN_RECT.left - 8,
43    top: CLOSE_BTN_RECT.top - 8,
44    right: CLOSE_BTN_RECT.right + 8,
45    bottom: CLOSE_BTN_RECT.bottom + 8,
46};
47
48static ACTIVE_NOTIFICATIONS: Lazy<Mutex<Vec<isize>>> = Lazy::new(|| Mutex::new(Vec::new()));
49static PRIMARY_MONITOR: Lazy<Mutex<MONITORINFOEXW>> =
50    Lazy::new(|| unsafe { Mutex::new(util::get_monitor_info(util::primary_monitor())) });
51
52/// Describes The notification
53#[non_exhaustive]
54#[derive(Debug, Clone)]
55pub struct Notification {
56    pub icon: Option<Vec<u8>>,
57    pub icon_width: u32,
58    pub icon_height: u32,
59    pub appname: String,
60    pub summary: String,
61    pub body: String,
62    pub timeout: Timeout,
63    pub silent: bool,
64}
65
66impl Default for Notification {
67    fn default() -> Notification {
68        Notification {
69            appname: util::current_exe_name(),
70            summary: String::new(),
71            body: String::new(),
72            icon: None,
73            icon_height: 32,
74            icon_width: 32,
75            timeout: Timeout::Default,
76            silent: false,
77        }
78    }
79}
80
81impl Notification {
82    /// Constructs a new Notification.
83    ///
84    /// Most fields are empty by default, only `appname` is initialized with the name of the current
85    /// executable.
86    pub fn new() -> Notification {
87        Notification::default()
88    }
89
90    /// Overwrite the appname field used for Notification.
91    pub fn appname(&mut self, appname: &str) -> &mut Notification {
92        self.appname = appname.to_owned();
93        self
94    }
95
96    /// Set the `summary`.
97    ///
98    /// Often acts as title of the notification. For more elaborate content use the `body` field.
99    pub fn summary(&mut self, summary: &str) -> &mut Notification {
100        self.summary = summary.to_owned();
101        self
102    }
103
104    /// Set the content of the `body` field.
105    ///
106    /// Multiline textual content of the notification.
107    /// Each line should be treated as a paragraph.
108    /// html markup is not supported.
109    pub fn body(&mut self, body: &str) -> &mut Notification {
110        self.body = body.to_owned();
111        self
112    }
113
114    /// Set the `icon` field from 32bpp RGBA data.
115    ///
116    /// The length of `rgba` must be divisible by 4, and `width * height` must equal
117    /// `rgba.len() / 4`. Otherwise, this will panic.
118    pub fn icon(&mut self, rgba: Vec<u8>, width: u32, height: u32) -> &mut Notification {
119        if rgba.len() % util::PIXEL_SIZE != 0 {
120            panic!("The length of `rgba` is not divisible by 4");
121        }
122        let pixel_count = rgba.len() / util::PIXEL_SIZE;
123        if pixel_count != (width * height) as usize {
124            panic!("`width * height` is not equal `rgba.len() / 4`");
125        } else {
126            self.icon = Some(rgba);
127            self.icon_width = width;
128            self.icon_height = height;
129        }
130        self
131    }
132
133    /// Set the `timeout` field.
134    pub fn timeout(&mut self, timeout: Timeout) -> &mut Notification {
135        self.timeout = timeout;
136        self
137    }
138
139    /// Set the `silent` field.
140    pub fn silent(&mut self, silent: bool) -> &mut Notification {
141        self.silent = silent;
142        self
143    }
144
145    /// Shows the Notification.
146    ///
147    /// Requires a win32 event_loop to be running on the thread, otherwise the notification will close immediately.
148    pub fn show(&self) -> Result<(), u32> {
149        unsafe {
150            let hinstance = GetModuleHandleW(ptr::null());
151
152            let class_name = w!("win7-notifications");
153            let wnd_class = WNDCLASSEXW {
154                lpfnWndProc: Some(window_proc),
155                lpszClassName: class_name,
156                hInstance: hinstance,
157                hbrBackground: CreateSolidBrush(WC),
158                cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
159                style: CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
160                cbClsExtra: 0,
161                cbWndExtra: 0,
162                hIcon: std::ptr::null_mut(),
163                hCursor: std::ptr::null_mut(), // must be null in order for cursor state to work properly
164                lpszMenuName: ptr::null(),
165                hIconSm: std::ptr::null_mut(),
166            };
167            RegisterClassExW(&wnd_class);
168
169            if let Ok(pm) = PRIMARY_MONITOR.lock() {
170                let RECT { right, bottom, .. } = pm.monitorInfo.rcWork;
171
172                let data = WindowData {
173                    window: std::ptr::null_mut(),
174                    mouse_hovering_close_btn: false,
175                    notification: self.clone(),
176                };
177
178                let hwnd = CreateWindowExW(
179                    WS_EX_TOPMOST | WS_EX_NOACTIVATE,
180                    class_name,
181                    w!("win7-notifications-window"),
182                    WS_POPUP | WS_BORDER,
183                    right - NW - 15,
184                    bottom - NH - 15,
185                    NW,
186                    NH,
187                    std::ptr::null_mut(),
188                    std::ptr::null_mut(),
189                    hinstance,
190                    Box::into_raw(Box::new(data)) as _,
191                );
192
193                if hwnd.is_null() {
194                    return Err(GetLastError());
195                }
196
197                // reposition active notifications and make room for new one
198                if let Ok(mut active_notifications) = ACTIVE_NOTIFICATIONS.lock() {
199                    active_notifications.push(hwnd as _);
200                    reposition_notifications(&active_notifications, right, bottom)
201                }
202
203                ShowWindow(hwnd, SW_SHOWNA);
204                if !self.silent {
205                    // Passing an invalid path to `PlaySoundW` will make windows play default sound.
206                    // https://docs.microsoft.com/en-us/previous-versions/dd743680(v=vs.85)#remarks
207                    PlaySoundW(w!("null"), hinstance, SND_ASYNC);
208                }
209
210                let timeout = self.timeout;
211                let hwnd = hwnd as isize;
212                thread::spawn(move || {
213                    thread::sleep(Duration::from_millis(timeout.into()));
214                    if timeout != Timeout::Never {
215                        close_notification(hwnd);
216                    };
217                });
218            }
219        }
220
221        Ok(())
222    }
223}
224
225unsafe fn close_notification(hwnd: isize) {
226    ShowWindow(hwnd as _, SW_HIDE);
227    CloseWindow(hwnd as _);
228
229    // We can NOT call `DestroyWindow` from this window
230    // Sending WM_CLOSE will by default make the windows call it on itself.
231    // Note WM_DESTROY should not be sent directly as it would create a leak
232    // see https://devblogs.microsoft.com/oldnewthing/20110926-00/?p=9553
233    SendMessageA(hwnd as _, WM_CLOSE, 0, 0);
234
235    if let Ok(mut active_notifications) = ACTIVE_NOTIFICATIONS.lock() {
236        if let Some(index) = active_notifications.iter().position(|e| *e == hwnd) {
237            active_notifications.remove(index);
238        }
239
240        // reposition notifications
241        if let Ok(pm) = PRIMARY_MONITOR.lock() {
242            let RECT { right, bottom, .. } = pm.monitorInfo.rcWork;
243            reposition_notifications(&active_notifications, right, bottom);
244        }
245    }
246}
247
248#[inline]
249unsafe fn reposition_notifications(notifications: &[isize], right: i32, bottom: i32) {
250    let mut i = notifications.len() as i32;
251    for &hwnd in notifications.iter() {
252        SetWindowPos(
253            hwnd as _,
254            std::ptr::null_mut(),
255            right - NW - 15,
256            bottom - 15 - (NH * i) - 10 * (i - 1),
257            0,
258            0,
259            SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER,
260        );
261        i -= 1;
262    }
263}
264
265struct WindowData {
266    window: HWND,
267    notification: Notification,
268    mouse_hovering_close_btn: bool,
269}
270
271pub unsafe extern "system" fn window_proc(
272    hwnd: HWND,
273    msg: u32,
274    wparam: WPARAM,
275    lparam: LPARAM,
276) -> LRESULT {
277    let mut userdata = GetWindowLongPtrW(hwnd, GWL_USERDATA);
278
279    match msg {
280        WM_NCCREATE => {
281            if userdata == 0 {
282                let createstruct = &*(lparam as *const CREATESTRUCTW);
283                userdata = createstruct.lpCreateParams as isize;
284                SetWindowLongPtrW(hwnd, GWL_USERDATA, userdata);
285            }
286            DefWindowProcW(hwnd, msg, wparam, lparam)
287        }
288
289        WM_CREATE => {
290            let userdata = userdata as *mut WindowData;
291            (*userdata).window = hwnd;
292            DefWindowProcW(hwnd, msg, wparam, lparam)
293        }
294
295        WM_PAINT | WM_PRINTCLIENT => {
296            let userdata = userdata as *mut WindowData;
297            let notification = &(*userdata).notification;
298
299            let mut ps = PAINTSTRUCT {
300                fErase: 0,
301                fIncUpdate: 0,
302                fRestore: 0,
303                hdc: std::ptr::null_mut(),
304                rcPaint: RECT {
305                    bottom: 0,
306                    left: 0,
307                    right: 0,
308                    top: 0,
309                },
310                rgbReserved: [0; 32],
311            };
312            let hdc = BeginPaint(hwnd, &mut ps);
313            SetBkColor(hdc, WC);
314
315            // draw notification icon
316            if let Some(icon) = &notification.icon {
317                let hicon = util::get_hicon_from_32bpp_rgba(
318                    icon.clone(),
319                    notification.icon_width,
320                    notification.icon_height,
321                );
322                DrawIconEx(
323                    hdc,
324                    NM,
325                    NM,
326                    hicon,
327                    NIS,
328                    NIS,
329                    0,
330                    std::ptr::null_mut(),
331                    DI_NORMAL,
332                );
333            }
334
335            // draw notification close button
336            let hpen = CreatePen(
337                PS_SOLID,
338                2,
339                if (*userdata).mouse_hovering_close_btn {
340                    TC
341                } else {
342                    SC
343                },
344            );
345            let old_hpen = SelectObject(hdc, hpen);
346            MoveToEx(
347                hdc,
348                CLOSE_BTN_RECT.left,
349                CLOSE_BTN_RECT.top,
350                std::ptr::null_mut(),
351            );
352            LineTo(hdc, CLOSE_BTN_RECT.right, CLOSE_BTN_RECT.bottom);
353            MoveToEx(
354                hdc,
355                CLOSE_BTN_RECT.right,
356                CLOSE_BTN_RECT.top,
357                std::ptr::null_mut(),
358            );
359            LineTo(hdc, CLOSE_BTN_RECT.left, CLOSE_BTN_RECT.bottom);
360            SelectObject(hdc, old_hpen);
361            DeleteObject(hpen);
362
363            // draw notification app name
364            SetTextColor(hdc, TC);
365            let (hfont, old_hfont) = util::set_font(hdc, "Segeo UI", 15, 400);
366            let appname = util::encode_wide(&notification.appname);
367            TextOutW(
368                hdc,
369                NM + NIS + (NM / 2),
370                NM,
371                appname.as_ptr(),
372                appname.len() as _,
373            );
374            SelectObject(hdc, old_hfont);
375            DeleteObject(hfont);
376
377            // draw notification summary (title)
378            let (hfont, old_hfont) = util::set_font(hdc, "Segeo UI", 17, 700);
379            let summary = util::encode_wide(&notification.summary);
380            TextOutW(
381                hdc,
382                NM,
383                NM + NIS + (NM / 2),
384                summary.as_ptr(),
385                summary.len() as _,
386            );
387            SelectObject(hdc, old_hfont);
388            DeleteObject(hfont);
389
390            // draw notification body
391            SetTextColor(hdc, SC);
392            let (hfont, old_hfont) = util::set_font(hdc, "Segeo UI", 17, 400);
393            let mut rc = RECT {
394                left: NM,
395                top: NM + NIS + (NM / 2) + 17 + (NM / 2),
396                right: NW - NM,
397                bottom: NH - NM,
398            };
399            let mut body = util::encode_wide(&notification.body);
400            DrawTextW(
401                hdc,
402                body.as_mut_ptr(),
403                body.len() as _,
404                &mut rc,
405                DT_LEFT | DT_EXTERNALLEADING | DT_WORDBREAK,
406            );
407            SelectObject(hdc, old_hfont);
408            DeleteObject(hfont);
409
410            EndPaint(hdc, &ps);
411            DefWindowProcW(hwnd, msg, wparam, lparam)
412        }
413
414        WM_MOUSEMOVE => {
415            let userdata = userdata as *mut WindowData;
416
417            let (x, y) = (GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam));
418            let hit = util::rect_contains(CLOSE_BTN_RECT_EXTRA, x as i32, y as i32);
419
420            SetCursor(LoadCursorW(
421                std::ptr::null_mut(),
422                if hit { IDC_HAND } else { IDC_ARROW },
423            ));
424            if hit != (*userdata).mouse_hovering_close_btn {
425                // only trigger redraw if the previous state is different than the new state
426                InvalidateRect(hwnd, std::ptr::null(), 0);
427            }
428            (*userdata).mouse_hovering_close_btn = hit;
429
430            DefWindowProcW(hwnd, msg, wparam, lparam)
431        }
432
433        WM_LBUTTONDOWN => {
434            let (x, y) = (GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam));
435
436            if util::rect_contains(CLOSE_BTN_RECT_EXTRA, x as i32, y as i32) {
437                close_notification(hwnd as _)
438            }
439
440            DefWindowProcW(hwnd, msg, wparam, lparam)
441        }
442
443        WM_DESTROY => {
444            let userdata = userdata as *mut WindowData;
445            drop(Box::from_raw(userdata));
446
447            DefWindowProcW(hwnd, msg, wparam, lparam)
448        }
449        _ => DefWindowProcW(hwnd, msg, wparam, lparam),
450    }
451}