1use 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
20const NW: i32 = 360;
22const NH: i32 = 170;
24const NM: i32 = 16;
26const NIS: i32 = 16;
28const WC: u32 = RGB(50, 57, 69);
30const TC: u32 = RGB(255, 255, 255);
32const 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#[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 pub fn new() -> Notification {
87 Notification::default()
88 }
89
90 pub fn appname(&mut self, appname: &str) -> &mut Notification {
92 self.appname = appname.to_owned();
93 self
94 }
95
96 pub fn summary(&mut self, summary: &str) -> &mut Notification {
100 self.summary = summary.to_owned();
101 self
102 }
103
104 pub fn body(&mut self, body: &str) -> &mut Notification {
110 self.body = body.to_owned();
111 self
112 }
113
114 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 pub fn timeout(&mut self, timeout: Timeout) -> &mut Notification {
135 self.timeout = timeout;
136 self
137 }
138
139 pub fn silent(&mut self, silent: bool) -> &mut Notification {
141 self.silent = silent;
142 self
143 }
144
145 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(), 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 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 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 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 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 if let Some(icon) = ¬ification.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 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 SetTextColor(hdc, TC);
365 let (hfont, old_hfont) = util::set_font(hdc, "Segeo UI", 15, 400);
366 let appname = util::encode_wide(¬ification.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 let (hfont, old_hfont) = util::set_font(hdc, "Segeo UI", 17, 700);
379 let summary = util::encode_wide(¬ification.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 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(¬ification.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 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}