on even implimentation, known bug is that is that cursor dies when task manager is opened may fix tomorrow I've run out of time and energy today

This commit is contained in:
2025-05-06 19:16:28 -05:00
parent 5583f6a517
commit d7c69f705f
2 changed files with 158 additions and 97 deletions
+5 -8
View File
@@ -238,11 +238,10 @@ fn boot() -> (CursorRush, Task<Message>) {
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<Message> {
// 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<Message> {
// 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) {
+153 -89
View File
@@ -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<Mutex<T>> for complex Option types
static ORIGINAL_CURSORS: Lazy<Mutex<Option<Vec<(DWORD, SyncHCURSOR)>>>> = Lazy::new(|| Mutex::new(None));
static CURRENT_CURSOR_IMAGE: Lazy<Mutex<Option<RgbaImage>>> = Lazy::new(|| Mutex::new(None));
static RENDER_THREAD: Lazy<Mutex<Option<thread::JoinHandle<()>>>> = Lazy::new(|| Mutex::new(None));
static LAST_CURSOR_POS: Lazy<Mutex<Option<POINT>>> = Lazy::new(|| Mutex::new(None));
// Use Lazy<Mutex<Option<Handle>>> for WinAPI handles with our thread-safe wrappers
@@ -100,6 +106,14 @@ mod windows {
static CURSOR_BITMAP: Lazy<Mutex<Option<SyncHBITMAP>>> = Lazy::new(|| Mutex::new(None));
static CURSOR_DC: Lazy<Mutex<Option<SyncHDC>>> = Lazy::new(|| Mutex::new(None));
// Mouse hook related variables
static MOUSE_HOOK_HANDLE: Lazy<Mutex<Option<SyncHHOOK>>> = 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::os::raw::c_void> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
static LAST_MOUSE_MOVE_TIME: Lazy<Mutex<Option<std::time::Instant>>> = 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(&current_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(&current_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};