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.
文件
../ bininputuiconfig.rs81.2 KBcontrol.rs4.1 KBcontroller.rs43.6 KBdiagnostics.rs24.2 KBdisplay.rs29.8 KBgeometry.rs21.4 KBgpu_renderer.rs37.4 KBinput.rs79.7 KBlaunch_broker.rs10.3 KBlib.rs350 Blimits.rs714 Bmain.rs24.4 KBmapping.rs46.5 KBnetwork.rs9.2 KBrenderer.rs20.6 KBsession_gate.rs14.9 KBsunshine.rs12.2 KBui.rs68.5 KBvirtual_display.rs7.2 KBwin32_flyout.rs3.4 KBwin32_tray.rs22.3 KBwindow_migration.rs43.8 KBsrc/win32_tray.rs22.3 KB
1
use std::cell::Cell;2
use std::error::Error;3
use std::io;4
use std::mem::size_of;5
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU32, Ordering};6
use std::sync::{Arc, Mutex, MutexGuard};7
8
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};9
use windows::Win32::Graphics::Gdi::{10
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateBitmap, CreateDIBSection, DIB_RGB_COLORS,11
DeleteObject, GetDC, ReleaseDC,12
};13
use windows::Win32::System::LibraryLoader::GetModuleHandleW;14
use 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
};19
use 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
};28
use windows::core::{BOOL, PCWSTR, w};29
30
use crate::diagnostics::{self, Level};31
32
const CLASS_NAME: PCWSTR = w!("SbmsTrayHostWindow");33
const TRAY_ICON_ID: u32 = 1;34
const WM_TRAY_ICON: u32 = WM_APP + 1;35
const WM_TRAY_STATUS: u32 = WM_APP + 2;36
const NIN_SELECT: u32 = 0x0400;37
const NIN_KEYSELECT: u32 = NIN_SELECT + 1;38
const OPEN_COMMAND: u32 = 1;39
const EXIT_COMMAND: u32 = 2;40
const MAINTENANCE_TIMER_ID: usize = 1;41
const RETRY_DELAYS_MS: [u32; 6] = [250, 500, 1_000, 2_000, 4_000, 8_000];42
const RECOVERY_RETRY_MS: u32 = 30_000;43
const HEALTH_CHECK_MS: u32 = 5_000;44
45
static CLASS_REGISTERED: AtomicBool = AtomicBool::new(false);46
static TASKBAR_CREATED_MESSAGE: AtomicU32 = AtomicU32::new(0);47
static OPEN_MESSAGE: AtomicU32 = AtomicU32::new(0);48
static CURRENT_HWND: AtomicIsize = AtomicIsize::new(0);49
50
#[derive(Clone, Copy, Debug, Eq, PartialEq)]51
pub enum TrayAction {52
Toggle,53
Open,54
Quit,55
}56
57
#[derive(Clone)]58
pub struct TrayHandle(Arc<TrayState>);59
60
impl 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
78
pub struct NativeTray {79
inner: Box<Inner>,80
}81
82
struct 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
94
struct TrayState {95
shared: Mutex<SharedTrayState>,96
}97
98
struct SharedTrayState {99
hwnd: isize,100
tooltip: String,101
}102
103
impl 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 hidden117
// 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 the135
// 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
190
impl 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
212
impl 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 an221
// 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
true271
}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
delay278
} 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() + 1289
),290
);291
}292
RECOVERY_RETRY_MS293
};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
347
pub 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
355
pub(crate) fn find_host_window() -> Option<HWND> {356
unsafe { FindWindowW(CLASS_NAME, PCWSTR::null()) }.ok()357
}358
359
pub(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
367
fn 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
377
unsafe 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
457
unsafe 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
462
fn 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
492
fn 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
500
fn 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
507
fn is_toggle_event(event: u32) -> bool {508
matches!(event, NIN_SELECT | NIN_KEYSELECT)509
}510
511
fn is_context_event(event: u32) -> bool {512
event == WM_CONTEXTMENU513
}514
515
fn 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
520
fn 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
538
fn lock_state(state: &TrayState) -> MutexGuard<'_, SharedTrayState> {539
state540
.shared541
.lock()542
.unwrap_or_else(|poisoned| poisoned.into_inner())543
}544
545
fn 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
message553
}554
555
fn 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
message563
}564
565
fn 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
577
fn 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 = image581
.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
634
fn 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
651
fn 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)]661
mod 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