13 Commits
Author SHA1 Message Date
Joby aa039c7291 I just needed to do something so I felt useful today
- Eliminated duplicate platform stubs: Created shared macro for macOS/Linux implementations
- Extracted cursor restoration logic: Shared helper function for Windows API calls
- Refactored cursor application: Helper function for copy/apply/error handling pattern
- Reduced ~70 lines of duplicate code while maintaining all functionality
2025-06-09 20:09:24 -05:00
jcweaver997 73634440cf added fire circle cursor 2025-05-31 16:08:00 -05:00
jcweaver997 eced2a561c added basic animation 2025-05-31 15:36:20 -05:00
jcweaver997 61157a2402 bundle cursors into cursor_rush.dat, started animator 2025-05-31 00:38:29 -05:00
Joby 67ee3dc8d1 Update README with current versions and accurate info 2025-05-30 23:26:41 -05:00
Joby 7016e96673 Refactor large functions into smaller components 2025-05-30 23:20:39 -05:00
Joby 09374eecb3 Replace mutex unwraps with proper error handling 2025-05-30 23:10:43 -05:00
Joby 4f71789710 Clean up debug logging spam
- Remove ~101 useless debug print statements from production code
- Remove verbose resource debugging and repetitive status messages
- Significantly reduce console noise while maintaining error visibility
2025-05-30 22:59:25 -05:00
Joby a9976cbbe5 Fix unsafe thread-safety docs for cursor handles 2025-05-30 22:38:50 -05:00
Joby bba65f2f97 refactor: eliminate code duplication in cursor operations and cleanup 2025-05-30 22:29:24 -05:00
Joby c1b8160f4a refactor: eliminate code duplication in cursor operations and cleanup
- Extract cursor application logic into apply_cursor_by_index() helper
- Extract cleanup logic into perform_app_cleanup() helper
- Update select_cursor(), next_cursor(), and system tray handlers to use helpers
- Reduce ~55 lines of duplicated code while maintaining functionality
- Mark step 1 of code cleanup TODO as completed
2025-05-30 22:24:33 -05:00
Joby 16fc8949b3 Fixed window dragging for main menu 2025-05-30 21:20:33 -05:00
jcweaver997 b31aefe896 fix steam integration 2025-05-30 19:28:34 -05:00
91 changed files with 999 additions and 540 deletions
+1
View File
@@ -1,3 +1,4 @@
/target /target
**/*.rs.bk **/*.rs.bk
Cargo.lock Cargo.lock
CODE_CLEANUP_TODO.md
+4
View File
@@ -0,0 +1,4 @@
[workspace]
members = ["frontend/src-tauri"]
resolver = "3"
+48 -12
View File
@@ -42,23 +42,25 @@ frontend/
### Prerequisites ### Prerequisites
- Rust 1.70.0 or later - Rust 1.56.0 or later (tested with 1.86.0)
- Cargo - Cargo
- Node.js 16.0.0 or later - Node.js 16.0.0 or later (tested with 20.11.0)
- npm 7.0.0 or later - npm 7.0.0 or later (tested with 10.2.4)
- Platform-specific dependencies: - Platform-specific dependencies:
- Windows: None (fully supported) - Windows: Visual Studio Build Tools (fully supported)
- macOS: Development in progress - macOS: Development in progress
- Linux: Development in progress - Linux: Development in progress
### Dependencies ### Dependencies
The application uses the following major dependencies: The application uses the following major dependencies:
- Tauri 1.6.0 - Cross-platform framework - Tauri 1.8.3 - Cross-platform framework
- React - Frontend framework - React 19.1.0 - Frontend framework
- image 0.24.7 - Image processing - image 0.24 - Image processing
- winapi 0.3.9 - Windows API bindings - winapi 0.3.9 - Windows API bindings
- once_cell 1.19.0 - Lazy initialization - once_cell 1.19.0 - Lazy initialization
- steamworks 0.11.0 - Steam integration
- parking_lot 0.12.3 - High-performance synchronization
### Build Commands ### Build Commands
@@ -69,11 +71,15 @@ cd frontend
# Install npm dependencies # Install npm dependencies
npm install npm install
# Run in development mode # Run in development mode (starts both frontend and backend)
npm run tauri dev npm run tauri dev
# Build for production # Build for production
npm run tauri build npm run tauri build
# Alternative: Run frontend and backend separately for debugging
# Terminal 1: npm run dev (starts Vite dev server)
# Terminal 2: npm run tauri dev --no-dev-server (starts Tauri without dev server)
``` ```
## Usage ## Usage
@@ -95,11 +101,16 @@ The application comes with sample cursors:
- `red.png` - A red-colored cursor - `red.png` - A red-colored cursor
To add your own cursors: To add your own cursors:
1. Create PNG images with transparent backgrounds 1. Create PNG or CUR images with transparent backgrounds
2. Place them in the `frontend/src/cursors` directory 2. Place them in the `frontend/src/cursors` directory
3. Restart the application to load the new cursors 3. Restart the application to load the new cursors
4. The cursor name in the UI will be derived from the filename 4. The cursor name in the UI will be derived from the filename
**Note**: The application automatically scans multiple directories for cursor files:
- `frontend/src/cursors/` (recommended location)
- `frontend/src-tauri/src/cursors/`
- `frontend/src-tauri/cursors/`
## Hotkeys ## Hotkeys
- Ctrl+Shift+F12: Exit the application (restores original system cursor) - Ctrl+Shift+F12: Exit the application (restores original system cursor)
@@ -120,17 +131,42 @@ To add your own cursors:
- The application uses event-driven cursor updates instead of polling when possible - The application uses event-driven cursor updates instead of polling when possible
- Cursor images are cached to improve performance when switching between cursors - Cursor images are cached to improve performance when switching between cursors
## Previously Fixed Issues ## Known Issues & Status
### Recently Fixed Issues
The following issues have been addressed in recent updates: The following issues have been addressed in recent updates:
- Task Manager compatibility: The application now properly handles elevated applications - Task Manager compatibility: The application now properly handles elevated applications
- System tray menu stability: Fixed issues with the tray menu disappearing when mousing over it - System tray menu stability: Fixed issues with the tray menu disappearing when mousing over it
### Current Known Issues
- Some system tray menu interactions may still be inconsistent on certain Windows versions
- Z-ordering issues where custom cursor may hide behind system tray menus due to Windows UI prioritization
- Performance may be affected when Task Manager or other elevated applications are open due to Windows security restrictions
## Steam Integration ## Steam Integration
The application includes integration with the Steam API for future gameified features: The application includes integration with the Steam API (AppID: 1430270):
- Shows as an active game in Steam for time tracking purposes
- Steam integration initializes automatically when the application starts
- Item drop functionality is implemented but not yet active - Item drop functionality is implemented but not yet active
- Steam must be running for this functionality to work - Steam must be running for the integration to work properly
- The application will appear in your Steam library as "running" while active
## Troubleshooting
### Build Issues
- **Missing Visual C++ Redistributable**: Install Visual Studio Build Tools or Visual C++ Redistributable packages
- **Node.js library issues**: Reinstall Node.js (version 16.0.0 or later) and ensure all dependencies are properly installed
- **Rust compilation errors**: Ensure Rust 1.56.0 or later is installed and up to date
### Runtime Issues
- **Application won't start**: Check that all dependencies are installed and Steam is running (if using Steam integration)
- **Cursor not changing**: Verify cursor files exist in the correct directory and have proper file permissions
- **System tray issues**: Try restarting the application or check Windows notification area settings
### Development Issues
- **Tauri dev server not starting**: Ensure port 5173 is available or specify a different port
- **Hot reload not working**: Check that the Vite dev server is running properly in the background
## Contributing ## Contributing
+9 -2
View File
@@ -6,12 +6,12 @@ authors = ["you"]
license = "" license = ""
repository = "" repository = ""
edition = "2021" edition = "2021"
rust-version = "1.56"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.5.2", features = [] } tauri-build = { version = "1.5.2", features = [] }
zip = { version = "4.0.0", features = ["zstd"] }
[dependencies] [dependencies]
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] } tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
@@ -21,7 +21,14 @@ image = "0.24"
# Ensure "consoleapi" is in this features list # Ensure "consoleapi" is in this features list
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] } winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
once_cell = "1.19.0" once_cell = "1.19.0"
steamworks = "0.10.0" steamworks = "0.11.0"
parking_lot = "0.12.3"
rand = "0.9.1"
tokio = { version = "1.45.1", features = ["macros", "parking_lot", "rt-multi-thread", "time"] }
zip = { version = "4.0.0", features = ["zstd"] }
anyhow = "1.0.98"
futures = "0.3.31"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!! # DO NOT REMOVE!!
+60 -2
View File
@@ -1,3 +1,61 @@
fn main() { use std::{env, fs::{self, read_dir}, io::Write, path::PathBuf};
tauri_build::build()
use zip::{write::SimpleFileOptions, ZipWriter};
fn main() -> Result<(), Box<dyn std::error::Error>> {
tauri_build::build();
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let profile_target_dir = out_dir
.ancestors()
.nth(3)
.ok_or("Failed to determine profile target directory from OUT_DIR")?;
println!(
"cargo:info=Build script running for target OS: {}",
env::var("CARGO_CFG_TARGET_OS").unwrap_or_default()
);
println!(
"cargo:info=Final libraries will be copied to: {}",
profile_target_dir.display()
);
// --- Define libraries and their source locations based on OS ---
let target_os = env::var("CARGO_CFG_TARGET_OS")?;
let source_subdir = if target_os == "windows" {
"lib/win64"
} else if target_os == "linux" {
"lib/linux64"
} else if target_os == "macos" {
"lib/osx"
} else {
return Err("Unsupported target".into());
};
for f in read_dir(source_subdir)? {
let f = f?;
println!("cargo:rerun-if-changed={}", f.path().display());
fs::copy(f.path(), profile_target_dir.join(f.file_name()))?;
}
// Build data file
println!("cargo:rerun-if-changed=src/cursors");
println!("cargo:rerun-if-changed=src/cursor_def.json");
let mut zip = ZipWriter::new(fs::OpenOptions::new().write(true).create(true).open(profile_target_dir.join("cursor_rush.dat"))?);
zip.start_file("cursor_def.json", SimpleFileOptions::default())?;
zip.write_all(&fs::read("src/cursor_def.json")?)?;
for f in read_dir("src/cursors")? {
let f = f?;
zip.start_file(f.file_name().to_string_lossy(), SimpleFileOptions::default())?;
zip.write_all(&fs::read(f.path())?)?;
}
zip.finish()?;
Ok(())
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+130
View File
@@ -0,0 +1,130 @@
use anyhow::Result;
use futures::FutureExt;
use image::{ImageFormat, RgbaImage};
use std::{env, fs, sync::Arc, time::Duration};
use tokio::time::interval;
use tokio::{select, time::Interval};
use zip::ZipArchive;
use crate::{
platform::{restore_default_cursor, set_system_cursor},
Cursor, CURSOR_CHANGED, CURSOR_CMD,
};
#[derive(Debug, Default)]
struct AnimationState {
cursor_index: Option<usize>,
frame_timer: Option<Interval>,
frame_index: usize,
loaded_image: Vec<RgbaImage>,
loaded_particles: Vec<Vec<RgbaImage>>,
}
#[tokio::main]
pub async fn animate_loop(cursors: Arc<Vec<Cursor>>) -> Result<()> {
let mut cmds = CURSOR_CMD.subscribe();
let mut selected: Option<usize> = None;
let mut animation_state = AnimationState::default();
let mut animation_interval = interval(Duration::from_secs_f32(1.0 / 30.0));
loop {
select! {
cmd = cmds.recv() => {
let cmd = cmd?;
println!("GOT CURSOR CMD {:?}", cmd);
match cmd {
crate::CursorCmd::Select(i) => {
if i < cursors.len() {
selected = Some(i);
}
}
crate::CursorCmd::Next => {
selected = Some(selected.map(|x| (x + 1) % cursors.len()).unwrap_or(0))
}
crate::CursorCmd::Reapply => {
},
crate::CursorCmd::Restore => {
},
}
CURSOR_CHANGED.send(selected.unwrap_or(0)).ok();
}
_ = animation_interval.tick() => {}
}
update_cursor(&mut animation_state, selected, &cursors).await?;
}
}
fn load(path: &str) -> Option<RgbaImage> {
let mut zip = ZipArchive::new(
fs::OpenOptions::new()
.read(true)
.open(env::current_exe().ok()?.parent()?.join("cursor_rush.dat"))
.ok()?,
)
.ok()?;
let mut def = zip.by_name(path).ok()?;
let mut bytes = Vec::new();
std::io::copy(&mut def, &mut bytes).ok();
Some(
image::load_from_memory_with_format(&bytes, ImageFormat::from_path(path).ok()?)
.ok()?
.to_rgba8(),
)
}
fn load_frames(frames: &Vec<String>) -> Vec<RgbaImage> {
frames.iter().map(|x| load(x)).flatten().collect()
}
async fn update_cursor(
animation_state: &mut AnimationState,
selected: Option<usize>,
cursors: &Vec<Cursor>,
) -> Result<()> {
if animation_state.cursor_index != selected {
*animation_state = AnimationState::default();
animation_state.cursor_index = selected;
if let Some(selected) = selected {
if cursors[selected].animation_speed > 0.0 {
animation_state.frame_timer = Some(interval(Duration::from_secs_f32(
1.0 / cursors[selected].animation_speed,
)))
}
animation_state.loaded_image = load_frames(&cursors[selected].frames);
animation_state.loaded_particles = cursors[selected]
.particles
.iter()
.map(|x| load_frames(&x.frames))
.collect();
if let Some(i) = animation_state.loaded_image.get(0) {
set_system_cursor(i)?;
}
} else {
restore_default_cursor()?;
}
} else {
if let Some(frame_timer) = &mut animation_state.frame_timer {
if frame_timer.tick().now_or_never().is_some() && animation_state.loaded_image.len() > 0
{
animation_state.frame_index += 1;
animation_state.frame_index %= animation_state.loaded_image.len();
if let Some(i) = animation_state
.loaded_image
.get(animation_state.frame_index)
{
set_system_cursor(i)?;
}
}
}
}
Ok(())
}
+96
View File
@@ -0,0 +1,96 @@
[
{
"name": "white",
"frames": [
"icon.png"
],
"animation_speed": 0.0,
"particles": []
},
{
"name": "red",
"frames": [
"red.png"
],
"animation_speed": 0.0,
"particles": []
},
{
"name": "flashing",
"frames": [
"red.png",
"icon.png"
],
"animation_speed": 30.0,
"particles": []
},
{
"name": "fire circle",
"source": "https://opengameart.org/content/fire-circle-fx",
"frames": [
"fire_circle_0000.png",
"fire_circle_0001.png",
"fire_circle_0002.png",
"fire_circle_0003.png",
"fire_circle_0004.png",
"fire_circle_0005.png",
"fire_circle_0006.png",
"fire_circle_0007.png",
"fire_circle_0008.png",
"fire_circle_0009.png",
"fire_circle_0010.png",
"fire_circle_0011.png",
"fire_circle_0012.png",
"fire_circle_0013.png",
"fire_circle_0014.png",
"fire_circle_0015.png",
"fire_circle_0016.png",
"fire_circle_0017.png",
"fire_circle_0018.png",
"fire_circle_0019.png",
"fire_circle_0020.png",
"fire_circle_0021.png",
"fire_circle_0022.png",
"fire_circle_0023.png",
"fire_circle_0024.png",
"fire_circle_0025.png",
"fire_circle_0026.png",
"fire_circle_0027.png",
"fire_circle_0028.png",
"fire_circle_0029.png",
"fire_circle_0030.png",
"fire_circle_0031.png",
"fire_circle_0032.png",
"fire_circle_0033.png",
"fire_circle_0034.png",
"fire_circle_0035.png",
"fire_circle_0036.png",
"fire_circle_0037.png",
"fire_circle_0038.png",
"fire_circle_0039.png",
"fire_circle_0040.png",
"fire_circle_0041.png",
"fire_circle_0042.png",
"fire_circle_0043.png",
"fire_circle_0044.png",
"fire_circle_0045.png",
"fire_circle_0046.png",
"fire_circle_0047.png",
"fire_circle_0048.png",
"fire_circle_0049.png",
"fire_circle_0050.png",
"fire_circle_0051.png",
"fire_circle_0052.png",
"fire_circle_0053.png",
"fire_circle_0054.png",
"fire_circle_0055.png",
"fire_circle_0056.png",
"fire_circle_0057.png",
"fire_circle_0058.png",
"fire_circle_0059.png",
"fire_circle_0060.png"
],
"animation_speed": 30.0,
"particles": []
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+11 -1
View File
@@ -18,9 +18,19 @@ impl Steam {
pub fn drop_item(&self) { pub fn drop_item(&self) {
// Placeholder for item drop functionality // Placeholder for item drop functionality
// The actual implementation would use the Steam API to trigger an item drop // The actual implementation would use the Steam API to trigger an item drop
println!("Steam item drop triggered (placeholder)");
// In a real implementation with the correct API version, we would use: // In a real implementation with the correct API version, we would use:
// self.client.get_inventory().trigger_item_drop(item_def_id); // self.client.get_inventory().trigger_item_drop(item_def_id);
} }
pub fn shutdown(&self) {
// The steamworks crate handles cleanup automatically when the client is dropped
// But we can add any additional cleanup logic here if needed
}
}
impl Drop for Steam {
fn drop(&mut self) {
self.shutdown();
}
} }
+153 -251
View File
@@ -1,22 +1,74 @@
use std::sync::Mutex; use integrations::steam::Steam;
use serde::{Serialize, Deserialize}; use parking_lot::Mutex;
use tauri::{Manager, AppHandle, CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent, WindowEvent}; use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf}; use std::sync::{Arc, LazyLock};
use std::env; use std::thread::spawn;
use std::{env, fs};
use tauri::{
AppHandle, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
SystemTrayMenuItem, WindowEvent,
};
use tokio::sync::broadcast;
use zip::ZipArchive;
use anyhow::{anyhow, Result};
mod platform;
mod integrations; mod integrations;
mod platform;
mod animator;
static STEAM: LazyLock<Mutex<Steam>> = LazyLock::new(|| Mutex::new(Steam::new()));
static CURSOR_CMD: LazyLock<broadcast::Sender<CursorCmd>> =
LazyLock::new(|| broadcast::channel(5).0);
static CURSOR_CHANGED: LazyLock<broadcast::Sender<usize>> =
LazyLock::new(|| broadcast::channel(5).0);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorCmd {
Select(usize),
Next,
Reapply,
Restore,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RandomRange {
Constant(f32),
Linear(f32, f32),
}
impl RandomRange {
pub fn next(&self) -> f32 {
match self {
RandomRange::Constant(_) => todo!(),
RandomRange::Linear(_, _) => todo!(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParticleConfig {
frames: Vec<String>,
animation_speed: f32,
gravity_x: RandomRange,
gravity_y: RandomRange,
rate: f32,
fade_in: f32,
fade_out: f32,
duration: RandomRange,
}
// Define cursor structure // Define cursor structure
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursor { pub struct Cursor {
name: String, name: String,
path: String, frames: Vec<String>,
animation_speed: f32,
particles: Vec<ParticleConfig>,
} }
// App state to track cursors // App state to track cursors
pub struct AppState { pub struct AppState {
cursors: Vec<Cursor>, cursors: Arc<Vec<Cursor>>,
current_index: usize, current_index: usize,
} }
@@ -26,142 +78,60 @@ pub struct AppStateWrapper(Mutex<AppState>);
// Command to get all cursors // Command to get all cursors
#[tauri::command] #[tauri::command]
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> { fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
let app_state = state.0.lock().unwrap(); let app_state = state.0.lock();
app_state.cursors.clone() (*app_state.cursors).clone()
} }
// Command to get current cursor index // Command to get current cursor index
#[tauri::command] #[tauri::command]
fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize { fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
let app_state = state.0.lock().unwrap(); let app_state = state.0.lock();
app_state.current_index app_state.current_index
} }
// Helper function to perform application cleanup (Steam shutdown and cursor restoration)
fn perform_app_cleanup() {
// Clean up Steam integration
{
let steam = STEAM.lock();
steam.shutdown();
}
// Clean up and restore default cursor before exit
match platform::restore_system_cursor() {
Ok(_) => {},
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
}
}
// Command to select a cursor // Command to select a cursor
#[tauri::command] #[tauri::command]
fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> { fn select_cursor(index: usize, app_handle: AppHandle) -> Result<(), String> {
println!("=== RESOURCE DEBUG [select_cursor] ==="); println!("=== RESOURCE DEBUG [select_cursor] ===");
let mut r = CURSOR_CHANGED.subscribe();
CURSOR_CMD.send(CursorCmd::Select(index)).ok();
let new_index = r.blocking_recv().unwrap();
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
let mut app_state = state.0.lock().unwrap(); Ok(())
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 // Command to go to the next cursor
#[tauri::command] #[tauri::command]
fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> { fn next_cursor(app_handle: AppHandle) -> Result<usize, String> {
println!("=== RESOURCE DEBUG [next_cursor] ==="); println!("=== RESOURCE DEBUG [next_cursor] ===");
let mut r = CURSOR_CHANGED.subscribe();
let mut app_state = state.0.lock().unwrap(); CURSOR_CMD.send(CursorCmd::Next).ok();
if !app_state.cursors.is_empty() { let new_index = r.blocking_recv().unwrap();
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len(); let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
Ok(new_index)
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 // Command to quit the application
#[tauri::command] #[tauri::command]
fn quit_app(app_handle: AppHandle) { fn quit_app(app_handle: AppHandle) {
// Clean up and restore default cursor before exit // Use the helper function to perform cleanup
match platform::restore_system_cursor() { perform_app_cleanup();
Ok(_) => println!("Default cursor restored"),
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
}
// Exit the application // Exit the application
std::thread::spawn(move || { std::thread::spawn(move || {
@@ -172,111 +142,64 @@ fn quit_app(app_handle: AppHandle) {
// Command to minimize the window to tray // Command to minimize the window to tray
#[tauri::command] #[tauri::command]
fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> { fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> {
println!("Minimizing to tray");
// Get the main window // Get the main window
let window = app_handle.get_window("main") let window = app_handle
.get_window("main")
.ok_or_else(|| "Failed to get main window".to_string())?; .ok_or_else(|| "Failed to get main window".to_string())?;
// Hide the window // Hide the window
window.hide().map_err(|e| format!("Failed to hide window: {}", e))?; window
.hide()
// The tray should already be visible as configured in tauri.conf.json .map_err(|e| format!("Failed to hide window: {}", e))?;
println!("Window hidden, application should now be accessible from the system tray");
Ok(()) Ok(())
} }
// Command to temporarily restore system cursor (for drag operations)
#[tauri::command]
fn temporarily_restore_cursor() -> Result<(), String> {
match platform::restore_system_cursor_temporarily() {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to restore system cursor: {}", e)),
}
}
// Command to re-apply custom cursor (after drag operations)
#[tauri::command]
fn reapply_custom_cursor() -> Result<(), String> {
CURSOR_CMD.send(CursorCmd::Reapply).ok();
Ok(())
}
// Command to manually restore default cursors
#[tauri::command]
fn restore_default_cursors() -> Result<(), String> {
println!("Manual restore default cursors command received");
CURSOR_CMD.send(CursorCmd::Restore).ok();
Ok(())
}
// Function to load cursor files from src/cursors directory // Function to load cursor files from src/cursors directory
fn load_cursor_files() -> Vec<Cursor> { fn load_cursor_files() -> Result<Vec<Cursor>> {
println!("=== RESOURCE DEBUG [load_cursor_files] ==="); println!("=== RESOURCE DEBUG [load_cursor_files] ===");
let mut cursors = Vec::new(); let mut zip = ZipArchive::new(
fs::OpenOptions::new().read(true).open(
// Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR) env::current_exe()?
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); .parent()
println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display()); .ok_or(anyhow!("failed to get parent"))?
.join("cursor_rush.dat"),
// Check multiple possible locations )?,
let possible_cursor_dirs = vec![ )?;
manifest_dir.join("../src/cursors"), let mut def = zip.by_name("cursor_def.json")?;
manifest_dir.join("src/cursors"), let mut bytes = Vec::new();
manifest_dir.join("cursors"), std::io::copy(&mut def, &mut bytes)?;
manifest_dir.clone(), // Check manifest dir itself Ok(serde_json::from_slice(&bytes)?)
];
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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
println!("Starting Cursor Rush application"); // Get LazyLock to create steam
let _ = STEAM.lock();
// Initialize platform-specific cursor handling // Initialize platform-specific cursor handling
if let Err(e) = platform::initialize() { if let Err(e) = platform::initialize() {
@@ -284,11 +207,17 @@ pub fn run() {
} }
// Load cursors from assets directory // Load cursors from assets directory
let cursors = load_cursor_files(); let cursors = Arc::new(load_cursor_files().expect("failed to load cursors"));
{
let cursors = cursors.clone();
spawn(move ||animator::animate_loop(cursors));
}
// Create app state // Create app state
let app_state = AppState { let app_state = AppState {
cursors, cursors: cursors,
current_index: 0, current_index: 0,
}; };
@@ -311,15 +240,12 @@ pub fn run() {
.system_tray(system_tray) .system_tray(system_tray)
.on_system_tray_event(|app, event| match event { .on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick { .. } => { SystemTrayEvent::LeftClick { .. } => {
println!("System tray received a left click");
if let Some(window) = app.get_window("main") { if let Some(window) = app.get_window("main") {
match window.is_visible() { match window.is_visible() {
Ok(visible) => { Ok(visible) => {
if visible { if visible {
println!("Window is already visible, focusing it");
let _ = window.set_focus(); let _ = window.set_focus();
} else { } else {
println!("Window is hidden, showing it");
if let Err(e) = window.show() { if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e); eprintln!("Failed to show window: {}", e);
} }
@@ -327,7 +253,7 @@ pub fn run() {
eprintln!("Failed to focus window: {}", e); eprintln!("Failed to focus window: {}", e);
} }
} }
}, }
Err(e) => { Err(e) => {
eprintln!("Failed to check window visibility: {}", e); eprintln!("Failed to check window visibility: {}", e);
// Try to show it anyway // Try to show it anyway
@@ -341,29 +267,12 @@ pub fn run() {
} }
} }
SystemTrayEvent::MenuItemClick { id, .. } => { SystemTrayEvent::MenuItemClick { id, .. } => {
println!("Menu item clicked: {}", id);
match id.as_str() { match id.as_str() {
"next_cursor" => { "next_cursor" => {
println!("Next cursor requested from tray"); println!("Next cursor requested from tray");
let state = app.state::<AppStateWrapper>(); CURSOR_CMD.send(CursorCmd::Next).ok();
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];
if Path::new(&cursor.path).exists() {
if let Err(e) = platform::set_system_cursor(&cursor.path) {
eprintln!("Error applying cursor from tray: {}", e);
} else {
// Emit an event to notify the frontend
let _ = app.emit_all("cursor_changed", app_state.current_index);
}
}
}
} }
"show_window" => { "show_window" => {
println!("Show window requested from tray");
if let Some(window) = app.get_window("main") { if let Some(window) = app.get_window("main") {
if let Err(e) = window.show() { if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e); eprintln!("Failed to show window: {}", e);
@@ -376,12 +285,9 @@ pub fn run() {
} }
} }
"quit" => { "quit" => {
println!("Quit requested from tray"); // Use the helper function to perform cleanup
// Clean up and restore default cursor before exit perform_app_cleanup();
match platform::restore_system_cursor() {
Ok(_) => println!("Default cursor restored"),
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
}
app.exit(0); app.exit(0);
} }
_ => {} _ => {}
@@ -397,13 +303,10 @@ pub fn run() {
// Get the window and hide it // Get the window and hide it
let window = event.window(); let window = event.window();
println!("Window close requested in backend handler, hiding window");
// Hide the window instead of closing it // Hide the window instead of closing it
if let Err(e) = window.hide() { if let Err(e) = window.hide() {
eprintln!("Failed to hide window: {}", e); eprintln!("Failed to hide window: {}", e);
} else {
println!("Window hidden successfully, app is now in system tray");
} }
} }
_ => {} _ => {}
@@ -415,15 +318,14 @@ pub fn run() {
select_cursor, select_cursor,
next_cursor, next_cursor,
quit_app, quit_app,
minimize_to_tray minimize_to_tray,
temporarily_restore_cursor,
reapply_custom_cursor,
restore_default_cursors
]) ])
.setup(|_app| { .setup(|_app| {
// Register for events or perform additional setup
println!("Tauri application setup complete");
// We're not setting up a window close handler here anymore // We're not setting up a window close handler here anymore
// The frontend will handle this based on the minimize-to-tray setting // The frontend will handle this based on the minimize-to-tray setting
Ok(()) Ok(())
}) })
// This matches exactly how it was before our changes // This matches exactly how it was before our changes
+3 -18
View File
@@ -1,20 +1,5 @@
// Linux platform implementation for cursor handling // Linux platform implementation for cursor handling
use super::Result; use super::stub_impl::create_platform_stubs;
// Initialize cursor handling // Generate stub implementations for Linux
pub fn init() -> Result<()> { create_platform_stubs!("Linux");
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
}
+3 -18
View File
@@ -1,20 +1,5 @@
// macOS platform implementation for cursor handling // macOS platform implementation for cursor handling
use super::Result; use super::stub_impl::create_platform_stubs;
// Initialize cursor handling // Generate stub implementations for macOS
pub fn init() -> Result<()> { create_platform_stubs!("macOS");
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
}
+23 -6
View File
@@ -3,6 +3,9 @@
// Common types for cursor handling // Common types for cursor handling
pub mod types; pub mod types;
// Shared stub implementations for unimplemented platforms
mod stub_impl;
// Windows implementation // Windows implementation
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod windows; mod windows;
@@ -15,12 +18,13 @@ mod macos;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
mod linux; mod linux;
// Error type for platform operations use anyhow::Result;
pub type Result<T> = std::result::Result<T, String>;
use image::RgbaImage;
// Platform-agnostic interface for cursor handling // Platform-agnostic interface for cursor handling
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub use self::windows::{set_cursor, restore_default_cursor, init}; pub use self::windows::{set_cursor, restore_default_cursor, restore_default_cursor_temporarily, init};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub use self::macos::{set_cursor, restore_default_cursor, init}; pub use self::macos::{set_cursor, restore_default_cursor, init};
@@ -41,9 +45,8 @@ pub fn initialize() -> Result<()> {
} }
// Cross-platform implementation for setting cursor // Cross-platform implementation for setting cursor
pub fn set_system_cursor(path: &str) -> Result<()> { pub fn set_system_cursor(image: &RgbaImage) -> Result<()> {
println!("Setting system cursor: {}", path); set_cursor(image)
set_cursor(path)
} }
// Cross-platform implementation for restoring default cursor // Cross-platform implementation for restoring default cursor
@@ -52,6 +55,20 @@ pub fn restore_system_cursor() -> Result<()> {
restore_default_cursor() restore_default_cursor()
} }
// Cross-platform implementation for temporarily restoring default cursor
pub fn restore_system_cursor_temporarily() -> Result<()> {
println!("Temporarily restoring system cursor");
#[cfg(target_os = "windows")]
{
restore_default_cursor_temporarily()
}
#[cfg(not(target_os = "windows"))]
{
// For non-Windows platforms, just use the regular restore for now
restore_default_cursor()
}
}
// Default implementations for non-supported platforms // Default implementations for non-supported platforms
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn set_cursor(_path: &str) -> Result<()> { pub fn set_cursor(_path: &str) -> Result<()> {
@@ -0,0 +1,30 @@
// Shared stub implementations for unimplemented platforms (macOS, Linux)
/// Creates a stub implementation for a platform that doesn't have cursor support yet
macro_rules! create_platform_stubs {
($platform_name:expr) => {
use super::Result;
use image::RgbaImage;
// Initialize cursor handling
pub fn init() -> Result<()> {
println!("{} cursor initialization not yet implemented", $platform_name);
Ok(()) // Return OK for now to not block platform-agnostic code
}
// Set cursor from image data
pub fn set_cursor(_image: &RgbaImage) -> Result<()> {
println!("{} cursor setting not yet implemented", $platform_name);
Ok(()) // Return OK for now to not block platform-agnostic code
}
// Restore default cursor
pub fn restore_default_cursor() -> Result<()> {
println!("{} cursor restoration not yet implemented", $platform_name);
Ok(()) // Return OK for now to not block platform-agnostic code
}
};
}
// Export the macro for use by platform modules
pub(crate) use create_platform_stubs;
-26
View File
@@ -1,26 +0,0 @@
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>,
}
+260 -156
View File
@@ -1,5 +1,6 @@
// Windows platform cursor management // Windows platform cursor management
use ::image::RgbaImage;
use std::ptr::null_mut; use std::ptr::null_mut;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use winapi::{ use winapi::{
@@ -15,33 +16,60 @@ use winapi::{
}, },
um::consoleapi::SetConsoleCtrlHandler, um::consoleapi::SetConsoleCtrlHandler,
shared::minwindef::{TRUE, FALSE, DWORD}, shared::minwindef::{TRUE, FALSE, DWORD},
shared::windef::{HDC, HBITMAP, HGDIOBJ},
}; };
use ::image::{RgbaImage, open as image_open};
use super::types::*;
use super::state::*; use super::state::*;
use super::types::*;
// Control handler function for console events (Ctrl+C, close, etc.) // Control handler function for console events (Ctrl+C, close, etc.)
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 { extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
println!("Control event received, attempting cleanup...");
cleanup(); // Call cleanup logic cleanup(); // Call cleanup logic
FALSE // Allow other handlers (like default exit) to run FALSE // Allow other handlers (like default exit) to run
} }
// Create a cursor from an RGBA image // Device context setup result
unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> { struct DeviceContexts {
let (width, height) = image.dimensions(); screen_dc: HDC,
let width_i32 = width as i32; color_dc: HDC,
let height_i32 = height as i32; mask_dc: HDC,
}
// Create device context and bitmap for the color data impl DeviceContexts {
unsafe fn cleanup(&self) {
DeleteDC(self.mask_dc);
DeleteDC(self.color_dc);
ReleaseDC(null_mut(), self.screen_dc);
}
}
// Bitmap creation result
struct CursorBitmaps {
color_bitmap: HBITMAP,
mask_bitmap: HBITMAP,
color_bits_ptr: *mut winapi::ctypes::c_void,
mask_bits_ptr: *mut winapi::ctypes::c_void,
old_color_bitmap: HGDIOBJ,
old_mask_bitmap: HGDIOBJ,
}
impl CursorBitmaps {
unsafe fn cleanup(&self, contexts: &DeviceContexts) {
SelectObject(contexts.color_dc, self.old_color_bitmap);
SelectObject(contexts.mask_dc, self.old_mask_bitmap);
DeleteObject(self.color_bitmap as _);
DeleteObject(self.mask_bitmap as _);
}
}
// Setup device contexts for cursor creation
unsafe fn setup_device_contexts() -> Option<DeviceContexts> {
let screen_dc = GetDC(null_mut()); let screen_dc = GetDC(null_mut());
if screen_dc.is_null() { if screen_dc.is_null() {
eprintln!("Failed to get screen DC"); eprintln!("Failed to get screen DC");
return None; return None;
} }
// Create a compatible DC for our bitmap
let color_dc = CreateCompatibleDC(screen_dc); let color_dc = CreateCompatibleDC(screen_dc);
if color_dc.is_null() { if color_dc.is_null() {
eprintln!("Failed to create compatible DC for color"); eprintln!("Failed to create compatible DC for color");
@@ -49,7 +77,6 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
return None; return None;
} }
// Create a compatible DC for our mask bitmap
let mask_dc = CreateCompatibleDC(screen_dc); let mask_dc = CreateCompatibleDC(screen_dc);
if mask_dc.is_null() { if mask_dc.is_null() {
eprintln!("Failed to create compatible DC for mask"); eprintln!("Failed to create compatible DC for mask");
@@ -58,11 +85,24 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
return None; return None;
} }
Some(DeviceContexts {
screen_dc,
color_dc,
mask_dc,
})
}
// Create bitmaps for cursor creation
unsafe fn create_cursor_bitmaps(
contexts: &DeviceContexts,
width: i32,
height: i32,
) -> Option<CursorBitmaps> {
// Set up BITMAPINFO structure for DIB // Set up BITMAPINFO structure for DIB
let mut bitmap_info: BITMAPINFO = std::mem::zeroed(); let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32; bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bitmap_info.bmiHeader.biWidth = width_i32; bitmap_info.bmiHeader.biWidth = width;
bitmap_info.bmiHeader.biHeight = -height_i32; // Negative for top-down DIB bitmap_info.bmiHeader.biHeight = -height; // Negative for top-down DIB
bitmap_info.bmiHeader.biPlanes = 1; bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA) bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA)
bitmap_info.bmiHeader.biCompression = BI_RGB; bitmap_info.bmiHeader.biCompression = BI_RGB;
@@ -71,7 +111,7 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
let mut color_bits_ptr: *mut winapi::ctypes::c_void = null_mut(); 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 mut ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
let color_bitmap = CreateDIBSection( let color_bitmap = CreateDIBSection(
color_dc, contexts.color_dc,
&bitmap_info, &bitmap_info,
DIB_RGB_COLORS, DIB_RGB_COLORS,
&mut ppv_bits_color, &mut ppv_bits_color,
@@ -82,9 +122,7 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
if color_bitmap.is_null() || color_bits_ptr.is_null() { if color_bitmap.is_null() || color_bits_ptr.is_null() {
eprintln!("Failed to create color DIB section"); eprintln!("Failed to create color DIB section");
DeleteDC(mask_dc); contexts.cleanup();
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None; return None;
} }
@@ -93,7 +131,7 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
let mut mask_bits_ptr: *mut winapi::ctypes::c_void = null_mut(); 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 mut ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
let mask_bitmap = CreateDIBSection( let mask_bitmap = CreateDIBSection(
mask_dc, contexts.mask_dc,
&bitmap_info, &bitmap_info,
DIB_RGB_COLORS, DIB_RGB_COLORS,
&mut ppv_bits, &mut ppv_bits,
@@ -105,32 +143,51 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
if mask_bitmap.is_null() || mask_bits_ptr.is_null() { if mask_bitmap.is_null() || mask_bits_ptr.is_null() {
eprintln!("Failed to create mask DIB section"); eprintln!("Failed to create mask DIB section");
DeleteObject(color_bitmap as _); DeleteObject(color_bitmap as _);
DeleteDC(mask_dc); contexts.cleanup();
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None; return None;
} }
// Select the bitmaps into the DCs // Select the bitmaps into the DCs
let old_color_bitmap = SelectObject(color_dc, color_bitmap as _); let old_color_bitmap = SelectObject(contexts.color_dc, color_bitmap as _);
let old_mask_bitmap = SelectObject(mask_dc, mask_bitmap as _); let old_mask_bitmap = SelectObject(contexts.mask_dc, mask_bitmap as _);
if old_color_bitmap.is_null() || old_mask_bitmap.is_null() { if old_color_bitmap.is_null() || old_mask_bitmap.is_null() {
eprintln!("Failed to select bitmaps into DCs"); eprintln!("Failed to select bitmaps into DCs");
DeleteObject(mask_bitmap as _); DeleteObject(mask_bitmap as _);
DeleteObject(color_bitmap as _); DeleteObject(color_bitmap as _);
DeleteDC(mask_dc); contexts.cleanup();
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None; return None;
} }
Some(CursorBitmaps {
color_bitmap,
mask_bitmap,
color_bits_ptr,
mask_bits_ptr,
old_color_bitmap,
old_mask_bitmap,
})
}
// Process image data and populate bitmaps
unsafe fn process_image_data(
image: &RgbaImage,
bitmaps: &CursorBitmaps,
width: u32,
height: u32,
) {
// Copy the image data to the color DIB, converting RGBA to BGRA // 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); let color_bits = std::slice::from_raw_parts_mut(
bitmaps.color_bits_ptr as *mut u8,
(width * height * 4) as usize,
);
// Calculate mask stride in bytes (1 bit per pixel, padded to DWORD) // Calculate mask stride in bytes (1 bit per pixel, padded to DWORD)
let mask_stride = ((width + 31) / 32) * 4; 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); let mask_bits = std::slice::from_raw_parts_mut(
bitmaps.mask_bits_ptr as *mut u8,
(mask_stride * height) as usize,
);
// Clear mask bits (all transparent initially) // Clear mask bits (all transparent initially)
for i in 0..(mask_stride * height) as usize { for i in 0..(mask_stride * height) as usize {
@@ -166,29 +223,25 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
} }
} }
} }
}
// Create cursor from prepared bitmaps
unsafe fn create_cursor_from_bitmaps(
bitmaps: &CursorBitmaps,
width: u32,
height: u32,
) -> Option<SyncHCURSOR> {
// Create ICONINFO structure // Create ICONINFO structure
let mut icon_info: ICONINFO = std::mem::zeroed(); let mut icon_info: ICONINFO = std::mem::zeroed();
icon_info.fIcon = FALSE; // This is a cursor, not an icon icon_info.fIcon = FALSE; // This is a cursor, not an icon
icon_info.xHotspot = width as DWORD / 2; // Center hotspot icon_info.xHotspot = width as DWORD / 2; // Center hotspot
icon_info.yHotspot = height as DWORD / 2; icon_info.yHotspot = height as DWORD / 2;
icon_info.hbmMask = mask_bitmap; icon_info.hbmMask = bitmaps.mask_bitmap;
icon_info.hbmColor = color_bitmap; icon_info.hbmColor = bitmaps.color_bitmap;
// Create the cursor // Create the cursor
let cursor = CreateIconIndirect(&mut icon_info as *mut _); 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() { if cursor.is_null() {
eprintln!("Failed to create cursor"); eprintln!("Failed to create cursor");
return None; return None;
@@ -198,22 +251,67 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
Some(SyncHCURSOR(cursor)) Some(SyncHCURSOR(cursor))
} }
// 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;
let contexts = setup_device_contexts()?;
let bitmaps = create_cursor_bitmaps(&contexts, width_i32, height_i32)?;
process_image_data(image, &bitmaps, width, height);
let cursor = create_cursor_from_bitmaps(&bitmaps, width, height);
bitmaps.cleanup(&contexts);
contexts.cleanup();
cursor
}
// Helper function to safely lock a mutex with error handling
fn safe_lock<'a, T>(mutex: &'a std::sync::Mutex<T>, operation_name: &str) -> Option<std::sync::MutexGuard<'a, T>> {
match mutex.lock() {
Ok(guard) => Some(guard),
Err(poisoned) => {
eprintln!("Mutex poisoned during {}: {}", operation_name, poisoned);
// For cleanup operations, we can still try to recover the data
Some(poisoned.into_inner())
}
}
}
// Cleanup function to restore cursors and clean up resources // Cleanup function to restore cursors and clean up resources
pub fn cleanup() { pub fn cleanup() {
println!("Running cursor cleanup..."); // Check if we have original cursors saved before attempting restoration
let _has_original_cursors = {
if let Some(guard) = safe_lock(&ORIGINAL_CURSORS, "checking original cursors") {
guard.is_some()
} else {
eprintln!("Failed to check original cursors during cleanup");
false
}
};
// Restore system cursors if they were hidden // Restore system cursors if they were hidden
// Use swap to ensure it runs only once // Use swap to ensure it runs only once
if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) { if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) {
println!("Restoring system cursors...");
unsafe { unsafe {
// Try multiple approaches to ensure cursors are restored // Try multiple approaches to ensure cursors are restored
// 1. First try to restore original cursors if we saved them // 1. First try to restore original cursors if we saved them
let original_cursors_opt = ORIGINAL_CURSORS.lock().unwrap().take(); let original_cursors_opt = {
if let Some(mut guard) = safe_lock(&ORIGINAL_CURSORS, "restoring original cursors") {
guard.take()
} else {
eprintln!("Failed to access original cursors for restoration");
None
}
};
if let Some(original_cursors) = original_cursors_opt { if let Some(original_cursors) = original_cursors_opt {
println!("Restoring {} saved original cursors", original_cursors.len());
for (cursor_id, cursor) in original_cursors { for (cursor_id, cursor) in original_cursors {
if !cursor.0.is_null() { if !cursor.0.is_null() {
// Make a copy of the original cursor // Make a copy of the original cursor
@@ -232,57 +330,68 @@ pub fn cleanup() {
} }
} }
} else { } else {
println!("No saved original cursors found");
// Try to restore each system cursor to its default
for &cursor_id in SYSTEM_CURSORS.iter() {
let default_cursor =
LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16));
if !default_cursor.is_null() {
let cursor_copy = CopyIcon(default_cursor);
if !cursor_copy.is_null() {
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to restore default cursor for ID: {}", cursor_id);
DestroyIcon(cursor_copy);
}
}
} }
// 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."); // Use the shared cursor restoration logic
restore_default_cursor_internal();
}
} }
// Destroy the custom cursor if it exists // Destroy the custom cursor if it exists
let custom_cursor_to_destroy = CUSTOM_CURSOR.lock().unwrap().take(); let custom_cursor_to_destroy = if let Some(mut guard) = safe_lock(&CUSTOM_CURSOR, "destroying custom cursor") {
guard.take()
} else {
eprintln!("Failed to access custom cursor for cleanup");
None
};
if let Some(cursor_wrapper) = custom_cursor_to_destroy { if let Some(cursor_wrapper) = custom_cursor_to_destroy {
unsafe { unsafe {
if !cursor_wrapper.0.is_null() { if !cursor_wrapper.0.is_null() {
DestroyIcon(cursor_wrapper.0); DestroyIcon(cursor_wrapper.0);
println!("Custom cursor destroyed during cleanup.");
} }
} }
} }
// Reset the last known cursor position // Reset the last known cursor position
*LAST_CURSOR_POS.lock().unwrap() = None; if let Some(mut guard) = safe_lock(&LAST_CURSOR_POS, "resetting cursor position") {
*guard = None;
} else {
eprintln!("Failed to reset cursor position during cleanup");
}
// Clear the current cursor image // Clear the current cursor image
*CURRENT_CURSOR_IMAGE.lock().unwrap() = None; if let Some(mut guard) = safe_lock(&CURRENT_CURSOR_IMAGE, "clearing cursor image") {
*guard = None;
} else {
eprintln!("Failed to clear cursor image during cleanup");
}
// Unregister control handler // Unregister control handler
if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) { if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) {
unsafe { unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 { if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 {
CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst); CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst);
println!("Control handler unregistered.");
} else { } else {
eprintln!("Failed to unregister control handler."); eprintln!("Failed to unregister control handler.");
} }
} }
} }
println!("Cursor cleanup complete.");
} }
// --- Public API Functions --- // --- Public API Functions ---
@@ -295,9 +404,11 @@ pub fn hide_system_cursor() -> bool {
unsafe { unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 { if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst); CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
println!("Control handler registered for clean exit.");
} else { } else {
eprintln!("Failed to register control handler: {}", std::io::Error::last_os_error()); eprintln!(
"Failed to register control handler: {}",
std::io::Error::last_os_error()
);
} }
} }
}); });
@@ -312,17 +423,26 @@ pub fn hide_system_cursor() -> bool {
unsafe { unsafe {
for &cursor_id in SYSTEM_CURSORS.iter() { for &cursor_id in SYSTEM_CURSORS.iter() {
// Get the original cursor // Get the original cursor
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16))); let original_cursor =
CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
if !original_cursor.is_null() { if !original_cursor.is_null() {
// Save the original cursor for restoration // Save the original cursor for restoration
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor))); original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
} else {
eprintln!("Failed to save original cursor for ID: {}", cursor_id);
} }
} }
} }
// Store original cursors for restoration // Store original cursors for restoration
if !original_cursors.is_empty() { if !original_cursors.is_empty() {
*ORIGINAL_CURSORS.lock().unwrap() = Some(original_cursors); if let Some(mut guard) = safe_lock(&ORIGINAL_CURSORS, "storing original cursors") {
*guard = Some(original_cursors);
} else {
eprintln!("Failed to store original cursors for restoration");
}
} else {
eprintln!("No original cursors were saved!");
} }
// Mark as hidden - this is now just a flag to indicate we've saved the original cursors // Mark as hidden - this is now just a flag to indicate we've saved the original cursors
@@ -332,51 +452,26 @@ pub fn hide_system_cursor() -> bool {
} }
// Update the cursor image using CursorInfo (with caching) // Update the cursor image using CursorInfo (with caching)
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool { pub fn update_cursor_info(image: &RgbaImage) -> 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 // Store the image in the global state for reference
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone()); if let Some(mut guard) = safe_lock(&CURRENT_CURSOR_IMAGE, "storing cursor image") {
*guard = Some(image.clone());
} else {
eprintln!("Failed to store cursor image in global state");
}
unsafe { unsafe {
// Create a new cursor first before destroying the old one // Create a new cursor first before destroying the old one
// This minimizes the time when no custom cursor is active // This minimizes the time when no custom cursor is active
let new_cursor = if let Some(ref cached_image) = cursor_info.image { let new_cursor = create_cursor_from_image(&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 { if let Some(new_cursor_wrapper) = new_cursor {
// Apply the new cursor to all system cursors first // Apply the new cursor to all system cursors first
let success = apply_cursor_to_all_system_cursors(new_cursor_wrapper.0); 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 // 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(mut cursor_guard) = safe_lock(&CUSTOM_CURSOR, "updating custom cursor") {
if let Some(old_cursor) = cursor_guard.take() { if let Some(old_cursor) = cursor_guard.take() {
// Only destroy the old cursor after the new one is applied // Only destroy the old cursor after the new one is applied
DestroyIcon(old_cursor.0); DestroyIcon(old_cursor.0);
@@ -384,14 +479,11 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
// Store the new cursor // Store the new cursor
*cursor_guard = Some(new_cursor_wrapper); *cursor_guard = Some(new_cursor_wrapper);
} else {
// Update the cached cursor in CursorInfo eprintln!("Failed to update custom cursor - mutex lock failed");
#[cfg(windows)] // Clean up the new cursor since we can't store it
{ DestroyIcon(new_cursor_wrapper.0);
// Convert SyncHCURSOR to WinCursorWrapper for storage in cursor_info return false;
cursor_info.win_cursor = Some(crate::platform::types::WinCursorWrapper {
handle: new_cursor_wrapper.0
});
} }
if !success { if !success {
@@ -403,27 +495,41 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
return true; return true;
} else { } else {
eprintln!("Failed to create cursor from image"); 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; return false;
} }
} }
} }
// Helper function to apply a cursor to a specific system cursor ID
unsafe fn apply_cursor_to_system_cursor(cursor: winapi::shared::windef::HCURSOR, cursor_id: DWORD, log_errors: bool) -> bool {
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor);
if cursor_copy.is_null() {
if log_errors {
let error = std::io::Error::last_os_error();
eprintln!(
"Failed to copy cursor for ID: {} - Error: {}",
cursor_id, error
);
}
return false;
}
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
if log_errors {
let error = std::io::Error::last_os_error();
eprintln!(
"Failed to set cursor for ID: {} - Error: {}",
cursor_id, error
);
}
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
return false;
}
true
}
// Apply a cursor to all system cursors // Apply a cursor to all system cursors
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool { unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
@@ -446,45 +552,21 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
32649, // IDC_HAND (links) 32649, // IDC_HAND (links)
]; ];
// Apply to priority cursors first // Apply to priority cursors first (with error logging)
for &cursor_id in priority_cursors.iter() { for &cursor_id in priority_cursors.iter() {
// Make a copy of the cursor if !apply_cursor_to_system_cursor(cursor, cursor_id, true) {
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; success = false;
} }
} }
// Now apply to the remaining cursors // Now apply to the remaining cursors (without error logging to reduce spam)
for &cursor_id in SYSTEM_CURSORS.iter() { for &cursor_id in SYSTEM_CURSORS.iter() {
// Skip the priority cursors we already processed // Skip the priority cursors we already processed
if priority_cursors.contains(&cursor_id) { if priority_cursors.contains(&cursor_id) {
continue; continue;
} }
// Make a copy of the cursor if !apply_cursor_to_system_cursor(cursor, cursor_id, false) {
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; success = false;
} }
} }
@@ -497,7 +579,29 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
success success
} }
// Restore the system cursor // Helper function for common cursor restoration logic
unsafe fn restore_default_cursor_internal() {
// Force Windows to reload default cursor settings
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
// Also try the A version for maximum compatibility
super::state::reset_system_cursors();
// Reset to default arrow cursor
let arrow_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
if !arrow_cursor.is_null() {
SetCursor(arrow_cursor);
}
}
// Restore the system cursor temporarily (without affecting cleanup state)
pub fn restore_cursor_temporarily() {
unsafe {
restore_default_cursor_internal();
}
}
// Restore the system cursor permanently (full cleanup)
pub fn restore_cursor() { pub fn restore_cursor() {
cleanup(); cleanup();
} }
+13 -18
View File
@@ -4,27 +4,16 @@ pub mod types;
mod cursor; mod cursor;
mod state; mod state;
use std::path::PathBuf; use image::RgbaImage;
use crate::platform::Result;
use anyhow::{anyhow, Result};
// Functions expected by the platform-agnostic interface // Functions expected by the platform-agnostic interface
pub fn set_cursor(path: &str) -> Result<()> { pub fn set_cursor(image: &RgbaImage) -> 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 // Use the existing update_cursor_info function to handle loading and applying the cursor
if !cursor::update_cursor_info(&mut cursor_info) { if !cursor::update_cursor_info(image) {
return Err(format!("Failed to update cursor from {}", path)); return Err(anyhow!("Failed to update cursor"));
} }
Ok(()) Ok(())
@@ -36,10 +25,16 @@ pub fn restore_default_cursor() -> Result<()> {
Ok(()) Ok(())
} }
pub fn restore_default_cursor_temporarily() -> Result<()> {
println!("Windows platform: temporarily restoring default cursor");
cursor::restore_cursor_temporarily();
Ok(())
}
pub fn init() -> Result<()> { pub fn init() -> Result<()> {
println!("Windows platform: initializing cursor system"); println!("Windows platform: initializing cursor system");
if !cursor::hide_system_cursor() { if !cursor::hide_system_cursor() {
return Err("Failed to initialize cursor system".to_string()); return Err(anyhow!("Failed to initialize cursor system"));
} }
Ok(()) Ok(())
} }
@@ -9,9 +9,24 @@ use winapi::{
// --- Thread-Safe Handle Wrappers --- // --- Thread-Safe Handle Wrappers ---
// These wrappers make raw Windows handles safe to share between threads // These wrappers make raw Windows handles safe to share between threads
// Wrapper for HCURSOR (cursor handle) // Wrapper for HCURSOR (cursor handle) that implements Send + Sync
//
// SAFETY: HCURSOR is a Windows handle that represents a cursor resource.
// Windows cursor handles are:
// 1. Reference-counted by the Windows kernel
// 2. Safe to share between threads (multiple threads can hold the same handle)
// 3. Safe to send between threads (the handle value itself is just a pointer-sized integer)
// 4. Immutable once created (cursor resources don't change after creation)
//
// The Windows API documentation confirms that cursor handles can be used from any thread,
// and the kernel manages the underlying resource lifetime through reference counting.
// Therefore, it's safe to implement Send + Sync for this wrapper.
//
// NOTE: The caller is responsible for ensuring proper cleanup (calling DestroyIcon)
// to avoid resource leaks, but this doesn't affect thread safety.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct SyncHCURSOR(pub HCURSOR); pub struct SyncHCURSOR(pub HCURSOR);
unsafe impl Send for SyncHCURSOR {} unsafe impl Send for SyncHCURSOR {}
unsafe impl Sync for SyncHCURSOR {} unsafe impl Sync for SyncHCURSOR {}
+35 -16
View File
@@ -26,34 +26,56 @@ function App() {
// Function to minimize app to tray // Function to minimize app to tray
const minimizeToTray = useCallback(async () => { const minimizeToTray = useCallback(async () => {
try { try {
console.log('Minimizing app to tray');
// First hide the window to ensure it disappears immediately // First hide the window to ensure it disappears immediately
await appWindow.hide(); await appWindow.hide();
console.log('Window hidden, now ensuring tray is visible');
// Then call the backend to ensure the tray is visible // Then call the backend to ensure the tray is visible
await invoke("minimize_to_tray"); await invoke("minimize_to_tray");
console.log('minimize_to_tray command completed successfully');
} catch (error) { } catch (error) {
console.error("Failed to minimize to tray:", error); console.error("Failed to minimize to tray:", error);
// Fallback to window API if the invoke fails // Fallback to window API if the invoke fails
try { try {
console.log('Trying fallback hide');
await appWindow.hide(); await appWindow.hide();
console.log('Fallback hide succeeded');
} catch (fallbackError) { } catch (fallbackError) {
console.error("Fallback hide also failed:", fallbackError); console.error("Fallback hide also failed:", fallbackError);
} }
} }
}, []); }, []);
// Function to handle window focus/show events to re-apply cursor
useEffect(() => {
const setupFocusHandler = async () => {
const unlistenFocus = await appWindow.onFocusChanged(async ({ payload: focused }) => {
if (focused) {
try {
await invoke("reapply_custom_cursor");
} catch (error) {
console.error('Failed to re-apply cursor on focus:', error);
}
}
});
return unlistenFocus;
};
let unlistenFocusFn;
setupFocusHandler().then(unlisten => {
unlistenFocusFn = unlisten;
}).catch(err => console.error('Failed to set up focus handler:', err));
return () => {
if (unlistenFocusFn) {
unlistenFocusFn();
}
};
}, []);
// Load cursors on mount and set up event listeners // Load cursors on mount and set up event listeners
useEffect(() => { useEffect(() => {
const loadCursors = async () => { const loadCursors = async () => {
try { try {
// Get cursors from the Tauri backend // Get cursors from the Tauri backend
const loadedCursors = await invoke("get_cursors"); const loadedCursors = await invoke("get_cursors");
console.log("Loaded cursors:", loadedCursors);
setCursors(loadedCursors || []); setCursors(loadedCursors || []);
// Get current cursor index // Get current cursor index
@@ -80,23 +102,24 @@ function App() {
useEffect(() => { useEffect(() => {
const setupCloseHandler = async () => { const setupCloseHandler = async () => {
const unlisten = await appWindow.onCloseRequested(async (event) => { const unlisten = await appWindow.onCloseRequested(async (event) => {
console.log('Window close requested');
if (settings.minimizeToTrayOnClose) { if (settings.minimizeToTrayOnClose) {
console.log('Minimize to tray setting is enabled, preventing close and hiding instead');
// Prevent the window from closing // Prevent the window from closing
event.preventDefault(); event.preventDefault();
// Temporarily restore cursor before hiding to tray
try {
await invoke("temporarily_restore_cursor");
} catch (error) {
console.error('Failed to restore cursor before hiding:', error);
}
// Hide the window instead // Hide the window instead
try { try {
console.log('Calling minimizeToTray function');
await minimizeToTray(); await minimizeToTray();
console.log('Window should now be hidden and app should be in system tray');
} catch (error) { } catch (error) {
console.error('Failed to minimize to tray on close:', error); console.error('Failed to minimize to tray on close:', error);
} }
} else { } else {
console.log('Minimize to tray setting is disabled, allowing window to close');
// Let the window close normally, but we need to call quit_app to ensure proper cleanup // Let the window close normally, but we need to call quit_app to ensure proper cleanup
try { try {
await invoke("quit_app"); await invoke("quit_app");
@@ -112,7 +135,6 @@ function App() {
let unlistenFn; let unlistenFn;
setupCloseHandler().then(unlisten => { setupCloseHandler().then(unlisten => {
unlistenFn = unlisten; unlistenFn = unlisten;
console.log('Close handler set up successfully');
}).catch(err => console.error('Failed to set up close handler:', err)); }).catch(err => console.error('Failed to set up close handler:', err));
// Clean up event listener on unmount // Clean up event listener on unmount
@@ -126,7 +148,6 @@ function App() {
// Functions to handle cursor operations using Tauri backend // Functions to handle cursor operations using Tauri backend
const selectCursor = async (index) => { const selectCursor = async (index) => {
try { try {
console.log(`Selecting cursor at index ${index}`);
await invoke("select_cursor", { index }); await invoke("select_cursor", { index });
setCurrentCursorIndex(index); setCurrentCursorIndex(index);
} catch (error) { } catch (error) {
@@ -138,7 +159,6 @@ function App() {
if (cursors.length === 0) return; if (cursors.length === 0) return;
try { try {
console.log('Switching to next cursor');
const newIndex = await invoke("next_cursor"); const newIndex = await invoke("next_cursor");
setCurrentCursorIndex(newIndex); setCurrentCursorIndex(newIndex);
} catch (error) { } catch (error) {
@@ -152,7 +172,6 @@ function App() {
// Function to exit app // Function to exit app
const quitApp = async () => { const quitApp = async () => {
try { try {
console.log('Quitting app');
await invoke("quit_app"); await invoke("quit_app");
} catch (error) { } catch (error) {
console.error("Failed to quit app:", error); console.error("Failed to quit app:", error);
+12 -1
View File
@@ -6,7 +6,8 @@
position: relative; position: relative;
z-index: 10; z-index: 10;
background-color: transparent; /* Reverted to original */ background-color: transparent; /* Reverted to original */
/* cursor: move; */ /* Removed, should be handled by drag region or default */ -webkit-app-region: drag; /* Ensure the header is draggable */
/* cursor: move; */ /* Let the system handle cursor during drag operations */
} }
.tabs-container { .tabs-container {
@@ -64,6 +65,16 @@
color: rgba(205, 214, 244, 0.8); color: rgba(205, 214, 244, 0.8);
} }
/* Dedicated drag area */
.drag-area {
flex: 1; /* Take up remaining space */
height: 100%;
min-width: 50px; /* Ensure there's always some draggable space */
-webkit-app-region: drag; /* Explicitly set as draggable */
/* cursor: move; */ /* Let the system handle cursor during drag operations */
background-color: transparent;
}
/* Window control buttons */ /* Window control buttons */
.window-controls { .window-controls {
-webkit-app-region: no-drag; /* Override drag region for buttons */ -webkit-app-region: no-drag; /* Override drag region for buttons */
+28 -1
View File
@@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import './Header.css'; import './Header.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { appWindow } from '@tauri-apps/api/window';
import { invoke } from '@tauri-apps/api/tauri';
import { import {
faGripVertical, faGripVertical,
faBoxOpen, faBoxOpen,
@@ -36,8 +38,29 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }
} }
}; };
// Handle window dragging programmatically as a fallback
const handleMouseDown = async (e) => {
// Only start dragging on left mouse button and if not clicking on interactive elements
if (e.button === 0 && !e.target.closest('.tab, .window-controls')) {
try {
// Temporarily restore system cursor for dragging
await invoke('temporarily_restore_cursor');
await appWindow.startDragging();
} catch (error) {
console.error('Failed to start window dragging:', error);
} finally {
// Re-apply custom cursor after dragging
try {
await invoke('reapply_custom_cursor');
} catch (error) {
console.error('Failed to re-apply custom cursor:', error);
}
}
}
};
return ( return (
<header className="app-header" data-tauri-drag-region> <header className="app-header" data-tauri-drag-region onMouseDown={handleMouseDown}>
<div className="tabs-container"> <div className="tabs-container">
{navItems.map((item) => ( {navItems.map((item) => (
<div <div
@@ -49,6 +72,10 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }
<FontAwesomeIcon icon={item.icon} /> <FontAwesomeIcon icon={item.icon} />
</div> </div>
))} ))}
{/* Dedicated drag area */}
<div className="drag-area" title="Drag to move window"></div>
<div className="window-controls"> <div className="window-controls">
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray"> <button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
_ _
@@ -29,7 +29,9 @@
.setting-item { .setting-item {
padding: 8px 0; padding: 8px 0;
display: flex; display: flex;
align-items: center; flex-direction: column;
align-items: flex-start;
gap: 8px;
} }
.setting-item label { .setting-item label {
@@ -73,3 +75,31 @@
font-size: 14px; font-size: 14px;
opacity: 0.8; opacity: 0.8;
} }
.restore-button {
background-color: var(--accent-color, #89b4fa);
color: var(--bg-color, #1e1e2e);
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.restore-button:hover {
background-color: var(--accent-hover, #74c7ec);
transform: translateY(-1px);
}
.restore-button:active {
transform: translateY(0);
}
.setting-description {
font-size: 12px;
opacity: 0.7;
margin: 0;
color: var(--text-color, #cdd6f4);
}
@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { invoke } from "@tauri-apps/api/tauri";
import './Settings.css'; import './Settings.css';
function Settings({ settings, setSettings }) { function Settings({ settings, setSettings }) {
@@ -10,6 +11,19 @@ function Settings({ settings, setSettings }) {
}); });
}; };
// Handler for restoring default cursors
const handleRestoreDefaultCursors = async () => {
try {
console.log('Manually restoring default cursors');
await invoke("restore_default_cursors");
console.log('Default cursors restored successfully');
alert('Default cursors restored successfully!');
} catch (error) {
console.error('Failed to restore default cursors:', error);
alert('Failed to restore default cursors: ' + error);
}
};
return ( return (
<div className="settings-container"> <div className="settings-container">
<h2 className="content-title">Settings</h2> <h2 className="content-title">Settings</h2>
@@ -50,6 +64,17 @@ function Settings({ settings, setSettings }) {
Auto-apply cursor on selection Auto-apply cursor on selection
</label> </label>
</div> </div>
<div className="setting-item">
<button
onClick={handleRestoreDefaultCursors}
className="restore-button"
>
Restore Default Cursors
</button>
<p className="setting-description">
Manually restore the system's default cursors if they're not working properly.
</p>
</div>
</div> </div>
<div className="settings-group"> <div className="settings-group">
-2
View File
@@ -3,7 +3,5 @@ import ReactDOM from "react-dom/client";
import App from "./App"; import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App /> <App />
</React.StrictMode>,
); );