diff --git a/Cargo.toml b/Cargo.toml index cfba46d..a471037 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,21 @@ version = "0.1.0" edition = "2021" [dependencies] +iced = { version = "0.13.1", default-features = true, features = ["image", "advanced"] } # GUI, bundles wgpu + winit +tray-icon = "0.20.1" # cross‑platform system tray +image = "0.24.7" # Image processing +walkdir = "2.4.0" # Directory traversal +global-hotkey = "0.4.2" # Global hotkey registration +lazy_static = "1.4.0" # Thread-safe static initialization + +# Platform-specific dependencies +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3.9", features = ["winuser", "wingdi", "consoleapi"] } # Windows API +ctor = "0.2.4" # Constructor/destructor attributes + +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2.7" # Objective-C bindings +cocoa = "0.24.1" # macOS Cocoa bindings + +[target.'cfg(target_os = "linux")'.dependencies] +x11 = { version = "2.21.0", features = ["xlib"] } # X11 bindings diff --git a/assets/default.png b/assets/default.png new file mode 100644 index 0000000..8643897 Binary files /dev/null and b/assets/default.png differ diff --git a/assets/red.png b/assets/red.png new file mode 100644 index 0000000..fd5c6ad Binary files /dev/null and b/assets/red.png differ diff --git a/src/main.rs b/src/main.rs index c66c0ee..58203bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,525 @@ -fn main() { - println!("Hello, Cursor Rush!"); +use iced::{ + widget::{button, text, Column, Row, container, image as iced_image}, + Element, Length, Subscription, Task, Theme, +}; +use std::{ + path::{Path, PathBuf}, + sync::{mpsc, Arc, Mutex}, + thread, + time::Duration, +}; +use ::image::{GenericImageView, RgbaImage, open as image_open}; +use walkdir::WalkDir; +use tray_icon::{ + TrayIconBuilder, + menu::{Menu, MenuEvent}, +}; +use global_hotkey::{ + GlobalHotKeyManager, + hotkey::HotKey as GlobalHotKey, + HotKeyState, +}; + +// Import our platform-specific module +mod platform; + +// Define a struct to hold cursor information +#[derive(Clone)] +struct CursorInfo { + name: String, + path: PathBuf, +} + +// We'll use a simpler approach for the cursor overlay + +// Define menu item IDs +const NEXT_CURSOR_ID: &str = "next_cursor"; +const QUIT_ID: &str = "quit"; + +// Shared state between the UI and the tray icon +struct SharedState { + current_index: usize, + cursors: Vec, + current_cursor_image: Option, + running: bool, +} + +// Our application state +#[derive(Default)] +struct CursorRush { + cursors: Vec, + current_index: usize, + shared_state: Option>>, + cursor_position: (f32, f32), // Track the mouse position +} + +// Messages that can be sent in our application +#[derive(Debug, Clone)] +enum Message { + NextCursor, + SelectCursor(usize), + Quit, + TrayEvent, + CursorMoved((f32, f32)), // New message for cursor movement +} + +// Function to load all PNG files from the assets directory +fn load_cursor_files() -> Vec { + let mut cursors = Vec::new(); + + println!("Looking for cursor PNG files in assets directory..."); + + // Check if assets directory exists + if !std::path::Path::new("assets").exists() { + println!("Assets directory not found!"); + return cursors; + } + + for entry in WalkDir::new("assets").into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + println!("Found file: {}", path.display()); + if path.is_file() && path.extension().map_or(false, |ext| ext == "png") { + let name = path.file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("Unknown") + .to_string(); + + println!("Adding cursor: {} at path {}", name, path.display()); + cursors.push(CursorInfo { + name, + path: path.to_path_buf(), + }); + } + } + + println!("Found {} cursor PNG files", cursors.len()); + + // Sort cursors by name for consistent ordering + cursors.sort_by(|a, b| a.name.cmp(&b.name)); + cursors +} + +// Function to validate a cursor image +fn validate_cursor_image(path: &Path) -> bool { + // Load the image to validate it + match image_open(path) { + Ok(img) => { + let (width, height) = img.dimensions(); + + // Ensure reasonable dimensions + if width > 128 || height > 128 || width == 0 || height == 0 { + eprintln!("Image dimensions not suitable for cursor: {}x{}", width, height); + return false; + } + + true + }, + Err(e) => { + eprintln!("Failed to open image {}: {}", path.display(), e); + false + }, + } +} + +// Apply the cursor at the given path +fn apply_cursor(path: &Path) -> bool { + // Validate the cursor image + if validate_cursor_image(path) { + // Use platform-specific cursor setting + if platform::set_cursor(path) { + println!("Successfully set cursor"); + return true; + } else { + eprintln!("Failed to set cursor"); + } + } + false +} + +// Top-level subscription function to listen for events +fn subscription(_state: &CursorRush) -> Subscription { + iced::event::listen().map(|event| { + if let iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }) = event { + // Track cursor position for UI display purposes + Message::CursorMoved((position.x, position.y)) + } else { + // We need to return some message, so we'll use TrayEvent as a no-op + Message::TrayEvent + } + }) +} + +// Boot function to initialize the application +fn boot() -> (CursorRush, Task) { + // Load cursor files from assets directory + let cursors = load_cursor_files(); + + if cursors.is_empty() { + eprintln!("No cursor PNG files found in assets directory!"); + return (CursorRush::default(), Task::none()); + } + + // Load the first cursor image + let current_cursor_image = if let Some(cursor) = cursors.first() { + match image_open(&cursor.path) { + Ok(img) => Some(img.to_rgba8()), + Err(_) => None, + } + } else { + None + }; + + // Apply the first cursor by default (this will hide the system cursor and start the render thread) + if let Some(cursor) = cursors.first() { + apply_cursor(&cursor.path); + // Explicitly call render_cursor to ensure the render thread starts + platform::render_cursor(); + } + + // Create shared state + let shared_state = Arc::new(Mutex::new(SharedState { + current_index: 0, + cursors: cursors.clone(), + current_cursor_image, + running: true, + })); + + // Create a channel to communicate with the UI + let (tx, rx) = mpsc::channel(); + + // Set up the tray icon + setup_tray_icon(&cursors, shared_state.clone(), tx.clone()); + + // Initialize the application state with loaded cursors + let mut state = CursorRush::default(); + state.cursors = cursors; + state.shared_state = Some(shared_state.clone()); + + // Set up global hotkey manager + let hotkey_manager = GlobalHotKeyManager::new().expect("Failed to create hotkey manager"); + + // Register Ctrl+Shift+F12 as the exit hotkey (uncommon but easy to remember) + use global_hotkey::hotkey::{Code, Modifiers}; + let hotkey = GlobalHotKey::new( + Some(Modifiers::CONTROL | Modifiers::SHIFT), + Code::F12, + ); + + if let Err(e) = hotkey_manager.register(hotkey) { + eprintln!("Failed to register hotkey: {}", e); + } else { + println!("Press Ctrl+Shift+F12 to exit the application"); + } + + // We're using the event-driven approach with the subscription function for mouse tracking + + // Spawn a thread to listen for tray events + let tx_clone = tx.clone(); + thread::spawn(move || { + loop { + if let Ok(_) = rx.recv() { + // Send a message to the UI + let _ = tx_clone.send(Message::TrayEvent); + } + } + }); + + // Spawn a thread to listen for hotkeys + let state_clone = shared_state.clone(); + thread::spawn(move || { + let event_receiver = global_hotkey::GlobalHotKeyEvent::receiver(); + loop { + if let Ok(event) = event_receiver.try_recv() { + if event.state == HotKeyState::Pressed { + println!("Exit hotkey pressed, shutting down..."); + + // Set running to false + if let Ok(mut state) = state_clone.lock() { + state.running = false; + } + + // Restore the system cursor + platform::restore_cursor(); + + // Exit the application + std::process::exit(0); + } + } + + // Small sleep to prevent high CPU usage + std::thread::sleep(Duration::from_millis(10)); + } + }); + + // Using the event-driven approach with the subscription function for cursor tracking + + (state, Task::none()) +} + +fn main() -> iced::Result { + iced::application("Cursor Rush", update, view) + .theme(|_| Theme::Dark) + .subscription(subscription) + .centered() + .run_with(boot) +} + +// Set up the system tray icon +fn setup_tray_icon( + cursors: &[CursorInfo], + shared_state: Arc>, + tx: mpsc::Sender, +) { + if cursors.is_empty() { + return; + } + + // Create a menu + let mut menu = Menu::new(); + + // Add menu items with IDs + let next_item = tray_icon::menu::MenuItem::with_id( + NEXT_CURSOR_ID, + "Next cursor", + true, + None, // No accelerator + ); + + let quit_item = tray_icon::menu::MenuItem::with_id( + QUIT_ID, + "Quit", + true, + None, // No accelerator + ); + + // Add items to menu + if let Ok(menu_with_items) = Menu::with_items(&[&next_item, &quit_item]) { + menu = menu_with_items; + } + + // Use the first PNG as the tray icon + let icon_path = &cursors[0].path; + + // Load the image + let img = match image_open(icon_path) { + Ok(img) => img, + Err(e) => { + eprintln!("Failed to open image {}: {}", icon_path.display(), e); + return; + } + }; + + // Convert to RGBA + let rgba_img = img.to_rgba8(); + let (width, height) = rgba_img.dimensions(); + + // Create icon from RGBA data + let icon = match tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height) { + Ok(icon) => icon, + Err(e) => { + eprintln!("Failed to create icon: {}", e); + return; + } + }; + + // Build the tray icon + let _tray = TrayIconBuilder::new() + .with_tooltip("Cursor Rush") + .with_icon(icon) + .with_menu(Box::new(menu)) + .build() + .unwrap(); + + // Listen for tray menu events in a separate thread + let state_clone = shared_state.clone(); + thread::spawn(move || { + let menu_channel = MenuEvent::receiver(); + while let Ok(event) = menu_channel.recv() { + match event.id.as_ref() { + NEXT_CURSOR_ID => { + // Handle next cursor event + let mut state = state_clone.lock().unwrap(); + if !state.cursors.is_empty() { + state.current_index = (state.current_index + 1) % state.cursors.len(); + 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(); + + // Load the new cursor image + let current_cursor_image = match image_open(&cursor.path) { + Ok(img) => Some(img.to_rgba8()), + Err(_) => None, + }; + + // Update the cursor image in shared state + state.current_cursor_image = current_cursor_image; + } + } + // Notify the UI + let _ = tx.send(Message::TrayEvent); + }, + QUIT_ID => { + // Restore the system cursor before exiting + platform::restore_cursor(); + + // Exit the application + std::process::exit(0); + }, + _ => {} // any other menu items you add later + } + } + }); +} + +// Update function to handle messages +fn update(state: &mut CursorRush, message: Message) -> Task { + match message { + Message::NextCursor => { + if !state.cursors.is_empty() { + state.current_index = (state.current_index + 1) % state.cursors.len(); + + // 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(); + + // Load the new cursor image + let current_cursor_image = match image::open(&cursor.path) { + Ok(img) => Some(img.to_rgba8()), + Err(_) => None, + }; + + // Update shared state + if let Some(shared) = &state.shared_state { + if let Ok(mut shared_state) = shared.lock() { + shared_state.current_index = state.current_index; + shared_state.current_cursor_image = current_cursor_image; + } + } + } + } + Task::none() + }, + Message::SelectCursor(idx) => { + if idx < state.cursors.len() { + state.current_index = idx; + + // 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(); + + // Load the new cursor image + let current_cursor_image = match image_open(&cursor.path) { + Ok(img) => Some(img.to_rgba8()), + Err(_) => None, + }; + + // Update shared state + if let Some(shared) = &state.shared_state { + if let Ok(mut shared_state) = shared.lock() { + shared_state.current_index = state.current_index; + shared_state.current_cursor_image = current_cursor_image; + } + } + } + } + Task::none() + }, + Message::Quit => { + // Restore the system cursor before exiting + platform::restore_cursor(); + + // Exit the application + std::process::exit(0); + }, + Message::TrayEvent => { + // Sync with shared state + if let Some(shared) = &state.shared_state { + if let Ok(shared_state) = shared.lock() { + state.current_index = shared_state.current_index; + } + } + Task::none() + }, + Message::CursorMoved(position) => { + // Update the cursor position + state.cursor_position = position; + Task::none() + }, + } +} + +// View function to render the UI +fn view(state: &CursorRush) -> Element { + let mut content = Column::new() + .spacing(16) + .push(text("Cursor Rush").size(24)); + + // Add a button for each cursor + if !state.cursors.is_empty() { + let current_cursor = &state.cursors[state.current_index]; + content = content.push(text(format!("Current cursor: {}", current_cursor.name))); + + // Display the current cursor image as a preview + let img_handle = iced_image::Handle::from_path(¤t_cursor.path); + let cursor_image = iced_image::Image::new(img_handle) + .width(Length::Fixed(64.0)) + .height(Length::Fixed(64.0)); + + content = content.push(cursor_image); + + // Create a row of cursor buttons + let mut cursor_row = Row::new().spacing(10); + + for (i, cursor) in state.cursors.iter().enumerate() { + // Create a small preview image for each cursor + let preview_handle = iced_image::Handle::from_path(&cursor.path); + let preview_image = iced_image::Image::new(preview_handle) + .width(Length::Fixed(32.0)) + .height(Length::Fixed(32.0)); + + let btn = button( + Row::new() + .spacing(5) + .push(preview_image) + .push(text(&cursor.name).size(16)) + ) + .on_press(Message::SelectCursor(i)); + + cursor_row = cursor_row.push(btn); + } + + content = content + .push(cursor_row) + .push(button("Next cursor").on_press(Message::NextCursor)) + .push(button("Quit").on_press(Message::Quit)); + + // Add instructions for the hotkey + content = content.push( + text("Press Ctrl+Shift+F12 to exit the application").size(14) + ); + } else { + content = content.push(text("No cursors found in assets directory")); + } + + // Add cursor position information for debugging + if !state.cursors.is_empty() { + let (x, y) = state.cursor_position; + content = content.push( + text(format!("Cursor position: ({:.1}, {:.1})", x, y)).size(12) + ); + } + + // Create the main container + container(content) + .width(Length::Fill) + .height(Length::Fill) + .center_x(Length::Fill) + .center_y(Length::Fill) + .padding(20) + .into() } diff --git a/src/platform.rs b/src/platform.rs new file mode 100644 index 0000000..80d1c82 --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,759 @@ +// Platform-specific cursor handling + +// Windows implementation +#[cfg(windows)] +mod windows { + use std::path::Path; + use std::sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}}; + use std::thread; + use std::time::Duration; + use std::sync::Once; + use std::fs::File; + use std::io::Write; + use std::process::Command; + use winapi::{ + um::winuser::{ + GetDC, ReleaseDC, GetCursorPos, LoadCursorW, + SetSystemCursor, CopyIcon, IDC_ARROW, + CreateCursor, SystemParametersInfoW, SPI_SETCURSORS, + CreateWindowExW, DestroyWindow, UpdateLayeredWindow, + SetLayeredWindowAttributes, ShowWindow, SetWindowPos, + RegisterClassExW, DefWindowProcW, + WS_EX_LAYERED, WS_EX_TRANSPARENT, WS_EX_TOPMOST, WS_EX_TOOLWINDOW, + WS_POPUP, SW_SHOW, HWND_TOPMOST, SWP_NOSIZE, SWP_NOMOVE, + LWA_ALPHA, LWA_COLORKEY, WM_DESTROY, WM_CLOSE, + CS_HREDRAW, CS_VREDRAW, + }, + um::wingdi::{ + SetPixel, CreateCompatibleDC, DeleteDC, SelectObject, DeleteObject, + CreateCompatibleBitmap, RGB, BLENDFUNCTION, AC_SRC_OVER, AC_SRC_ALPHA, + }, + um::consoleapi::SetConsoleCtrlHandler, + shared::windef::{HWND, POINT, HCURSOR, SIZE}, + shared::minwindef::{DWORD, BOOL, TRUE, FALSE, UINT, WPARAM, LPARAM, LRESULT}, + }; + + // Import the Windows API function to reset system cursors + #[link(name = "user32")] + extern "system" { + fn SystemParametersInfoA(uiAction: u32, uiParam: u32, pvParam: *mut std::ffi::c_void, fWinIni: u32) -> BOOL; + } + + // Constants for SystemParametersInfo + const SPI_SETCURSORS_A: u32 = 0x0057; + const SPIF_UPDATEINIFILE: u32 = 0x01; + const SPIF_SENDCHANGE: u32 = 0x02; + use std::ptr::null_mut; + use ::image::{RgbaImage, open as image_open}; + use lazy_static::lazy_static; + + // Static variables for cursor state + static mut CURSOR_HIDDEN: bool = false; + + // Flag to control the rendering thread + static RENDERING_ACTIVE: AtomicBool = AtomicBool::new(false); + + // Define all system cursor IDs we'll replace + static SYSTEM_CURSORS: [DWORD; 11] = [ + 32512, // IDC_ARROW + 32513, // IDC_IBEAM + 32515, // IDC_CROSS + 32514, // IDC_WAIT + 32649, // IDC_HAND + 32650, // IDC_APPSTARTING + 32648, // IDC_NO + 32646, // IDC_SIZEALL + 32645, // IDC_SIZENS + 32644, // IDC_SIZEWE + 32642, // IDC_SIZENWSE + ]; + + // Use lazy_static for thread-safe access to the cursor image + lazy_static! { + static ref CURRENT_CURSOR_IMAGE: Arc>> = Arc::new(Mutex::new(None)); + static ref RENDER_THREAD: Mutex>> = Mutex::new(None); + } + + // We need to use a static variable for the window handle since HWND isn't Send + static mut CURSOR_WINDOW: Option = None; + + // Window class name for our cursor window + const CURSOR_WINDOW_CLASS: &str = "CursorRushOverlay"; + + // Window procedure for our cursor window + unsafe extern "system" fn window_proc(hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM) -> LRESULT { + match msg { + WM_CLOSE | WM_DESTROY => { + DestroyWindow(hwnd); + 0 + }, + _ => DefWindowProcW(hwnd, msg, wparam, lparam), + } + } + + // Create a layered, topmost window for our cursor + unsafe fn create_cursor_window() -> Option { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + + // Convert class name to wide string + let class_name: Vec = OsStr::new(CURSOR_WINDOW_CLASS) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + // Register window class + let mut wc = std::mem::zeroed::(); + wc.cbSize = std::mem::size_of::() as u32; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = Some(window_proc); + wc.hInstance = std::ptr::null_mut(); + wc.lpszClassName = class_name.as_ptr(); + wc.hCursor = LoadCursorW(std::ptr::null_mut(), IDC_ARROW as _); + + let atom = RegisterClassExW(&wc); + if atom == 0 { + eprintln!("Failed to register window class"); + return None; + } + + // Create window with layered, transparent, and topmost attributes + // WS_EX_TRANSPARENT: Makes the window transparent to mouse clicks + // WS_EX_LAYERED: Required for transparency and UpdateLayeredWindow + // WS_EX_TOPMOST: Keeps the window above all other windows + // WS_EX_TOOLWINDOW: Prevents the window from showing in the taskbar + // WS_EX_NOACTIVATE: Prevents the window from becoming active when clicked + let hwnd = CreateWindowExW( + WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_TOOLWINDOW | 0x08000000, // WS_EX_NOACTIVATE + class_name.as_ptr(), + std::ptr::null(), // Window title (none) + WS_POPUP, // Popup window with no border + 0, 0, // Position (will be updated based on cursor position) + 128, 128, // Size (will be updated based on cursor image) + std::ptr::null_mut(), // Parent window + std::ptr::null_mut(), // Menu + std::ptr::null_mut(), // Instance + std::ptr::null_mut(), // Additional data + ); + + if hwnd.is_null() { + eprintln!("Failed to create cursor window"); + return None; + } + + // Make the window transparent with click-through capability + SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA | LWA_COLORKEY); + + // Show the window + ShowWindow(hwnd, SW_SHOW); + + // Ensure it stays on top + SetWindowPos( + hwnd, + HWND_TOPMOST, + 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE + ); + + Some(hwnd) + } + + // We'll use a static variable for original cursors since they're not thread-safe + static mut ORIGINAL_CURSORS: Option> = None; + + // Flag to track if we've registered the control handler + static mut CTRL_HANDLER_REGISTERED: bool = false; + + // Use Once to ensure initialization happens only once + static INIT: Once = Once::new(); + + // Control handler function for console events (Ctrl+C, close, etc.) + extern "system" fn ctrl_handler(_ctrl_type: DWORD) -> BOOL { + println!("Control event received, restoring cursors..."); + // Call our cleanup function to restore cursors + cleanup(); + // Return FALSE to allow other handlers to process the event + FALSE + } + + // Cleanup function to restore cursors and clean up resources + fn cleanup() { + unsafe { + // Only restore if we've hidden the cursor + if CURSOR_HIDDEN { + println!("Restoring system cursors..."); + + // First, force Windows to reload the cursors + SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0); + + // Try multiple restoration methods and track success + let mut restoration_success = false; + + // Method 1: Try to restore the original system cursors if we have them + if let Some(ref original_cursors) = ORIGINAL_CURSORS { + if !original_cursors.is_empty() { + let mut method1_success = true; + println!("Attempting to restore {} saved original cursors", original_cursors.len()); + + for &(cursor_id, cursor) in original_cursors.iter() { + // Make a copy of the original cursor + let cursor_copy = CopyIcon(cursor as _) as HCURSOR; + if !cursor_copy.is_null() { + // Restore the system cursor + if SetSystemCursor(cursor_copy, cursor_id) == 0 { + eprintln!("Failed to restore system cursor {}", cursor_id); + method1_success = false; + } + } else { + method1_success = false; + } + } + + if method1_success { + println!("Successfully restored original cursors"); + restoration_success = true; + } + } + } + + // Method 2: If method 1 failed, try with the default arrow cursor + if !restoration_success { + println!("Using arrow cursor restoration method"); + let mut method2_success = true; + + // Load the default arrow cursor + let arrow_cursor = LoadCursorW(null_mut(), 32512 as _); // IDC_ARROW = 32512 + if !arrow_cursor.is_null() { + // Make a copy for each system cursor + for &cursor_id in SYSTEM_CURSORS.iter() { + let cursor_copy = CopyIcon(arrow_cursor as _) as HCURSOR; + if !cursor_copy.is_null() { + if SetSystemCursor(cursor_copy, cursor_id) == 0 { + eprintln!("Failed to set system cursor {} to arrow", cursor_id); + method2_success = false; + } + } else { + method2_success = false; + } + } + } else { + method2_success = false; + } + + if method2_success { + println!("Successfully restored cursors using arrow cursor"); + restoration_success = true; + } + } + + // Method 3: Reset all system cursors to their defaults using Windows API + if !restoration_success { + println!("Using system reset method"); + // Call our reset function which uses both W and A versions of SystemParametersInfo + reset_system_cursors(); + + // We'll assume this worked unless we have evidence it didn't + restoration_success = true; + + // Try to verify by checking if we can load a cursor + let test_cursor = LoadCursorW(null_mut(), 32512 as _); // IDC_ARROW + if test_cursor.is_null() { + restoration_success = false; + } + } + + // Method 4: Only create and run a batch file as a last resort if all other methods failed + if !restoration_success { + println!("All direct restoration methods failed, using batch file fallback"); + create_cursor_reset_file(); + } + + CURSOR_HIDDEN = false; + println!("System cursors restoration complete"); + } + } + + // Stop the rendering thread + RENDERING_ACTIVE.store(false, Ordering::SeqCst); + + // Destroy the cursor window if it exists + unsafe { + if let Some(hwnd) = CURSOR_WINDOW.take() { + DestroyWindow(hwnd); + println!("Cursor window destroyed during cleanup"); + } + } + } + + // Register the cleanup function with control handler + fn register_cleanup() { + // Use Once to ensure this only happens once + INIT.call_once(|| { + unsafe { + // Register our control handler + if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) == 0 { + eprintln!("Failed to register control handler"); + } else { + println!("Control handler registered for cursor restoration"); + CTRL_HANDLER_REGISTERED = true; + } + } + }); + } + + // Initialize module + fn initialize() { + // Register cleanup handler on module load + register_cleanup(); + + // Reset system cursors on startup in case they were left in a bad state + reset_system_cursors(); + } + + // Call initialize when module is loaded + #[ctor::ctor] + fn module_init() { + initialize(); + println!("Windows cursor module initialized with cleanup handlers"); + } + + // Call cleanup when module is unloaded + #[ctor::dtor] + fn module_cleanup() { + println!("Module unloading, restoring cursors..."); + // Just call cleanup which now handles all restoration methods + cleanup(); + // No need to create a reset file here as cleanup() will do it if needed + } + + // Function to reset all system cursors to their defaults + fn reset_system_cursors() { + unsafe { + // Try multiple approaches to reset cursors + + // First try with the W version + let result1 = SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0); + + // Then try with the A version with flags to update the system settings + let result2 = SystemParametersInfoA( + SPI_SETCURSORS_A, + 0, + null_mut(), + SPIF_UPDATEINIFILE | SPIF_SENDCHANGE + ); + + // Try direct call to rundll32 as a more forceful approach + if result1 == 0 && result2 == 0 { + // Both API calls failed, try using Command + match Command::new("rundll32.exe") + .args(&["user32.dll,SystemParametersInfo", "0x0057", "0", "0", "0x01"]) + .status() + { + Ok(status) => { + if status.success() { + println!("Reset system cursors using rundll32"); + } else { + eprintln!("rundll32 cursor reset failed with status: {}", status); + } + }, + Err(e) => eprintln!("Failed to execute rundll32: {}", e), + } + } else { + println!("Reset system cursors to defaults via API"); + } + } + } + + // Create a batch file that will reset the cursors and run it + fn create_cursor_reset_file() { + // Create a temporary batch file to reset cursors + let batch_content = r#"@echo off +echo Resetting system cursors... +rundll32.exe user32.dll,SystemParametersInfo 0x0057 0 0 0x01 | 0x02 +echo Cursors reset. +timeout /t 2 +del "%~f0" +"#; + + let file_path = "reset_cursors.bat"; + + // Write the batch file + match File::create(file_path) { + Ok(mut file) => { + if let Err(e) = file.write_all(batch_content.as_bytes()) { + eprintln!("Failed to write to batch file: {}", e); + return; + } + + println!("Created cursor reset batch file"); + + // Run the batch file in a separate process + match Command::new("cmd") + .args(&["/C", "start", "/min", file_path]) + .spawn() + { + Ok(_) => println!("Launched cursor reset batch file"), + Err(e) => eprintln!("Failed to launch batch file: {}", e), + } + }, + Err(e) => eprintln!("Failed to create batch file: {}", e), + } + } + + // Function to save the original system cursors + fn save_original_cursors() { + unsafe { + // Only save if we haven't already + if ORIGINAL_CURSORS.is_none() { + let mut cursors = Vec::new(); + + for &cursor_id in SYSTEM_CURSORS.iter() { + let cursor = LoadCursorW(null_mut(), cursor_id as _); + if !cursor.is_null() { + // Make a copy of the cursor + let cursor_copy = CopyIcon(cursor as _) as HCURSOR; + if !cursor_copy.is_null() { + cursors.push((cursor_id, cursor_copy)); + } + } + } + + ORIGINAL_CURSORS = Some(cursors); + println!("Saved {} original system cursors", ORIGINAL_CURSORS.as_ref().unwrap().len()); + } + } + } + + pub fn set_cursor(path: &Path) -> bool { + // Save the original system cursors before hiding them + save_original_cursors(); + + // Register cleanup handler to ensure cursor is restored on exit + register_cleanup(); + + // Hide the system cursor + unsafe { + if !CURSOR_HIDDEN { + // Create an invisible cursor (1x1 transparent cursor) + // Create bitmap data for a 1x1 cursor + let and_plane: [u8; 1] = [0xFF]; // All bits 1 = transparent + let xor_plane: [u8; 1] = [0x00]; // All bits 0 = black + + let empty_cursor = CreateCursor( + null_mut(), // hInst + 0, // xHotSpot + 0, // yHotSpot + 1, // nWidth + 1, // nHeight + and_plane.as_ptr() as *const _, // pvANDPlane + xor_plane.as_ptr() as *const _, // pvXORPlane + ); + + if empty_cursor.is_null() { + eprintln!("Failed to create empty cursor"); + return false; + } + + // Replace all system cursors with our empty cursor + for &cursor_id in SYSTEM_CURSORS.iter() { + // Make a copy of our empty cursor for each system cursor + let cursor_copy = CopyIcon(empty_cursor as _) as HCURSOR; + if !cursor_copy.is_null() { + // Set the system cursor + if SetSystemCursor(cursor_copy, cursor_id) == 0 { + eprintln!("Failed to set system cursor {}", cursor_id); + } + } + } + + CURSOR_HIDDEN = true; + println!("System cursors hidden"); + } + } + + // Load the cursor image + match image_open(path) { + Ok(img) => { + let rgba_img = img.to_rgba8(); + *CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(rgba_img); + println!("Using PNG cursor: {:?}", path); + + // Start the rendering thread if it's not already running + start_render_thread_if_needed(); + + true + }, + Err(e) => { + eprintln!("Failed to load cursor image: {}", e); + false + } + } + } + + // Function to restore the cursor when the program exits + pub fn restore_cursor() { + println!("Explicit restore_cursor called"); + + // Call our cleanup function which handles cursor restoration + cleanup(); + + // Join the thread if it exists + if let Some(handle) = RENDER_THREAD.lock().unwrap().take() { + // Use a timeout to avoid hanging if the thread is stuck + let thread_handle = handle; + match thread_handle.join() { + Ok(_) => println!("Render thread joined successfully"), + Err(_) => eprintln!("Error joining render thread"), + } + } + + // Destroy the cursor window if it exists + unsafe { + if let Some(hwnd) = CURSOR_WINDOW.take() { + if DestroyWindow(hwnd) == 0 { + eprintln!("Failed to destroy cursor window"); + } else { + println!("Cursor window destroyed"); + } + } + } + + // Clear the cursor image + *CURRENT_CURSOR_IMAGE.lock().unwrap() = None; + + // Unregister our control handler to avoid double cleanup + unsafe { + if CTRL_HANDLER_REGISTERED { + if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) == 0 { + eprintln!("Failed to unregister control handler"); + } else { + CTRL_HANDLER_REGISTERED = false; + println!("Control handler unregistered"); + } + } + } + + // Final verification that cursors are restored + unsafe { + if CURSOR_HIDDEN { + println!("Cursor still marked as hidden after cleanup, forcing reset"); + reset_system_cursors(); + CURSOR_HIDDEN = false; + } + } + } + + // Function to start the rendering thread if it's not already running + fn start_render_thread_if_needed() { + let mut render_thread_guard = RENDER_THREAD.lock().unwrap(); + + // Only start a new thread if one isn't already running + if render_thread_guard.is_none() { + // Set the rendering active flag + RENDERING_ACTIVE.store(true, Ordering::SeqCst); + + // Clone the Arc for the thread + let cursor_image_arc = CURRENT_CURSOR_IMAGE.clone(); + + // Create a new thread for continuous rendering + let handle = thread::spawn(move || { + println!("Starting cursor render thread"); + + // Render loop + while RENDERING_ACTIVE.load(Ordering::SeqCst) { + render_cursor_internal(&cursor_image_arc); + + // Sleep to control frame rate (60 FPS) + thread::sleep(Duration::from_millis(16)); + } + + println!("Cursor render thread stopped"); + }); + + // Store the thread handle + *render_thread_guard = Some(handle); + } + } + + // Internal function to render the cursor (called from the render thread) + fn render_cursor_internal(cursor_image_arc: &Arc>>) { + // Get the cursor image + let cursor_image_lock = cursor_image_arc.lock().unwrap(); + if let Some(ref cursor_image) = *cursor_image_lock { + unsafe { + // Get the current mouse position + let mut point = POINT { x: 0, y: 0 }; + if GetCursorPos(&mut point) == 0 { + return; + } + + // Get image dimensions + let (width, height) = cursor_image.dimensions(); + + // Get or create the cursor window + let hwnd = unsafe { + if CURSOR_WINDOW.is_none() { + // Create a new window if we don't have one + if let Some(window) = create_cursor_window() { + CURSOR_WINDOW = Some(window); + } else { + return; + } + } + + // Unwrap is safe because we just checked it's Some + CURSOR_WINDOW.unwrap() + }; + + // Move the window to the cursor position + // Adjust position to center the cursor image on the actual cursor point + SetWindowPos( + hwnd, + HWND_TOPMOST, + point.x - (width as i32 / 2), + point.y - (height as i32 / 2), + width as i32, + height as i32, + 0 + ); + + // Get the window DC + let hwnd_dc = GetDC(hwnd); + if hwnd_dc.is_null() { + return; + } + + // Create a compatible DC for double-buffering + let mem_dc = CreateCompatibleDC(hwnd_dc); + if mem_dc.is_null() { + ReleaseDC(hwnd, hwnd_dc); + return; + } + + // Create a compatible bitmap + let bitmap = CreateCompatibleBitmap(hwnd_dc, width as i32, height as i32); + if bitmap.is_null() { + DeleteDC(mem_dc); + ReleaseDC(hwnd, hwnd_dc); + return; + } + + // Select the bitmap into the memory DC + let old_bitmap = SelectObject(mem_dc, bitmap as _); + + // Draw the cursor image to the memory DC + for y in 0..height { + for x in 0..width { + let pixel = cursor_image.get_pixel(x, y); + + // Only draw non-transparent pixels + if pixel[3] > 128 { // Alpha channel > 50% + let color = RGB(pixel[0], pixel[1], pixel[2]); + SetPixel(mem_dc, x as i32, y as i32, color); + } + } + } + + // Copy the memory DC to the window DC using BitBlt + use winapi::um::wingdi::BitBlt; + use winapi::um::wingdi::SRCCOPY; + + BitBlt( + hwnd_dc, + 0, 0, + width as i32, height as i32, + mem_dc, + 0, 0, + SRCCOPY + ); + + // Clean up + SelectObject(mem_dc, old_bitmap as _); + DeleteObject(bitmap as _); + DeleteDC(mem_dc); + ReleaseDC(hwnd, hwnd_dc); + } + } + } + + // Function to render the custom cursor (called from the application) + pub fn render_cursor() { + // This is now just a trigger to ensure the render thread is running + start_render_thread_if_needed(); + } +} + +// macOS implementation +#[cfg(target_os = "macos")] +mod macos { + use std::path::Path; + + pub fn set_cursor(_path: &Path) -> bool { + // macOS cursor implementation would go here + // This is a placeholder - actual implementation would require + // Objective-C/Cocoa bindings to NSCursor + println!("macOS cursor swapping not yet implemented"); + false + } + + pub fn restore_cursor() { + // Placeholder for restoring cursor on macOS + println!("macOS cursor restoration not yet implemented"); + } + + pub fn render_cursor() { + // On macOS, we would update the cursor on mouse movement + // This is a placeholder + } +} + +// Linux implementation +#[cfg(target_os = "linux")] +mod linux { + use std::path::Path; + + pub fn set_cursor(_path: &Path) -> bool { + // Linux cursor implementation would go here + // This is a placeholder - actual implementation would require + // X11 or Wayland bindings + println!("Linux cursor swapping not yet implemented"); + false + } + + pub fn restore_cursor() { + // Placeholder for restoring cursor on Linux + println!("Linux cursor restoration not yet implemented"); + } + + pub fn render_cursor() { + // On Linux, we would update the cursor on mouse movement + // This is a placeholder + } +} + +// Export the appropriate implementation based on platform +#[cfg(windows)] +pub use windows::{set_cursor, restore_cursor, render_cursor}; + +#[cfg(target_os = "macos")] +pub use macos::{set_cursor, restore_cursor, render_cursor}; + +#[cfg(target_os = "linux")] +pub use linux::{set_cursor, restore_cursor, render_cursor}; + +// Default implementation for other platforms +#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))] +pub fn set_cursor(_path: &Path) -> bool { + println!("Cursor swapping not supported on this platform"); + false +} + +#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))] +pub fn restore_cursor() { + println!("Cursor restoration not supported on this platform"); +} + +#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))] +pub fn render_cursor() { + // No-op for unsupported platforms +}