Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / src
a37d6c4b· 固定源码快照
src/win32_tray.rs22.3 KB
1use std::cell::Cell;
2use std::error::Error;
3use std::io;
4use std::mem::size_of;
5use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU32, Ordering};
6use std::sync::{Arc, Mutex, MutexGuard};
7
8use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
9use windows::Win32::Graphics::Gdi::{
10 BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateBitmap, CreateDIBSection, DIB_RGB_COLORS,
11 DeleteObject, GetDC, ReleaseDC,
12};
13use windows::Win32::System::LibraryLoader::GetModuleHandleW;
14use windows::Win32::UI::Shell::{
15 NIF_ICON, NIF_MESSAGE, NIF_SHOWTIP, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY, NIM_SETFOCUS,
16 NIM_SETVERSION, NOTIFYICON_VERSION_4, NOTIFYICONDATAW, NOTIFYICONDATAW_0, NOTIFYICONIDENTIFIER,
17 Shell_NotifyIconGetRect, Shell_NotifyIconW,
18};
19use windows::Win32::UI::WindowsAndMessaging::{
20 AppendMenuW, ChangeWindowMessageFilterEx, CreateIconIndirect, CreatePopupMenu, CreateWindowExW,
21 DefWindowProcW, DestroyIcon, DestroyMenu, DestroyWindow, FindWindowW, GWLP_USERDATA,
22 GetWindowLongPtrW, HICON, HMENU, ICONINFO, KillTimer, MF_SEPARATOR, MF_STRING, MSGFLT_ALLOW,
23 PostMessageW, RegisterClassW, RegisterWindowMessageW, SetForegroundWindow, SetTimer,
24 SetWindowLongPtrW, TPM_BOTTOMALIGN, TPM_LEFTALIGN, TPM_RETURNCMD, TPM_RIGHTBUTTON,
25 TrackPopupMenu, WM_APP, WM_CONTEXTMENU, WM_NULL, WM_TIMER, WNDCLASSW, WS_EX_NOACTIVATE,
26 WS_EX_TOOLWINDOW, WS_POPUP,
27};
28use windows::core::{BOOL, PCWSTR, w};
29
30use crate::diagnostics::{self, Level};
31
32const CLASS_NAME: PCWSTR = w!("SbmsTrayHostWindow");
33const TRAY_ICON_ID: u32 = 1;
34const WM_TRAY_ICON: u32 = WM_APP + 1;
35const WM_TRAY_STATUS: u32 = WM_APP + 2;
36const NIN_SELECT: u32 = 0x0400;
37const NIN_KEYSELECT: u32 = NIN_SELECT + 1;
38const OPEN_COMMAND: u32 = 1;
39const EXIT_COMMAND: u32 = 2;
40const MAINTENANCE_TIMER_ID: usize = 1;
41const RETRY_DELAYS_MS: [u32; 6] = [250, 500, 1_000, 2_000, 4_000, 8_000];
42const RECOVERY_RETRY_MS: u32 = 30_000;
43const HEALTH_CHECK_MS: u32 = 5_000;
44
45static CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);
46static TASKBAR_CREATED_MESSAGE: AtomicU32 = AtomicU32::new(0);
47static OPEN_MESSAGE: AtomicU32 = AtomicU32::new(0);
48static CURRENT_HWND: AtomicIsize = AtomicIsize::new(0);
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum TrayAction {
52 Toggle,
53 Open,
54 Quit,
55}
56
57#[derive(Clone)]
58pub struct TrayHandle(Arc<TrayState>);
59
60impl TrayHandle {
61 pub fn set_status(&self, status: &str) {
62 let mut state = lock_state(&self.0);
63 state.tooltip = format!("SBMS · {status}");
64 let raw = state.hwnd;
65 if raw != 0 {
66 let _ = unsafe {
67 PostMessageW(
68 Some(HWND(raw as *mut _)),
69 WM_TRAY_STATUS,
70 WPARAM(0),
71 LPARAM(0),
72 )
73 };
74 }
75 }
76}
77
78pub struct NativeTray {
79 inner: Box<Inner>,
80}
81
82struct Inner {
83 hwnd: HWND,
84 hicon: HICON,
85 hmenu: HMENU,
86 state: Arc<TrayState>,
87 registered: Cell<bool>,
88 next_retry: Cell<usize>,
89 recovery_announced: Cell<bool>,
90 registration_reason: Cell<&'static str>,
91 action: Box<dyn Fn(TrayAction)>,
92}
93
94struct TrayState {
95 shared: Mutex<SharedTrayState>,
96}
97
98struct SharedTrayState {
99 hwnd: isize,
100 tooltip: String,
101}
102
103impl NativeTray {
104 pub fn new(action: impl Fn(TrayAction) + 'static) -> Result<Self, Box<dyn Error>> {
105 ensure_class_registered()?;
106 let taskbar_created = taskbar_created_message();
107 if taskbar_created == 0 {
108 return Err(io::Error::last_os_error().into());
109 }
110 let open_message = open_message();
111 if open_message == 0 {
112 return Err(io::Error::last_os_error().into());
113 }
114
115 let hinstance = unsafe { GetModuleHandleW(None) }?;
116 // TaskbarCreated is a broadcast and does not reach HWND_MESSAGE windows. A hidden
117 // tool window remains absent from the taskbar while receiving Explorer broadcasts.
118 let hwnd = unsafe {
119 CreateWindowExW(
120 WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
121 CLASS_NAME,
122 PCWSTR::null(),
123 WS_POPUP,
124 0,
125 0,
126 0,
127 0,
128 None,
129 None,
130 Some(hinstance.into()),
131 None,
132 )
133 }?;
134 // The tray runs elevated, while Explorer normally does not. Explicitly allow the
135 // shell callback and recreation messages through UIPI for this process-owned window.
136 for message in [taskbar_created, WM_TRAY_ICON, open_message] {
137 if let Err(error) =
138 unsafe { ChangeWindowMessageFilterEx(hwnd, message, MSGFLT_ALLOW, None) }
139 {
140 let _ = unsafe { DestroyWindow(hwnd) };
141 return Err(error.into());
142 }
143 }
144
145 let hicon = match create_hicon() {
146 Ok(icon) => icon,
147 Err(error) => {
148 let _ = unsafe { DestroyWindow(hwnd) };
149 return Err(error);
150 }
151 };
152 let hmenu = match create_menu() {
153 Ok(menu) => menu,
154 Err(error) => {
155 let _ = unsafe { DestroyIcon(hicon) };
156 let _ = unsafe { DestroyWindow(hwnd) };
157 return Err(error.into());
158 }
159 };
160
161 let state = Arc::new(TrayState {
162 shared: Mutex::new(SharedTrayState {
163 hwnd: hwnd.0 as isize,
164 tooltip: "SBMS · Stopped".into(),
165 }),
166 });
167 let inner = Box::new(Inner {
168 hwnd,
169 hicon,
170 hmenu,
171 state,
172 registered: Cell::new(false),
173 next_retry: Cell::new(0),
174 recovery_announced: Cell::new(false),
175 registration_reason: Cell::new("startup"),
176 action: Box::new(action),
177 });
178 unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, &*inner as *const Inner as _) };
179 CURRENT_HWND.store(hwnd.0 as isize, Ordering::Release);
180 inner.begin_registration("startup");
181
182 Ok(Self { inner })
183 }
184
185 pub fn handle(&self) -> TrayHandle {
186 TrayHandle(Arc::clone(&self.inner.state))
187 }
188}
189
190impl Drop for NativeTray {
191 fn drop(&mut self) {
192 let mut state = lock_state(&self.inner.state);
193 state.hwnd = 0;
194 unsafe {
195 let _ = KillTimer(Some(self.inner.hwnd), MAINTENANCE_TIMER_ID);
196 let _ = Shell_NotifyIconW(NIM_DELETE, &identity_data(self.inner.hwnd));
197 SetWindowLongPtrW(self.inner.hwnd, GWLP_USERDATA, 0);
198 let _ = DestroyMenu(self.inner.hmenu);
199 let _ = DestroyWindow(self.inner.hwnd);
200 let _ = DestroyIcon(self.inner.hicon);
201 }
202 drop(state);
203 let _ = CURRENT_HWND.compare_exchange(
204 self.inner.hwnd.0 as isize,
205 0,
206 Ordering::AcqRel,
207 Ordering::Acquire,
208 );
209 }
210}
211
212impl Inner {
213 fn begin_registration(&self, reason: &'static str) {
214 let _ = unsafe { KillTimer(Some(self.hwnd), MAINTENANCE_TIMER_ID) };
215 self.registration_reason.set(reason);
216 self.next_retry.set(0);
217 self.recovery_announced.set(false);
218 self.registered.set(false);
219
220 // Clear any stale identity before re-adding after a failed modify or an
221 // Explorer restart. NIM_DELETE is harmless when no icon exists.
222 let _ = unsafe { Shell_NotifyIconW(NIM_DELETE, &identity_data(self.hwnd)) };
223 if !self.try_register(1) {
224 self.schedule_retry();
225 }
226 }
227
228 fn try_register(&self, attempt: usize) -> bool {
229 let reason = self.registration_reason.get();
230 let state = lock_state(&self.state);
231 let data = notify_icon_data(self.hwnd, self.hicon, &state.tooltip);
232 drop(state);
233 if !unsafe { Shell_NotifyIconW(NIM_ADD, &data) }.as_bool() {
234 diagnostics::log(
235 Level::Warn,
236 "tray",
237 "registration-failed",
238 None,
239 format!("reason={reason} attempt={attempt}"),
240 );
241 return false;
242 }
243
244 let mut version = identity_data(self.hwnd);
245 version.Anonymous = NOTIFYICONDATAW_0 {
246 uVersion: NOTIFYICON_VERSION_4,
247 };
248 if !unsafe { Shell_NotifyIconW(NIM_SETVERSION, &version) }.as_bool() {
249 diagnostics::log(
250 Level::Warn,
251 "tray",
252 "set-version-failed",
253 None,
254 format!("reason={reason} attempt={attempt}"),
255 );
256 let _ = unsafe { Shell_NotifyIconW(NIM_DELETE, &identity_data(self.hwnd)) };
257 return false;
258 }
259
260 self.registered.set(true);
261 self.next_retry.set(0);
262 diagnostics::log(
263 Level::Info,
264 "tray",
265 "ready",
266 None,
267 format!("native tray icon registered reason={reason} attempt={attempt}"),
268 );
269 self.arm_timer(HEALTH_CHECK_MS, "health-check");
270 true
271 }
272
273 fn schedule_retry(&self) {
274 let retry = self.next_retry.get();
275 let delay = if let Some(delay) = RETRY_DELAYS_MS.get(retry).copied() {
276 self.next_retry.set(retry + 1);
277 delay
278 } else {
279 if !self.recovery_announced.replace(true) {
280 diagnostics::log(
281 Level::Warn,
282 "tray",
283 "registration-recovery-loop",
284 None,
285 format!(
286 "reason={} fast_attempts={} retry_ms={RECOVERY_RETRY_MS}",
287 self.registration_reason.get(),
288 RETRY_DELAYS_MS.len() + 1
289 ),
290 );
291 }
292 RECOVERY_RETRY_MS
293 };
294 self.arm_timer(delay, "registration-retry");
295 }
296
297 fn arm_timer(&self, delay: u32, purpose: &'static str) {
298 let timer = unsafe { SetTimer(Some(self.hwnd), MAINTENANCE_TIMER_ID, delay, None) };
299 if timer == 0 {
300 diagnostics::log(
301 Level::Error,
302 "tray",
303 "maintenance-timer-failed",
304 None,
305 format!("purpose={purpose} delay_ms={delay}"),
306 );
307 }
308 }
309
310 fn retry_registration(&self) {
311 let _ = unsafe { KillTimer(Some(self.hwnd), MAINTENANCE_TIMER_ID) };
312 let attempt = self.next_retry.get() + 1;
313 if !self.try_register(attempt) {
314 self.schedule_retry();
315 }
316 }
317
318 fn maintain_registration(&self) {
319 if !self.registered.get() {
320 self.retry_registration();
321 } else {
322 self.update_status("health-check");
323 }
324 }
325
326 fn update_status(&self, reason: &'static str) {
327 if !self.registered.get() {
328 return;
329 }
330
331 let state = lock_state(&self.state);
332 let data = notify_icon_data(self.hwnd, self.hicon, &state.tooltip);
333 drop(state);
334 if !unsafe { Shell_NotifyIconW(NIM_MODIFY, &data) }.as_bool() {
335 diagnostics::log(
336 Level::Warn,
337 "tray",
338 "icon-update-failed",
339 None,
340 format!("reason={reason}; tray icon is unavailable; registering it again"),
341 );
342 self.begin_registration(reason);
343 }
344 }
345}
346
347pub fn icon_rect() -> Option<RECT> {
348 let raw = CURRENT_HWND.load(Ordering::Acquire);
349 if raw == 0 {
350 return None;
351 }
352 icon_rect_for_hwnd(HWND(raw as *mut _))
353}
354
355pub(crate) fn find_host_window() -> Option<HWND> {
356 unsafe { FindWindowW(CLASS_NAME, PCWSTR::null()) }.ok()
357}
358
359pub(crate) fn request_open(hwnd: HWND) -> windows::core::Result<()> {
360 let message = open_message();
361 if message == 0 {
362 return Err(windows::core::Error::from_thread());
363 }
364 unsafe { PostMessageW(Some(hwnd), message, WPARAM(0), LPARAM(0)) }
365}
366
367fn icon_rect_for_hwnd(hwnd: HWND) -> Option<RECT> {
368 let identifier = NOTIFYICONIDENTIFIER {
369 cbSize: size_of::<NOTIFYICONIDENTIFIER>() as u32,
370 hWnd: hwnd,
371 uID: TRAY_ICON_ID,
372 ..Default::default()
373 };
374 unsafe { Shell_NotifyIconGetRect(&identifier) }.ok()
375}
376
377unsafe extern "system" fn wnd_proc(
378 hwnd: HWND,
379 message: u32,
380 wparam: WPARAM,
381 lparam: LPARAM,
382) -> LRESULT {
383 if message == WM_TRAY_ICON {
384 if let Some(inner) = unsafe { inner(hwnd) } {
385 let Some(event) = notification_event(lparam) else {
386 return LRESULT(0);
387 };
388 if is_toggle_event(event) {
389 diagnostics::log(
390 Level::Debug,
391 "tray",
392 "action",
393 None,
394 format!("toggle event=0x{event:04X}"),
395 );
396 (inner.action)(TrayAction::Toggle);
397 } else if is_context_event(event) {
398 diagnostics::log(
399 Level::Debug,
400 "tray",
401 "action",
402 None,
403 format!("context-menu event=0x{event:04X}"),
404 );
405 let point = tray_icon_center(hwnd).unwrap_or_else(|| callback_point(wparam));
406 show_menu(inner, point);
407 }
408 }
409 return LRESULT(0);
410 }
411
412 if message == WM_TRAY_STATUS {
413 if let Some(inner) = unsafe { inner(hwnd) } {
414 inner.update_status("status-update");
415 }
416 return LRESULT(0);
417 }
418
419 if message == open_message() && message != 0 {
420 if let Some(inner) = unsafe { inner(hwnd) } {
421 diagnostics::log(
422 Level::Debug,
423 "tray",
424 "action",
425 None,
426 "external open request",
427 );
428 (inner.action)(TrayAction::Open);
429 }
430 return LRESULT(0);
431 }
432
433 if message == WM_TIMER && wparam.0 == MAINTENANCE_TIMER_ID {
434 if let Some(inner) = unsafe { inner(hwnd) } {
435 inner.maintain_registration();
436 }
437 return LRESULT(0);
438 }
439
440 if message == taskbar_created_message() && message != 0 {
441 if let Some(inner) = unsafe { inner(hwnd) } {
442 diagnostics::log(
443 Level::Info,
444 "tray",
445 "taskbar-created",
446 None,
447 "Explorer notification area was recreated; registering tray icon again",
448 );
449 inner.begin_registration("taskbar-created");
450 }
451 return LRESULT(0);
452 }
453
454 unsafe { DefWindowProcW(hwnd, message, wparam, lparam) }
455}
456
457unsafe fn inner(hwnd: HWND) -> Option<&'static Inner> {
458 let pointer = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *const Inner;
459 unsafe { pointer.as_ref() }
460}
461
462fn show_menu(inner: &Inner, point: POINT) {
463 let _ = unsafe { SetForegroundWindow(inner.hwnd) };
464 let command = unsafe {
465 TrackPopupMenu(
466 inner.hmenu,
467 TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_LEFTALIGN | TPM_BOTTOMALIGN,
468 point.x,
469 point.y,
470 None,
471 inner.hwnd,
472 None,
473 )
474 }
475 .0 as u32;
476 let _ = unsafe { Shell_NotifyIconW(NIM_SETFOCUS, &identity_data(inner.hwnd)) };
477 // The foreground-window + WM_NULL sequence keeps native popup dismissal reliable.
478 let _ = unsafe { PostMessageW(Some(inner.hwnd), WM_NULL, WPARAM(0), LPARAM(0)) };
479 match command {
480 OPEN_COMMAND => {
481 diagnostics::log(Level::Debug, "tray", "menu", None, "open");
482 (inner.action)(TrayAction::Open);
483 }
484 EXIT_COMMAND => {
485 diagnostics::log(Level::Info, "tray", "menu", None, "exit");
486 (inner.action)(TrayAction::Quit);
487 }
488 _ => {}
489 }
490}
491
492fn callback_point(wparam: WPARAM) -> POINT {
493 let packed = wparam.0 as u32;
494 POINT {
495 x: i32::from(packed as u16 as i16),
496 y: i32::from((packed >> 16) as u16 as i16),
497 }
498}
499
500fn tray_icon_center(hwnd: HWND) -> Option<POINT> {
501 icon_rect_for_hwnd(hwnd).map(|rect| POINT {
502 x: rect.left + (rect.right - rect.left) / 2,
503 y: rect.top + (rect.bottom - rect.top) / 2,
504 })
505}
506
507fn is_toggle_event(event: u32) -> bool {
508 matches!(event, NIN_SELECT | NIN_KEYSELECT)
509}
510
511fn is_context_event(event: u32) -> bool {
512 event == WM_CONTEXTMENU
513}
514
515fn notification_event(lparam: LPARAM) -> Option<u32> {
516 let packed = lparam.0 as u32;
517 ((packed >> 16) == TRAY_ICON_ID).then_some(packed & 0xffff)
518}
519
520fn ensure_class_registered() -> windows::core::Result<()> {
521 if CLASS_REGISTERED.load(Ordering::Acquire) {
522 return Ok(());
523 }
524 let hinstance = unsafe { GetModuleHandleW(None) }?;
525 let class = WNDCLASSW {
526 lpfnWndProc: Some(wnd_proc),
527 hInstance: hinstance.into(),
528 lpszClassName: CLASS_NAME,
529 ..Default::default()
530 };
531 if unsafe { RegisterClassW(&class) } == 0 {
532 return Err(windows::core::Error::from_thread());
533 }
534 CLASS_REGISTERED.store(true, Ordering::Release);
535 Ok(())
536}
537
538fn lock_state(state: &TrayState) -> MutexGuard<'_, SharedTrayState> {
539 state
540 .shared
541 .lock()
542 .unwrap_or_else(|poisoned| poisoned.into_inner())
543}
544
545fn taskbar_created_message() -> u32 {
546 let cached = TASKBAR_CREATED_MESSAGE.load(Ordering::Acquire);
547 if cached != 0 {
548 return cached;
549 }
550 let message = unsafe { RegisterWindowMessageW(w!("TaskbarCreated")) };
551 TASKBAR_CREATED_MESSAGE.store(message, Ordering::Release);
552 message
553}
554
555fn open_message() -> u32 {
556 let cached = OPEN_MESSAGE.load(Ordering::Acquire);
557 if cached != 0 {
558 return cached;
559 }
560 let message = unsafe { RegisterWindowMessageW(w!("SBMS.Tray.Open.v1")) };
561 OPEN_MESSAGE.store(message, Ordering::Release);
562 message
563}
564
565fn create_menu() -> windows::core::Result<HMENU> {
566 let menu = unsafe { CreatePopupMenu() }?;
567 let result = unsafe { AppendMenuW(menu, MF_STRING, OPEN_COMMAND as usize, w!("Open SBMS")) }
568 .and_then(|_| unsafe { AppendMenuW(menu, MF_SEPARATOR, 0, PCWSTR::null()) })
569 .and_then(|_| unsafe { AppendMenuW(menu, MF_STRING, EXIT_COMMAND as usize, w!("Exit")) });
570 if let Err(error) = result {
571 let _ = unsafe { DestroyMenu(menu) };
572 return Err(error);
573 }
574 Ok(menu)
575}
576
577fn create_hicon() -> Result<HICON, Box<dyn Error>> {
578 let image = slint::Image::load_from_svg_data(include_bytes!("../ui/tray-icon.svg"))
579 .map_err(|_| io::Error::other("failed to decode the embedded tray icon"))?;
580 let pixels = image
581 .to_rgba8_premultiplied()
582 .ok_or_else(|| io::Error::other("failed to rasterize the embedded tray icon"))?;
583 let width = i32::try_from(pixels.width())?;
584 let height = i32::try_from(pixels.height())?;
585 let bitmap_info = BITMAPINFO {
586 bmiHeader: BITMAPINFOHEADER {
587 biSize: size_of::<BITMAPINFOHEADER>() as u32,
588 biWidth: width,
589 biHeight: -height,
590 biPlanes: 1,
591 biBitCount: 32,
592 biCompression: BI_RGB.0,
593 ..Default::default()
594 },
595 ..Default::default()
596 };
597
598 let dc = unsafe { GetDC(None) };
599 let mut bits = std::ptr::null_mut();
600 let color =
601 unsafe { CreateDIBSection(Some(dc), &bitmap_info, DIB_RGB_COLORS, &mut bits, None, 0) };
602 let _ = unsafe { ReleaseDC(None, dc) };
603 let color = color?;
604 if bits.is_null() {
605 let _ = unsafe { DeleteObject(color.into()) };
606 return Err(io::Error::other("tray icon bitmap has no pixel storage").into());
607 }
608
609 let rgba = pixels.as_bytes();
610 let destination = unsafe { std::slice::from_raw_parts_mut(bits as *mut u8, rgba.len()) };
611 for (source, destination) in rgba.chunks_exact(4).zip(destination.chunks_exact_mut(4)) {
612 destination.copy_from_slice(&[source[2], source[1], source[0], source[3]]);
613 }
614
615 let mask_stride = usize::try_from(width)?.div_ceil(16) * 2;
616 let mask_bits = vec![0u8; mask_stride * usize::try_from(height)?];
617 let mask = unsafe { CreateBitmap(width, height, 1, 1, Some(mask_bits.as_ptr().cast())) };
618 if mask.0.is_null() {
619 let _ = unsafe { DeleteObject(color.into()) };
620 return Err(io::Error::last_os_error().into());
621 }
622 let icon_info = ICONINFO {
623 fIcon: BOOL(1),
624 hbmMask: mask,
625 hbmColor: color,
626 ..Default::default()
627 };
628 let icon = unsafe { CreateIconIndirect(&icon_info) };
629 let _ = unsafe { DeleteObject(mask.into()) };
630 let _ = unsafe { DeleteObject(color.into()) };
631 Ok(icon?)
632}
633
634fn notify_icon_data(hwnd: HWND, hicon: HICON, tooltip: &str) -> NOTIFYICONDATAW {
635 let mut tip = [0u16; 128];
636 for (destination, source) in tip.iter_mut().take(127).zip(tooltip.encode_utf16()) {
637 *destination = source;
638 }
639 NOTIFYICONDATAW {
640 cbSize: size_of::<NOTIFYICONDATAW>() as u32,
641 hWnd: hwnd,
642 uID: TRAY_ICON_ID,
643 uFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_SHOWTIP,
644 uCallbackMessage: WM_TRAY_ICON,
645 hIcon: hicon,
646 szTip: tip,
647 ..Default::default()
648 }
649}
650
651fn identity_data(hwnd: HWND) -> NOTIFYICONDATAW {
652 NOTIFYICONDATAW {
653 cbSize: size_of::<NOTIFYICONDATAW>() as u32,
654 hWnd: hwnd,
655 uID: TRAY_ICON_ID,
656 ..Default::default()
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663
664 #[test]
665 fn retry_backoff_is_bounded_and_increasing() {
666 assert_eq!(RETRY_DELAYS_MS, [250, 500, 1_000, 2_000, 4_000, 8_000]);
667 assert!(RETRY_DELAYS_MS.windows(2).all(|pair| pair[0] < pair[1]));
668 assert!(RECOVERY_RETRY_MS > *RETRY_DELAYS_MS.last().unwrap());
669 }
670
671 #[test]
672 fn notification_events_preserve_mouse_and_keyboard_access() {
673 for event in [NIN_SELECT, NIN_KEYSELECT] {
674 assert!(is_toggle_event(event));
675 assert!(!is_context_event(event));
676 }
677 assert!(is_context_event(WM_CONTEXTMENU));
678 assert!(!is_toggle_event(WM_CONTEXTMENU));
679 }
680
681 #[test]
682 fn callback_coordinates_preserve_signed_virtual_screen_positions() {
683 let packed = ((-20_i16 as u16 as u32) << 16) | (-10_i16 as u16 as u32);
684 let point = callback_point(WPARAM(packed as usize));
685 assert_eq!((point.x, point.y), (-10, -20));
686 }
687
688 #[test]
689 fn version_four_notifications_require_the_expected_icon_id() {
690 let selected = LPARAM(((TRAY_ICON_ID << 16) | NIN_SELECT) as isize);
691 assert_eq!(notification_event(selected), Some(NIN_SELECT));
692
693 let foreign = LPARAM((((TRAY_ICON_ID + 1) << 16) | NIN_SELECT) as isize);
694 assert_eq!(notification_event(foreign), None);
695 }
696}
697