tauri integration

This commit is contained in:
2025-05-16 17:38:33 +02:00
parent 2e71cd8b9e
commit 06b2f0ffaf
66 changed files with 7239 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "cursor-rush"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.56"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.2", features = [] }
[dependencies]
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
image = "0.24"
# Ensure "consoleapi" is in this features list
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
once_cell = "1.19.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

+294
View File
@@ -0,0 +1,294 @@
use std::sync::Mutex;
use serde::{Serialize, Deserialize};
use tauri::{Manager, AppHandle};
use std::path::{Path, PathBuf};
use std::env;
mod platform;
// Define cursor structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursor {
name: String,
path: String,
}
// App state to track cursors
pub struct AppState {
cursors: Vec<Cursor>,
current_index: usize,
}
// Thread-safe app state wrapper
pub struct AppStateWrapper(Mutex<AppState>);
// Command to get all cursors
#[tauri::command]
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
let app_state = state.0.lock().unwrap();
app_state.cursors.clone()
}
// Command to get current cursor index
#[tauri::command]
fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
let app_state = state.0.lock().unwrap();
app_state.current_index
}
// Command to select a cursor
#[tauri::command]
fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> {
println!("=== RESOURCE DEBUG [select_cursor] ===");
let mut app_state = state.0.lock().unwrap();
if index < app_state.cursors.len() {
app_state.current_index = index;
let cursor = &app_state.cursors[index];
println!("Cursor selected: {}", cursor.name);
println!("Original path: {}", cursor.path);
// Get the resource directory
let resource_dir = app_handle.path_resolver().resource_dir();
println!("Resource dir: {:?}", resource_dir);
// List contents of resource dir
if let Some(res_dir) = &resource_dir {
println!("Listing contents of resource dir: {}", res_dir.display());
if let Ok(entries) = std::fs::read_dir(res_dir) {
for (i, entry) in entries.filter_map(Result::ok).enumerate() {
println!(" {}: {}", i, entry.path().display());
// List subdirectories
if entry.path().is_dir() {
if let Ok(subentries) = std::fs::read_dir(entry.path()) {
for (j, subentry) in subentries.filter_map(Result::ok).enumerate() {
println!(" {}.{}: {}", i, j, subentry.path().display());
}
}
}
}
} else {
println!("Failed to read resource directory");
}
}
let filename = Path::new(&cursor.path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
println!("Extracted filename: {}", filename);
// Try using the actual file path directly
println!("Using direct file path: {}", cursor.path);
// Verify the file exists
if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
}
// Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", index); // Ignore error
println!("Successfully applied cursor using actual file path");
return Ok(());
} else {
println!("File doesn't exist: {}", cursor.path);
return Err(format!("Cursor file does not exist: {}", cursor.path));
}
} else {
Err("Index out of bounds".to_string())
}
}
// Command to go to the next cursor
#[tauri::command]
fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> {
println!("=== RESOURCE DEBUG [next_cursor] ===");
let mut app_state = state.0.lock().unwrap();
if !app_state.cursors.is_empty() {
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
let cursor = &app_state.cursors[app_state.current_index];
println!("Next cursor: {} ({})", cursor.name, cursor.path);
let filename = Path::new(&cursor.path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
// Try both resource paths like in select_cursor
println!("Extracted filename: {}", filename);
// Try using the actual file path directly
println!("Using direct file path: {}", cursor.path);
// Verify the file exists
if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
}
// Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", app_state.current_index); // Ignore error
println!("Successfully applied cursor using actual file path");
return Ok(app_state.current_index);
} else {
println!("File doesn't exist: {}", cursor.path);
return Err(format!("Cursor file does not exist: {}", cursor.path));
}
} else {
Err("No cursors available".to_string())
}
}
// Command to quit the application
#[tauri::command]
fn quit_app(app_handle: AppHandle) {
// Clean up and restore default cursor before exit
match platform::restore_system_cursor() {
Ok(_) => println!("Default cursor restored"),
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
}
// Exit the application
std::thread::spawn(move || {
app_handle.exit(0);
});
}
// Function to load cursor files from src/cursors directory
fn load_cursor_files() -> Vec<Cursor> {
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
let mut cursors = Vec::new();
// Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR)
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display());
// Check multiple possible locations
let possible_cursor_dirs = vec![
manifest_dir.join("../src/cursors"),
manifest_dir.join("src/cursors"),
manifest_dir.join("cursors"),
manifest_dir.clone(), // Check manifest dir itself
];
println!("CHECKING FOR PNG/CUR FILES IN MULTIPLE DIRECTORIES:");
for (i, cursors_dir) in possible_cursor_dirs.iter().enumerate() {
println!("Checking dir {}: {}", i+1, cursors_dir.display());
println!(" Directory exists: {}", cursors_dir.exists());
if cursors_dir.exists() {
println!(" Looking for cursor files in this directory...");
if let Ok(entries) = std::fs::read_dir(cursors_dir) {
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
println!(" Found: {}", path.display());
if path.is_file() && path.extension().map_or(false, |ext| ext == "png" || ext == "cur") {
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(Cursor {
name,
path: path.to_string_lossy().to_string(),
});
}
}
}
if !cursors.is_empty() {
println!(" Found {} cursor files in this directory.", cursors.len());
break;
}
}
}
// If we found no cursors, use mock data with paths relative to what resolve_resource expects
if cursors.is_empty() {
println!("No cursors found in any directory. Using mock data instead.");
println!("IMPORTANT: This is probably the source of your problem!");
println!("The app could not find your cursor PNG files. Check that they exist in one of these directories:");
for dir in &possible_cursor_dirs {
println!(" {}", dir.display());
}
// Mock data paths should reflect their eventual resource path structure
cursors.push(Cursor {
name: "Default".to_string(),
path: "src/cursors/default.png".to_string(), // Filename will be 'default.png'
});
cursors.push(Cursor {
name: "Icon".to_string(),
path: "src/cursors/icon.png".to_string(), // Filename will be 'icon.png'
});
cursors.push(Cursor {
name: "Red".to_string(),
path: "src/cursors/red.png".to_string(), // Filename will be 'red.png'
});
println!("Created {} mock cursor entries", cursors.len());
}
println!("Loaded {} cursor files", cursors.len());
// Sort cursors by name for consistent ordering
cursors.sort_by(|a, b| a.name.cmp(&b.name));
cursors
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
println!("Starting Cursor Rush application");
// Initialize platform-specific cursor handling
if let Err(e) = platform::initialize() {
eprintln!("Failed to initialize platform: {}", e);
}
// Load cursors from assets directory
let cursors = load_cursor_files();
// Create app state
let app_state = AppState {
cursors,
current_index: 0,
};
// This is one of the simplest ways to initialize Tauri
tauri::Builder::default()
.manage(AppStateWrapper(Mutex::new(app_state)))
.invoke_handler(tauri::generate_handler![
get_cursors,
get_current_cursor_index,
select_cursor,
next_cursor,
quit_app
])
.setup(|_app| {
// Register for events or perform additional setup
println!("Tauri application setup complete");
Ok(())
})
// This matches exactly how it was before our changes
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cursor_rush::run() // Corrected line
}
+20
View File
@@ -0,0 +1,20 @@
// Linux platform implementation for cursor handling
use super::Result;
// Initialize cursor handling
pub fn init() -> Result<()> {
println!("Linux cursor initialization not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn set_cursor(path: &str) -> Result<()> {
// Linux cursor implementation would go here
println!("Linux cursor setting not yet implemented for path: {}", path);
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn restore_default_cursor() -> Result<()> {
// Linux cursor restoration would go here
println!("Linux cursor restoration not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
+20
View File
@@ -0,0 +1,20 @@
// macOS platform implementation for cursor handling
use super::Result;
// Initialize cursor handling
pub fn init() -> Result<()> {
println!("macOS cursor initialization not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn set_cursor(path: &str) -> Result<()> {
// macOS cursor implementation would go here
println!("macOS cursor setting not yet implemented for path: {}", path);
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn restore_default_cursor() -> Result<()> {
// macOS cursor restoration would go here
println!("macOS cursor restoration not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
+64
View File
@@ -0,0 +1,64 @@
// Platform-specific cursor handling
// Common types for cursor handling
pub mod types;
// Windows implementation
#[cfg(target_os = "windows")]
mod windows;
// macOS implementation
#[cfg(target_os = "macos")]
mod macos;
// Linux implementation
#[cfg(target_os = "linux")]
mod linux;
// Error type for platform operations
pub type Result<T> = std::result::Result<T, String>;
// Platform-agnostic interface for cursor handling
#[cfg(target_os = "windows")]
pub use self::windows::{set_cursor, restore_default_cursor, init};
#[cfg(target_os = "macos")]
pub use self::macos::{set_cursor, restore_default_cursor, init};
#[cfg(target_os = "linux")]
pub use self::linux::{set_cursor, restore_default_cursor, init};
// Initialize platform-specific cursor handling
pub fn initialize() -> Result<()> {
#[cfg(target_os = "windows")]
{
init()
}
#[cfg(not(target_os = "windows"))]
{
Ok(())
}
}
// Cross-platform implementation for setting cursor
pub fn set_system_cursor(path: &str) -> Result<()> {
println!("Setting system cursor: {}", path);
set_cursor(path)
}
// Cross-platform implementation for restoring default cursor
pub fn restore_system_cursor() -> Result<()> {
println!("Restoring system cursor");
restore_default_cursor()
}
// Default implementations for non-supported platforms
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn set_cursor(_path: &str) -> Result<()> {
Err("Cursor customization not supported on this platform".to_string())
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn restore_default_cursor() -> Result<()> {
Err("Cursor restoration not supported on this platform".to_string())
}
+26
View File
@@ -0,0 +1,26 @@
use std::path::PathBuf;
use image::RgbaImage;
// Windows-specific cursor handle wrapper
#[cfg(target_os = "windows")]
#[derive(Debug, Clone, Copy)]
pub struct WinCursorWrapper {
pub handle: winapi::shared::windef::HCURSOR,
}
#[cfg(target_os = "windows")]
unsafe impl Send for WinCursorWrapper {}
// Common type for cursor information
#[derive(Debug, Clone)]
pub struct CursorInfo {
pub name: String,
pub path: PathBuf,
pub image: Option<RgbaImage>,
pub hotspot_x: i32,
pub hotspot_y: i32,
// Platform-specific fields
#[cfg(target_os = "windows")]
pub win_cursor: Option<WinCursorWrapper>,
}
@@ -0,0 +1,512 @@
// Windows platform cursor management
use std::ptr::null_mut;
use std::sync::atomic::Ordering;
use winapi::{
um::winuser::{
GetDC, ReleaseDC, LoadCursorW, SetSystemCursor, CopyIcon,
SystemParametersInfoW, SPI_SETCURSORS, SetCursor,
MAKEINTRESOURCEW, IDC_ARROW, CreateIconIndirect, ICONINFO,
DestroyIcon,
},
um::wingdi::{
CreateCompatibleDC, DeleteDC, SelectObject, DeleteObject,
BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, BI_RGB, CreateDIBSection,
},
um::consoleapi::SetConsoleCtrlHandler,
shared::minwindef::{TRUE, FALSE, DWORD},
};
use ::image::{RgbaImage, open as image_open};
use super::types::*;
use super::state::*;
// Control handler function for console events (Ctrl+C, close, etc.)
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
println!("Control event received, attempting cleanup...");
cleanup(); // Call cleanup logic
FALSE // Allow other handlers (like default exit) to run
}
// Create a cursor from an RGBA image
unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
let (width, height) = image.dimensions();
let width_i32 = width as i32;
let height_i32 = height as i32;
// Create device context and bitmap for the color data
let screen_dc = GetDC(null_mut());
if screen_dc.is_null() {
eprintln!("Failed to get screen DC");
return None;
}
// Create a compatible DC for our bitmap
let color_dc = CreateCompatibleDC(screen_dc);
if color_dc.is_null() {
eprintln!("Failed to create compatible DC for color");
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Create a compatible DC for our mask bitmap
let mask_dc = CreateCompatibleDC(screen_dc);
if mask_dc.is_null() {
eprintln!("Failed to create compatible DC for mask");
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Set up BITMAPINFO structure for DIB
let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bitmap_info.bmiHeader.biWidth = width_i32;
bitmap_info.bmiHeader.biHeight = -height_i32; // Negative for top-down DIB
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA)
bitmap_info.bmiHeader.biCompression = BI_RGB;
// Create the color DIB section
let mut color_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
let mut ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
let color_bitmap = CreateDIBSection(
color_dc,
&bitmap_info,
DIB_RGB_COLORS,
&mut ppv_bits_color,
null_mut(),
0,
);
color_bits_ptr = ppv_bits_color;
if color_bitmap.is_null() || color_bits_ptr.is_null() {
eprintln!("Failed to create color DIB section");
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Create the mask DIB section (1-bit monochrome)
bitmap_info.bmiHeader.biBitCount = 1; // 1 bit per pixel for mask
let mut mask_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
let mut ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
let mask_bitmap = CreateDIBSection(
mask_dc,
&bitmap_info,
DIB_RGB_COLORS,
&mut ppv_bits,
null_mut(),
0,
);
mask_bits_ptr = ppv_bits;
if mask_bitmap.is_null() || mask_bits_ptr.is_null() {
eprintln!("Failed to create mask DIB section");
DeleteObject(color_bitmap as _);
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Select the bitmaps into the DCs
let old_color_bitmap = SelectObject(color_dc, color_bitmap as _);
let old_mask_bitmap = SelectObject(mask_dc, mask_bitmap as _);
if old_color_bitmap.is_null() || old_mask_bitmap.is_null() {
eprintln!("Failed to select bitmaps into DCs");
DeleteObject(mask_bitmap as _);
DeleteObject(color_bitmap as _);
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Copy the image data to the color DIB, converting RGBA to BGRA
let color_bits = std::slice::from_raw_parts_mut(color_bits_ptr as *mut u8, (width * height * 4) as usize);
// Calculate mask stride in bytes (1 bit per pixel, padded to DWORD)
let mask_stride = ((width + 31) / 32) * 4;
let mask_bits = std::slice::from_raw_parts_mut(mask_bits_ptr as *mut u8, (mask_stride * height) as usize);
// Clear mask bits (all transparent initially)
for i in 0..(mask_stride * height) as usize {
mask_bits[i] = 0xFF; // All bits set = transparent
}
// Process image pixels
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y);
let color_idx = ((y * width + x) * 4) as usize;
// Get RGBA components
let r = pixel[0];
let g = pixel[1];
let b = pixel[2];
let a = pixel[3];
// Set color data (BGR order)
color_bits[color_idx + 0] = b;
color_bits[color_idx + 1] = g;
color_bits[color_idx + 2] = r;
color_bits[color_idx + 3] = a;
// Update mask bit if pixel is not transparent
if a > 0 {
// Calculate bit position in mask
let byte_idx = (y * mask_stride + (x / 8)) as usize;
let bit_idx = 7 - (x % 8); // Bits are stored MSB first
// Clear the bit (0 = opaque in AND mask)
mask_bits[byte_idx] &= !(1 << bit_idx);
}
}
}
// Create ICONINFO structure
let mut icon_info: ICONINFO = std::mem::zeroed();
icon_info.fIcon = FALSE; // This is a cursor, not an icon
icon_info.xHotspot = width as DWORD / 2; // Center hotspot
icon_info.yHotspot = height as DWORD / 2;
icon_info.hbmMask = mask_bitmap;
icon_info.hbmColor = color_bitmap;
// Create the cursor
let cursor = CreateIconIndirect(&mut icon_info as *mut _);
// Clean up resources
SelectObject(color_dc, old_color_bitmap);
SelectObject(mask_dc, old_mask_bitmap);
DeleteDC(color_dc);
DeleteDC(mask_dc);
ReleaseDC(null_mut(), screen_dc);
// Windows makes a copy of the bitmaps for the cursor, so we can delete them
DeleteObject(color_bitmap as _);
DeleteObject(mask_bitmap as _);
if cursor.is_null() {
eprintln!("Failed to create cursor");
return None;
}
// Return the cursor wrapped in our thread-safe type
Some(SyncHCURSOR(cursor))
}
// Cleanup function to restore cursors and clean up resources
pub fn cleanup() {
println!("Running cursor cleanup...");
// Restore system cursors if they were hidden
// Use swap to ensure it runs only once
if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) {
println!("Restoring system cursors...");
unsafe {
// Try multiple approaches to ensure cursors are restored
// 1. First try to restore original cursors if we saved them
let original_cursors_opt = ORIGINAL_CURSORS.lock().unwrap().take();
if let Some(original_cursors) = original_cursors_opt {
println!("Restoring {} saved original cursors", original_cursors.len());
for (cursor_id, cursor) in original_cursors {
if !cursor.0.is_null() {
// Make a copy of the original cursor
let cursor_copy = CopyIcon(cursor.0);
if !cursor_copy.is_null() {
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to restore original cursor for ID: {}", cursor_id);
DestroyIcon(cursor_copy);
}
} else {
eprintln!("Failed to copy original cursor for ID: {}", cursor_id);
}
// Destroy the original cursor we saved
DestroyIcon(cursor.0);
}
}
} else {
println!("No saved original cursors found");
}
// 2. Force Windows to reload default cursor settings
println!("Forcing Windows to reload default cursor settings");
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
// 3. Also try the A version for maximum compatibility
super::state::reset_system_cursors();
// 4. Reset to default arrow cursor
let arrow_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
if !arrow_cursor.is_null() {
SetCursor(arrow_cursor);
println!("Reset to default arrow cursor.");
}
}
println!("System cursors restoration complete.");
}
// Destroy the custom cursor if it exists
let custom_cursor_to_destroy = CUSTOM_CURSOR.lock().unwrap().take();
if let Some(cursor_wrapper) = custom_cursor_to_destroy {
unsafe {
if !cursor_wrapper.0.is_null() {
DestroyIcon(cursor_wrapper.0);
println!("Custom cursor destroyed during cleanup.");
}
}
}
// Reset the last known cursor position
*LAST_CURSOR_POS.lock().unwrap() = None;
// Clear the current cursor image
*CURRENT_CURSOR_IMAGE.lock().unwrap() = None;
// Unregister control handler
if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) {
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 {
CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst);
println!("Control handler unregistered.");
} else {
eprintln!("Failed to unregister control handler.");
}
}
}
println!("Cursor cleanup complete.");
}
// --- Public API Functions ---
// Hide the system cursor - now just initializes the control handler
pub fn hide_system_cursor() -> bool {
// Initialize once
INIT.get_or_init(|| {
// Register control handler for clean exit
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
println!("Control handler registered for clean exit.");
} else {
eprintln!("Failed to register control handler: {}", std::io::Error::last_os_error());
}
}
});
// Check if already hidden
if CURSOR_HIDDEN.load(Ordering::SeqCst) {
return true; // Already hidden
}
// Save original cursors for restoration
let mut original_cursors = Vec::new();
unsafe {
for &cursor_id in SYSTEM_CURSORS.iter() {
// Get the original cursor
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
if !original_cursor.is_null() {
// Save the original cursor for restoration
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
}
}
}
// Store original cursors for restoration
if !original_cursors.is_empty() {
*ORIGINAL_CURSORS.lock().unwrap() = Some(original_cursors);
}
// Mark as hidden - this is now just a flag to indicate we've saved the original cursors
CURSOR_HIDDEN.store(true, Ordering::SeqCst);
true
}
// Update the cursor image using CursorInfo (with caching)
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool {
println!("Updating cursor to: {}", cursor_info.name);
// Use cached image if available, otherwise load from disk
let image: image::RgbaImage = if let Some(ref cached_image) = cursor_info.image {
println!("Using cached image for cursor: {}", cursor_info.name);
let img_clone: image::RgbaImage = cached_image.clone();
img_clone
} else {
println!("Loading image from disk for cursor: {}", cursor_info.name);
match image_open(&cursor_info.path) {
Ok(img) => {
let rgba = img.to_rgba8();
// Cache the image for future use
// Make sure to specify the type to avoid type inference issues
let image_copy: image::RgbaImage = rgba.clone();
cursor_info.image = Some(image_copy);
rgba
},
Err(e) => {
eprintln!("Failed to load cursor image: {}", e);
return false;
}
}
};
// Store the image in the global state for reference
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone());
unsafe {
// Create a new cursor first before destroying the old one
// This minimizes the time when no custom cursor is active
let new_cursor = if let Some(ref cached_image) = cursor_info.image {
// Always create a fresh cursor from the image to avoid invalid handle issues
create_cursor_from_image(cached_image)
} else {
create_cursor_from_image(&image)
};
if let Some(new_cursor_wrapper) = new_cursor {
// Apply the new cursor to all system cursors first
let success = apply_cursor_to_all_system_cursors(new_cursor_wrapper.0);
// Now that the new cursor is applied, we can clean up the old one
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
if let Some(old_cursor) = cursor_guard.take() {
// Only destroy the old cursor after the new one is applied
DestroyIcon(old_cursor.0);
}
// Store the new cursor
*cursor_guard = Some(new_cursor_wrapper);
// Update the cached cursor in CursorInfo
#[cfg(windows)]
{
// Convert SyncHCURSOR to WinCursorWrapper for storage in cursor_info
cursor_info.win_cursor = Some(crate::platform::types::WinCursorWrapper {
handle: new_cursor_wrapper.0
});
}
if !success {
eprintln!("Failed to apply cursor to all system cursors");
// Even if we failed to set all system cursors, try to at least set the current cursor
SetCursor(new_cursor_wrapper.0);
}
return true;
} else {
eprintln!("Failed to create cursor from image");
// If we failed to create a new cursor, try to use the cached one as a fallback
#[cfg(windows)]
if let Some(cached_cursor) = &cursor_info.win_cursor {
if !cached_cursor.handle.is_null() {
println!("Falling back to cached cursor as a last resort");
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
// Convert WinCursorWrapper to SyncHCURSOR
let sync_cursor = super::types::SyncHCURSOR(cached_cursor.handle);
*cursor_guard = Some(sync_cursor);
SetCursor(cached_cursor.handle);
return true;
}
}
return false;
}
}
}
// Apply a cursor to all system cursors
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
let mut success = true;
// First, ensure the cursor is valid
if cursor.is_null() {
eprintln!("Cannot apply null cursor to system cursors");
return false;
}
// Immediately set the current cursor to minimize flashing
SetCursor(cursor);
// Apply the cursor to all system cursor types
// Start with the most commonly used cursors first
let priority_cursors = [
32512, // IDC_ARROW (most important)
32513, // IDC_IBEAM (text cursor)
32649, // IDC_HAND (links)
];
// Apply to priority cursors first
for &cursor_id in priority_cursors.iter() {
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor);
if cursor_copy.is_null() {
let error = std::io::Error::last_os_error();
eprintln!("Failed to copy cursor for priority ID: {} - Error: {}", cursor_id, error);
success = false;
continue;
}
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
let error = std::io::Error::last_os_error();
eprintln!("Failed to set cursor for priority ID: {} - Error: {}", cursor_id, error);
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
success = false;
}
}
// Now apply to the remaining cursors
for &cursor_id in SYSTEM_CURSORS.iter() {
// Skip the priority cursors we already processed
if priority_cursors.contains(&cursor_id) {
continue;
}
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor);
if cursor_copy.is_null() {
// Don't log errors for non-priority cursors to reduce console spam
success = false;
continue;
}
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
// Don't log errors for non-priority cursors to reduce console spam
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
success = false;
}
}
// If we had any failures, set the current cursor again as a fallback
if !success {
SetCursor(cursor);
}
success
}
// Restore the system cursor
pub fn restore_cursor() {
cleanup();
}
// Function to handle mouse movement and update the cursor
// Kept for potential future use but currently not needed with SetSystemCursor approach
#[allow(dead_code)]
pub fn handle_mouse_move() {
// With SetSystemCursor, we don't need to constantly update the cursor on mouse move
// This function is kept for API compatibility, but it's now a no-op
// System cursors are already set to our custom cursor
}
@@ -0,0 +1,45 @@
// Windows platform implementation module
pub mod types;
mod cursor;
mod state;
use std::path::PathBuf;
use crate::platform::Result;
// Functions expected by the platform-agnostic interface
pub fn set_cursor(path: &str) -> Result<()> {
println!("Windows platform: setting cursor to {}", path);
// Create a cursor info struct to pass to update_cursor_info
let mut cursor_info = crate::platform::types::CursorInfo {
name: path.split('\\').last().unwrap_or("unknown").to_string(),
path: PathBuf::from(path),
image: None,
hotspot_x: 0, // Default hotspot at top-left
hotspot_y: 0,
#[cfg(target_os = "windows")]
win_cursor: None,
};
// Use the existing update_cursor_info function to handle loading and applying the cursor
if !cursor::update_cursor_info(&mut cursor_info) {
return Err(format!("Failed to update cursor from {}", path));
}
Ok(())
}
pub fn restore_default_cursor() -> Result<()> {
println!("Windows platform: restoring default cursor");
cursor::restore_cursor();
Ok(())
}
pub fn init() -> Result<()> {
println!("Windows platform: initializing cursor system");
if !cursor::hide_system_cursor() {
return Err("Failed to initialize cursor system".to_string());
}
Ok(())
}
@@ -0,0 +1,57 @@
// Windows platform state management
use std::sync::{Mutex, atomic::AtomicBool};
use winapi::{
shared::minwindef::DWORD,
shared::windef::POINT,
};
use ::image::RgbaImage;
use once_cell::sync::{Lazy, OnceCell};
use super::types::*;
// --- Thread-Safe Static Variables ---
// Use AtomicBool for simple flags
pub static CURSOR_HIDDEN: AtomicBool = AtomicBool::new(false);
pub static CTRL_HANDLER_REGISTERED: AtomicBool = AtomicBool::new(false);
// Use Lazy<Mutex<T>> for complex Option types
pub static ORIGINAL_CURSORS: Lazy<Mutex<Option<Vec<(DWORD, SyncHCURSOR)>>>> = Lazy::new(|| Mutex::new(None));
pub static CURRENT_CURSOR_IMAGE: Lazy<Mutex<Option<RgbaImage>>> = Lazy::new(|| Mutex::new(None));
pub 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
pub static CUSTOM_CURSOR: Lazy<Mutex<Option<SyncHCURSOR>>> = Lazy::new(|| Mutex::new(None));
// Note: Legacy variables for window-based cursor approach and mouse hook tracking
// have been removed as part of code cleanup
// Use OnceCell for INIT to avoid `static mut`
pub static INIT: OnceCell<()> = OnceCell::new();
// Reset system cursors using the Windows API
pub fn reset_system_cursors() {
unsafe {
// Use both versions for maximum compatibility
winapi::um::winuser::SystemParametersInfoW(
winapi::um::winuser::SPI_SETCURSORS,
0,
std::ptr::null_mut(),
0
);
// 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) -> i32;
}
SystemParametersInfoA(
SPI_SETCURSORS_A,
0,
std::ptr::null_mut(),
SPIF_UPDATEINIFILE | SPIF_SENDCHANGE
);
}
}
@@ -0,0 +1,40 @@
// Windows platform types and constants
use winapi::{
shared::windef::HCURSOR,
shared::minwindef::DWORD,
};
// --- Thread-Safe Handle Wrappers ---
// These wrappers make raw Windows handles safe to share between threads
// Wrapper for HCURSOR (cursor handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHCURSOR(pub HCURSOR);
unsafe impl Send for SyncHCURSOR {}
unsafe impl Sync for SyncHCURSOR {}
// --- Constants ---
// Constants for SystemParametersInfo
pub const SPI_SETCURSORS_A: u32 = 0x0057;
pub const SPIF_UPDATEINIFILE: u32 = 0x01;
pub const SPIF_SENDCHANGE: u32 = 0x02;
// Note: Constants for window-based cursor approach have been removed as part of code cleanup
// Define all system cursor IDs we'll replace
pub 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
];
+60
View File
@@ -0,0 +1,60 @@
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:5173",
"distDir": "../dist"
},
"package": {
"productName": "cursor-rush",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false,
"window": {
"all": false,
"startDragging": true
},
"shell": {
"all": false,
"open": true
},
"fs": {
"all": false,
"scope": [
"$APP/cursors/*"
]
}
},
"bundle": {
"active": true,
"identifier": "com.cursor-rush.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"targets": "all"
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"title": "Cursor Rush",
"width": 310,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true
}
]
}
}