From f4c83e31c0411d8537d5a9baa242fa7e65af617b Mon Sep 17 00:00:00 2001 From: JobyGitGud Date: Tue, 6 May 2025 19:16:28 -0500 Subject: [PATCH] on event implementation, known bug is that cursor dies when task manager is opened may fix tomorrow I've run out of time and energy today --- src/main.rs | 13 +-- src/platform.rs | 242 ++++++++++++++++++++++++++++++------------------ 2 files changed, 158 insertions(+), 97 deletions(-) diff --git a/src/main.rs b/src/main.rs index f9fc667..a75841a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,11 +238,10 @@ fn boot() -> (CursorRush, Task) { eprintln!("Failed to hide system cursor at startup!"); } - // Apply the first cursor by default (this will update the cursor image and start the render thread) + // Apply the first cursor by default if let Some(cursor) = cursors.first() { apply_cursor(&cursor.path); - // Explicitly call render_cursor to ensure the render thread starts - platform::render_cursor(); + // No need to call render_cursor anymore as the mouse hook handles rendering } // Create shared state @@ -456,7 +455,7 @@ fn setup_tray_icon( if let Some(cursor) = state.cursors.get(state.current_index) { println!("Tray: Updating cursor image: {}", cursor.name); apply_cursor(&cursor.path); - platform::render_cursor(); // Ensure render starts/updates + // No need to call render_cursor anymore as the mouse hook handles rendering state.current_cursor_image = image_open(&cursor.path).ok().map(|img| img.to_rgba8()); } } @@ -587,8 +586,7 @@ fn update(state: &mut CursorRush, message: Message) -> Task { // Apply the new cursor if let Some(cursor) = state.cursors.get(state.current_index) { apply_cursor(&cursor.path); - // Ensure the render thread is running with the new cursor - platform::render_cursor(); + // No need to call render_cursor anymore as the mouse hook handles rendering // Load the new cursor image let current_cursor_image = match image::open(&cursor.path) { @@ -614,8 +612,7 @@ fn update(state: &mut CursorRush, message: Message) -> Task { // Apply the selected cursor if let Some(cursor) = state.cursors.get(state.current_index) { apply_cursor(&cursor.path); - // Ensure the render thread is running with the new cursor - platform::render_cursor(); + // No need to call render_cursor anymore as the mouse hook handles rendering // Load the new cursor image let current_cursor_image = match image_open(&cursor.path) { diff --git a/src/platform.rs b/src/platform.rs index 523dfd0..46e54f4 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -7,7 +7,6 @@ mod windows { use std::path::Path; // Make Mutex and AtomicBool readily available use std::sync::{Mutex, atomic::{AtomicBool, Ordering}}; - use std::thread; use std::time::Duration; // Remove Once - Lazy handles initialization // use std::sync::Once; @@ -20,11 +19,16 @@ mod windows { ShowWindow, SetWindowPos, RegisterClassExW, DefWindowProcW, WS_EX_LAYERED, WS_EX_TRANSPARENT, WS_EX_TOPMOST, WS_EX_TOOLWINDOW, WS_EX_NOACTIVATE, // Added NOACTIVATE - WS_POPUP, SW_SHOW, HWND_TOPMOST, SWP_NOSIZE, SWP_NOMOVE, - WM_DESTROY, WM_CLOSE, /* WM_MOUSEMOVE, */ // Remove WM_MOUSEMOVE handling from window proc + WS_POPUP, SW_SHOW, HWND_TOPMOST, SWP_NOSIZE, SWP_NOMOVE, SWP_NOACTIVATE, // Added SWP_NOACTIVATE + WM_DESTROY, WM_CLOSE, WM_MOUSEMOVE, WM_RBUTTONDOWN, WM_RBUTTONUP, // Added right mouse button events CS_HREDRAW, CS_VREDRAW, ULW_ALPHA, WNDCLASSEXW, // Import WNDCLASSEXW MAKEINTRESOURCEW, // Add this for converting cursor IDs to resource pointers + // Add mouse hook related imports + SetWindowsHookExW, UnhookWindowsHookEx, CallNextHookEx, + WH_MOUSE_LL, HC_ACTION, MSLLHOOKSTRUCT, }, + um::libloaderapi::GetModuleHandleW, + shared::windef::HHOOK, um::wingdi::{ CreateCompatibleDC, DeleteDC, SelectObject, DeleteObject, BLENDFUNCTION, AC_SRC_OVER, AC_SRC_ALPHA, @@ -80,19 +84,21 @@ mod windows { unsafe impl Send for SyncHDC {} unsafe impl Sync for SyncHDC {} + // Wrapper for HHOOK (hook handle) + #[derive(Copy, Clone, Debug)] + struct SyncHHOOK(HHOOK); + unsafe impl Send for SyncHHOOK {} + unsafe impl Sync for SyncHHOOK {} + // --- Thread-Safe Static Variables --- // Use AtomicBool for simple flags static CURSOR_HIDDEN: AtomicBool = AtomicBool::new(false); static CTRL_HANDLER_REGISTERED: AtomicBool = AtomicBool::new(false); - // Flag to control the rendering thread (already AtomicBool, keep it) - static RENDERING_ACTIVE: AtomicBool = AtomicBool::new(false); - // Use Lazy> for complex Option types static ORIGINAL_CURSORS: Lazy>>> = Lazy::new(|| Mutex::new(None)); static CURRENT_CURSOR_IMAGE: Lazy>> = Lazy::new(|| Mutex::new(None)); - static RENDER_THREAD: Lazy>>> = Lazy::new(|| Mutex::new(None)); static LAST_CURSOR_POS: Lazy>> = Lazy::new(|| Mutex::new(None)); // Use Lazy>> for WinAPI handles with our thread-safe wrappers @@ -100,6 +106,14 @@ mod windows { static CURSOR_BITMAP: Lazy>> = Lazy::new(|| Mutex::new(None)); static CURSOR_DC: Lazy>> = Lazy::new(|| Mutex::new(None)); + // Mouse hook related variables + static MOUSE_HOOK_HANDLE: Lazy>> = Lazy::new(|| Mutex::new(None)); + // This one is for CallNextHookEx to avoid locking inside the hook callback + static CURRENT_HOOK_FOR_CALLBACK: std::sync::atomic::AtomicPtr = + std::sync::atomic::AtomicPtr::new(std::ptr::null_mut()); + static LAST_MOUSE_MOVE_TIME: Lazy>> = Lazy::new(|| Mutex::new(None)); + const MOUSE_UPDATE_INTERVAL: Duration = Duration::from_millis(8); // Throttle to ~120Hz + // --- End Thread-Safe Static Variables --- @@ -132,11 +146,118 @@ mod windows { *CURSOR_WINDOW.lock().unwrap() = None; 0 }, - // WM_MOUSEMOVE is handled by the render thread polling GetCursorPos + // WM_MOUSEMOVE is handled by the low-level mouse hook _ => DefWindowProcW(hwnd, msg, wparam, lparam), } } + // Low-level mouse hook procedure + unsafe extern "system" fn low_level_mouse_proc(n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT { + if n_code == HC_ACTION { + // Extract mouse position from the hook struct for any mouse event + let p_msll_hook_struct = l_param as *const MSLLHOOKSTRUCT; + if !p_msll_hook_struct.is_null() { + let msll_hook_struct = *p_msll_hook_struct; + let current_pos = msll_hook_struct.pt; + + match w_param as UINT { + WM_MOUSEMOVE => { + let mut last_update_time_guard = LAST_MOUSE_MOVE_TIME.lock().unwrap(); + let now = std::time::Instant::now(); + + if last_update_time_guard.map_or(true, |last_time| now.duration_since(last_time) >= MOUSE_UPDATE_INTERVAL) { + // update_cursor_position already contains logic to only update if position actually changed via LAST_CURSOR_POS + update_cursor_position(¤t_pos); + *last_update_time_guard = Some(now); + } + }, + // Handle right mouse button down - immediately update cursor position and force topmost + WM_RBUTTONDOWN => { + // Force an immediate update of the cursor position when right-clicking + // This ensures the overlay is visible before the menu appears + update_cursor_position(¤t_pos); + + // Force the cursor window to be topmost + if let Some(hwnd_wrapper) = *CURSOR_WINDOW.lock().unwrap() { + SetWindowPos( + hwnd_wrapper.0, + HWND_TOPMOST, + 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, // SWP_NOACTIVATE is critical + ); + } + }, + // Handle other mouse events if needed + _ => { /* Other mouse messages can be handled here if needed */ } + } + } + } + + // Call the next hook in the chain. + // Pass the HHOOK of our own hook, loaded from CURRENT_HOOK_FOR_CALLBACK. + CallNextHookEx( + CURRENT_HOOK_FOR_CALLBACK.load(std::sync::atomic::Ordering::Relaxed) as HHOOK, + n_code, + w_param, + l_param, + ) + } + + // Install mouse hook function + fn install_mouse_hook_if_needed() -> bool { + let mut hook_guard = MOUSE_HOOK_HANDLE.lock().unwrap(); + if hook_guard.is_none() { + unsafe { + // GetModuleHandleW(null_mut()) gets the HMODULE for the current process (EXE) + // which is correct for a hook procedure within the same process. + let h_instance = GetModuleHandleW(null_mut()); + if h_instance.is_null() { + eprintln!("Failed to get module handle for setting mouse hook: {}", std::io::Error::last_os_error()); + return false; + } + + let hook = SetWindowsHookExW( + WH_MOUSE_LL, // Hook type: Low-level mouse + Some(low_level_mouse_proc), // Pointer to hook procedure + h_instance, // HMODULE of the DLL containing the hook proc (or current process) + 0 // Thread ID (0 for all threads on current desktop) + ); + + if !hook.is_null() { + // Create wrapped handle + let sync_hook = SyncHHOOK(hook); + *hook_guard = Some(sync_hook); + CURRENT_HOOK_FOR_CALLBACK.store(hook as *mut _, std::sync::atomic::Ordering::SeqCst); + println!("Low-level mouse hook installed successfully."); + return true; + } else { + eprintln!("Failed to install low-level mouse hook: {}", std::io::Error::last_os_error()); + return false; + } + } + } + true // Already installed + } + + // Uninstall mouse hook function + fn uninstall_mouse_hook() { + let mut hook_guard = MOUSE_HOOK_HANDLE.lock().unwrap(); + if let Some(hook_wrapper) = hook_guard.take() { // .take() removes it from Option and returns it + unsafe { + // Extract the raw handle using .0 + let hook = hook_wrapper.0; + if UnhookWindowsHookEx(hook) != 0 { + CURRENT_HOOK_FOR_CALLBACK.store(std::ptr::null_mut(), std::sync::atomic::Ordering::SeqCst); + println!("Low-level mouse hook uninstalled successfully."); + } else { + eprintln!("Failed to uninstall low-level mouse hook: {}", std::io::Error::last_os_error()); + // Put it back if unhooking failed, though this is unlikely. + *hook_guard = Some(hook_wrapper); + } + } + } + } + // Function to update the cursor position (Render thread responsibility) fn update_cursor_position(point: &POINT) { // Lock the statics needed @@ -189,10 +310,9 @@ mod windows { AlphaFormat: AC_SRC_ALPHA, // Use per-pixel alpha }; - // Ensure the window is visible and topmost *before* updating - // This helps prevent flicker and ensures it stays on top - ShowWindow(hwnd, SW_SHOW); - SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + // The window is already set to visible and topmost when created in create_cursor_window() + // But we need to re-assert TOPMOST after UpdateLayeredWindow to ensure it stays above system tray menus + // The key is to use SWP_NOACTIVATE to prevent stealing focus from the menu // Update the layered window atomically let result = UpdateLayeredWindow( @@ -207,6 +327,16 @@ mod windows { ULW_ALPHA, ); + // Re-assert TOPMOST after successful update, with SWP_NOACTIVATE to prevent stealing focus + if result != 0 { + SetWindowPos( + hwnd, + HWND_TOPMOST, + 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, // Added SWP_NOACTIVATE to prevent stealing focus + ); + } + if result == 0 { use std::io::Error; let error = Error::last_os_error(); @@ -297,20 +427,8 @@ mod windows { // Cleanup function to restore cursors and clean up resources fn cleanup() { - // Stop the rendering thread FIRST - if RENDERING_ACTIVE.swap(false, Ordering::SeqCst) { // Use swap to ensure it's only attempted once - println!("Stopping render thread..."); - // Join the thread handle - if let Some(handle) = RENDER_THREAD.lock().unwrap().take() { - match handle.join() { - Ok(_) => println!("Render thread joined successfully."), - Err(e) => eprintln!("Error joining render thread: {:?}", e), - } - } - } else { - // Render thread was already stopped or never started - // println!("Render thread already stopped."); - } + // Uninstall the mouse hook FIRST + uninstall_mouse_hook(); // Restore system cursors if they were hidden @@ -777,6 +895,12 @@ mod windows { drop(window_guard); + // Install the mouse hook + if !install_mouse_hook_if_needed() { + eprintln!("Mouse hook installation failed, custom cursor might not follow mouse."); + // We'll continue anyway as this is not a fatal error + } + // Initialize cursor position immediately after update unsafe { let mut point = POINT { x: 0, y: 0 }; @@ -785,9 +909,6 @@ mod windows { } } - // Start the rendering thread if it's not already running - start_render_thread_if_needed(); - true }, Err(e) => { @@ -815,66 +936,9 @@ mod windows { // No need to duplicate cleanup logic here } - // Function to start the rendering thread if it's not already running - fn start_render_thread_if_needed() { - // Check if active *before* locking the thread handle mutex for performance - if !RENDERING_ACTIVE.load(Ordering::SeqCst) { - let mut render_thread_guard = RENDER_THREAD.lock().unwrap(); - // Double check inside the lock - if render_thread_guard.is_none() { - // Set the flag *before* spawning the thread - RENDERING_ACTIVE.store(true, Ordering::SeqCst); - - // Spawn the thread - let handle = thread::spawn(|| { - println!("Starting cursor render thread..."); - // Render loop - while RENDERING_ACTIVE.load(Ordering::Relaxed) { // Relaxed is fine for loop check - render_cursor_internal(); // Call internal render function - // Use a variable sleep time, e.g., target 120Hz (approx 8ms) - thread::sleep(Duration::from_millis(8)); - } - println!("Cursor render thread stopped."); - }); - - // Store the thread handle - *render_thread_guard = Some(handle); - println!("Render thread started and handle stored."); - } - // else: Another thread started it between the outer check and acquiring the lock - } - // else: Already running - } - - // Internal function to render the cursor (called from the render thread) - fn render_cursor_internal() { - unsafe { - let mut point = POINT { x: 0, y: 0 }; - if GetCursorPos(&mut point) == 0 { - // Failed to get cursor position, maybe log error? - // eprintln!("GetCursorPos failed in render loop"); - return; - } - - // --- Check if window needs to be topmost --- - // It's generally better to ensure topmost status less frequently, - // perhaps only when the window is created or maybe periodically, - // as calling SetWindowPos constantly can have overhead. - // However, keeping it here ensures responsiveness if other apps fight for topmost. - // Let's keep it for now. - if let Some(hwnd_wrapper) = *CURSOR_WINDOW.lock().unwrap() { // Get handle copy - SetWindowPos(hwnd_wrapper.0, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - } - // --- End Topmost Check --- - - // Update the cursor graphic position - update_cursor_position(&point); - } - } - - // Public function to ensure the custom cursor is rendering (main purpose now is to start thread) + // Public function that used to start the render thread, now a no-op since we use the mouse hook pub fn render_cursor() { - start_render_thread_if_needed(); + // No-op: Cursor rendering is now handled by the mouse hook } } // end mod windows @@ -920,7 +984,7 @@ mod linux { // (Keep these as they are) #[cfg(windows)] -pub use windows::{restore_cursor, render_cursor, hide_system_cursor, update_cursor_image}; +pub use windows::{restore_cursor, hide_system_cursor, update_cursor_image, render_cursor}; #[cfg(target_os = "macos")] pub use macos::{restore_cursor, render_cursor, hide_system_cursor, update_cursor_image};