From 1976ed646613cdd85d1b55e1131faf9e884431b8 Mon Sep 17 00:00:00 2001 From: lars-berger Date: Thu, 30 Jan 2025 00:24:10 +0800 Subject: [PATCH] working test setup --- Cargo.lock | 1 + Cargo.toml | 3 + crates/systray-util/Cargo.toml | 9 ++ crates/systray-util/src/main.rs | 7 ++ crates/systray-util/src/tray.rs | 215 +++++++++++++++++--------------- crates/systray-util/src/util.rs | 100 ++++++++++++++- 6 files changed, 231 insertions(+), 104 deletions(-) create mode 100644 crates/systray-util/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index fee7a61d..feef3078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5366,6 +5366,7 @@ dependencies = [ "thiserror 2.0.11", "tokio", "tracing", + "tracing-subscriber", "windows 0.58.0", "windows-core 0.58.0", ] diff --git a/Cargo.toml b/Cargo.toml index a6170f8b..04e4d65b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,14 @@ windows = { version = "0.58", features = [ "Media_Control", "Win32_Devices_FunctionDiscovery", "Win32_Globalization", + "Win32_Graphics_Gdi", "Win32_Media", "Win32_Media_Audio", "Win32_Media_Audio_Endpoints", "Win32_System_Console", + "Win32_System_DataExchange", "Win32_System_SystemServices", + "Win32_System_Threading", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_TextServices", diff --git a/crates/systray-util/Cargo.toml b/crates/systray-util/Cargo.toml index 60331d0f..e13e0083 100644 --- a/crates/systray-util/Cargo.toml +++ b/crates/systray-util/Cargo.toml @@ -6,11 +6,20 @@ license = "" repository = "" edition = "2021" +[lib] +path = "src/lib.rs" + +# TODO: Remove this after testing. +[[bin]] +name = "systray-util" +path = "src/main.rs" + [dependencies] serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } thiserror = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/systray-util/src/main.rs b/crates/systray-util/src/main.rs new file mode 100644 index 00000000..98e7a29e --- /dev/null +++ b/crates/systray-util/src/main.rs @@ -0,0 +1,7 @@ +use systray_util::run; + +fn main() -> systray_util::Result<()> { + tracing_subscriber::fmt().init(); + + run() +} diff --git a/crates/systray-util/src/tray.rs b/crates/systray-util/src/tray.rs index d5b4fcda..cd62c326 100644 --- a/crates/systray-util/src/tray.rs +++ b/crates/systray-util/src/tray.rs @@ -4,11 +4,52 @@ use tokio::sync::mpsc; use windows::Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM}, System::DataExchange::COPYDATASTRUCT, - UI::WindowsAndMessaging::WM_COPYDATA, + UI::{ + Shell::{ + NOTIFYICONDATAW_0, NOTIFY_ICON_DATA_FLAGS, + NOTIFY_ICON_INFOTIP_FLAGS, NOTIFY_ICON_STATE, + }, + WindowsAndMessaging::{ + DefWindowProcW, SendMessageW, SetTimer, SetWindowPos, HICON, + HWND_TOPMOST, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, WM_COPYDATA, + WM_TIMER, + }, + }, }; use crate::Util; +#[repr(C)] +struct ShellTrayData { + dw_magic: i32, + dw_message: u32, + nid: NotifyIconDataFixed, +} + +#[repr(C)] +struct NotifyIconDataFixed { + cb_size: u32, + hwnd_raw: u32, + uid: u32, + flags: NOTIFY_ICON_DATA_FLAGS, + callback_message: u32, + hicon_raw: u32, + sz_tip: [u16; 128], + state: NOTIFY_ICON_STATE, + state_mask: NOTIFY_ICON_STATE, + sz_info: [u16; 256], + union: NOTIFYICONDATAW_0, + sz_info_title: [u16; 64], + info_flags: NOTIFY_ICON_INFOTIP_FLAGS, + guid: windows::core::GUID, + balloon_icon: HICON, +} + +enum PlatformEvent { + // TODO: Add proper type. + TrayUpdate(String), +} + /// Global instance of sender for platform events. /// /// For use with window procedure. @@ -26,123 +67,95 @@ pub fn run() -> crate::Result<()> { let window = Util::create_message_window("Shell_TrayWnd", Some(window_proc))?; + // TODO: Check whether this can be done in a better way. Check out + // SimpleClassicTheme.Taskbar project for potential implementation. + unsafe { SetTimer(HWND(window as _), 1, 100, None) }; + Util::run_message_loop(); Ok(()) } -unsafe extern "system" fn window_proc( +extern "system" fn window_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { match msg { - WM_COPYDATA => { - tracing::info!("Incoming WM_COPYDATA message."); - - let copy_data = match (lparam.0 as *const COPYDATASTRUCT).as_ref() { - Some(data) => { - println!(" Got COPYDATASTRUCT"); - data - } - None => { - println!(" Invalid COPYDATASTRUCT pointer"); - return LRESULT(0); - } + WM_COPYDATA => handle_copy_data(hwnd, wparam, lparam), + WM_TIMER => { + // Regain tray priority. + let _ = unsafe { + SetWindowPos( + hwnd, + HWND_TOPMOST, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ) }; - // Forward to real tray first - let fwd_result = if let Some(real_tray) = find_real_tray(hwnd) { - println!(" Forwarding to real tray: {:?}", real_tray); - SendMessageW(real_tray, WM_COPYDATA, wparam, lparam) - } else { - println!(" No real tray found"); - LRESULT(1) - }; + LRESULT(0) + } + _ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }, + } +} - if copy_data.dwData == 1 && !copy_data.lpData.is_null() { - println!(" Processing tray data"); - let tray_data = - std::mem::transmute::<_, &ShellTrayData>(copy_data.lpData); - - println!( - " Icon data - hwnd: {:#x}, id: {}, flags: {:#x}", - tray_data.nid.hwnd_raw, tray_data.nid.uid, tray_data.nid.flags.0 - ); - - let window_data_ptr = - GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowData; - if !window_data_ptr.is_null() { - println!(" Got window data pointer"); - let window_data = &mut *window_data_ptr; - - // Update mappings - window_data.update_icon_mapping( - tray_data.nid.hwnd_raw, - tray_data.nid.uid, - ); - - // Get icon data - if let Some(base64_icon) = - get_icon_base64(HICON(tray_data.nid.hicon_raw as *mut _)) - { - println!( - " Successfully got icon base64 data, length: {}", - base64_icon.len() - ); - - // Create the payload - let payload = json!({ - "header": { - "magic": tray_data.dw_magic, - "message_type": tray_data.dw_message - }, - "nid": { - "uID": tray_data.nid.uid, - "hWnd": format!("{:x}", tray_data.nid.hwnd_raw), - "uFlags": tray_data.nid.flags.0, - "szTip": String::from_utf16_lossy(&tray_data.nid.sz_tip) - .trim_matches(|c| c == '\0' || c == '\u{1}') - .to_string(), - "icon_data": base64_icon - } - }); - - println!(" Emitting tray-update event"); - if let Err(e) = - window_data.app_handle.emit("tray-update", payload) - { - println!(" Error emitting event: {:?}", e); - } else { - println!(" Event emitted successfully"); - } - } else { - println!(" Failed to get icon data"); - } - } else { - println!(" Window data pointer is null"); - } - } else { - println!(" Invalid or empty tray data"); +fn handle_copy_data( + hwnd: HWND, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + tracing::info!("Incoming `WM_COPYDATA` message."); + + // Extract COPYDATASTRUCT. + let copy_data = + match unsafe { (lparam.0 as *const COPYDATASTRUCT).as_ref() } { + Some(data) => data, + None => { + tracing::warn!("Invalid COPYDATASTRUCT pointer."); + forward_to_real_tray(hwnd, wparam, lparam); + return LRESULT(0); } + }; - fwd_result - } + // Process tray data if valid. + if copy_data.dwData == 1 && !copy_data.lpData.is_null() { + process_tray_data(hwnd, copy_data); + } - WM_TIMER => { - let _ = SetWindowPos( - hwnd, - HWND_TOPMOST, - 0, - 0, - 0, - 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, - ); - LRESULT(0) - } + forward_to_real_tray(hwnd, wparam, lparam); + + LRESULT(0) +} + +fn process_tray_data(hwnd: HWND, copy_data: ©DATASTRUCT) { + tracing::info!("Processing tray data."); + + // Get tray data + let tray_data = + unsafe { std::mem::transmute::<_, &ShellTrayData>(copy_data.lpData) }; + + tracing::info!( + "Icon data - hwnd: {:#x}, id: {}, flags: {:#x}", + tray_data.nid.hwnd_raw, + tray_data.nid.uid, + tray_data.nid.flags.0 + ); +} + +fn forward_to_real_tray(hwnd: HWND, wparam: WPARAM, lparam: LPARAM) { + if let Some(real_tray) = Util::tray_window(hwnd.0 as isize) { + tracing::debug!("Forwarding to real tray window: {:?}", real_tray); - _ => DefWindowProcW(hwnd, msg, wparam, lparam), + // TODO: Add error handling. + let _ = unsafe { + SendMessageW(HWND(real_tray as _), WM_COPYDATA, wparam, lparam) + }; + } else { + tracing::debug!("No real tray found."); } } diff --git a/crates/systray-util/src/util.rs b/crates/systray-util/src/util.rs index 8f0b275f..3d26b217 100644 --- a/crates/systray-util/src/util.rs +++ b/crates/systray-util/src/util.rs @@ -1,16 +1,17 @@ use std::{os::windows::io::AsRawHandle, thread::JoinHandle}; use windows::Win32::{ - Foundation::{HANDLE, LPARAM, WPARAM}, + Foundation::{BOOL, HANDLE, HWND, LPARAM, WPARAM}, System::Threading::GetThreadId, UI::WindowsAndMessaging::{ - CreateWindowExW, DispatchMessageW, GetMessageW, PostThreadMessageW, + CreateWindowExW, DispatchMessageW, EnumWindows, FindWindowExW, + FindWindowW, GetClassNameW, GetMessageW, PostThreadMessageW, RegisterClassW, TranslateMessage, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, MSG, WINDOW_EX_STYLE, WM_QUIT, WNDCLASSW, WNDPROC, WS_OVERLAPPEDWINDOW, }, }; -use windows_core::PCWSTR; +use windows_core::{w, PCWSTR, PWSTR}; pub type WindowProcedure = WNDPROC; @@ -102,4 +103,97 @@ impl Util { pub fn to_wide(string: &str) -> Vec { string.encode_utf16().chain(std::iter::once(0)).collect() } + + /// TODO: Could be significantly simplified. + pub fn tray_window(spy_window: isize) -> Option { + let real_tray = + unsafe { FindWindowW(w!("Shell_TrayWnd"), PWSTR::null()) }.ok()?; + + if real_tray != HWND(spy_window as _) { + if let Ok(_) = unsafe { + FindWindowExW( + real_tray, + HWND(std::ptr::null_mut()), + w!("TrayNotifyWnd"), + PWSTR::null(), + ) + } { + return Some(real_tray.0 as isize); + } + } + + let mut info = FindRealTrayInfo { + spy_window: HWND(spy_window as _), + result_window: HWND(std::ptr::null_mut()), + }; + + unsafe extern "system" fn enum_proc( + hwnd: HWND, + lparam: LPARAM, + ) -> BOOL { + let info = lparam.0 as *mut FindRealTrayInfo; + let mut class_name = [0u16; 16]; + + if GetClassNameW(hwnd, &mut class_name) != 0 { + let class_str = String::from_utf16_lossy( + &class_name + [..class_name.iter().position(|&x| x == 0).unwrap_or(0)], + ); + if class_str == "Shell_TrayWnd" && hwnd != (*info).spy_window { + if let Ok(_) = FindWindowExW( + hwnd, + HWND(std::ptr::null_mut()), + w!("TrayNotifyWnd"), + PWSTR::null(), + ) { + (*info).result_window = hwnd; + return false.into(); + } + } + } + true.into() + } + + let _ = unsafe { + EnumWindows(Some(enum_proc), LPARAM(&mut info as *mut _ as isize)) + }; + + if !info.result_window.0.is_null() { + Some(info.result_window.0 as isize) + } else { + let progman = + unsafe { FindWindowW(w!("Progman"), PWSTR::null()) }.ok()?; + let tray = unsafe { + FindWindowExW( + progman, + HWND(std::ptr::null_mut()), + w!("Shell_TrayWnd"), + PWSTR::null(), + ) + } + .ok()?; + + if tray != HWND(spy_window as _) { + if let Ok(_) = unsafe { + FindWindowExW( + tray, + HWND(std::ptr::null_mut()), + w!("TrayNotifyWnd"), + PWSTR::null(), + ) + } { + Some(tray.0 as isize) + } else { + None + } + } else { + None + } + } + } +} + +struct FindRealTrayInfo { + spy_window: HWND, + result_window: HWND, }