16 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
Joby f60f058cf9 Merge frontend-integration into master, making it the new standard 2025-05-17 21:11:47 -05:00
Joby 9f733ed39b Integrate Tauri frontend with categorization system and Steam integration 2025-05-17 21:07:31 -05:00
Joby 0133b61944 updated readme with run directions 2025-05-17 17:25:50 -05:00
95 changed files with 1323 additions and 582 deletions
+1
View File
@@ -1,3 +1,4 @@
/target
**/*.rs.bk
Cargo.lock
CODE_CLEANUP_TODO.md
+3
View File
@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}
+4
View File
@@ -0,0 +1,4 @@
[workspace]
members = ["frontend/src-tauri"]
resolver = "3"
+91 -60
View File
@@ -11,101 +11,106 @@ A cross-platform application for customizing your cursor with gameified logic.
- System tray integration with context menu
- Global hotkey support
- Steam integration for item drops
- Tauri-based frontend with React
## Project Structure
```
src/
├── app/ # Application core logic
│ ├── mod.rs # Main app module exports
│ ├── state.rs # Application state management
│ ├── messages.rs # Message definitions
│ └── cursor.rs # Cursor-related functionality
├── ui/ # User interface components
│ ├── mod.rs # UI module exports
│ ├── view.rs # View rendering logic
│ └── tray.rs # System tray functionality
├── platform/ # Platform-specific implementations
│ ├── mod.rs # Platform module exports
│ ├── windows/ # Windows-specific code
│ │ ├── mod.rs # Windows module exports
│ │ ├── types.rs # Windows type definitions
│ │ ├── state.rs # Windows state management
│ │ └── cursor.rs # Windows cursor handling
│ ├── macos/ # macOS-specific code (in development)
│ │ ├── mod.rs # macOS module exports
│ │ ├── state.rs # macOS state management
│ │ └── cursor.rs # macOS cursor handling
│ ├── linux/ # Linux-specific code (in development)
│ │ ├── mod.rs # Linux module exports
│ │ ├── state.rs # Linux state management
│ │ └── cursor.rs # Linux cursor handling
│ └── unsupported/ # Fallback for unsupported platforms
│ ├── mod.rs # Unsupported module exports
│ ├── state.rs # Unsupported state management
│ └── cursor.rs # Unsupported cursor handling
├── integrations/ # External integrations
│ ├── mod.rs # Integrations module exports
│ └── steam.rs # Steam integration
├── utils/ # Utility functions and helpers
│ ├── mod.rs # Utils module exports
│ ├── config.rs # Configuration management
│ └── logging.rs # Logging utilities
└── main.rs # Application entry point
frontend/
├── src/ # React frontend code
│ ├── App.jsx # Main React component
│ ├── components/ # UI components
│ └── cursors/ # Cursor image files
├── src-tauri/ # Rust backend code
│ ├── src/ # Rust source code
│ │ ├── lib.rs # Main library code
│ │ ├── main.rs # Application entry point
│ │ ├── integrations/ # External integrations
│ │ │ ├── mod.rs # Integrations module exports
│ │ │ └── steam.rs # Steam integration
│ │ └── platform/ # Platform-specific code
│ │ ├── mod.rs # Platform module exports
│ │ ├── types.rs # Common type definitions
│ │ ├── windows/ # Windows-specific code
│ │ ├── macos/ # macOS-specific code
│ │ └── linux/ # Linux-specific code
│ └── Cargo.toml # Rust dependencies
└── package.json # npm dependencies
```
## Building
### Prerequisites
- Rust 1.70.0 or later
- Rust 1.56.0 or later (tested with 1.86.0)
- Cargo
- Node.js 16.0.0 or later (tested with 20.11.0)
- npm 7.0.0 or later (tested with 10.2.4)
- Platform-specific dependencies:
- Windows: None (fully supported)
- Windows: Visual Studio Build Tools (fully supported)
- macOS: Development in progress
- Linux: Development in progress
### Dependencies
The application uses the following major dependencies:
- iced 0.13.1 - GUI framework
- tray-icon 0.20.1 - System tray functionality
- image 0.24.7 - Image processing
- global-hotkey 0.4.2 - Global hotkey registration
- Tauri 1.8.3 - Cross-platform framework
- React 19.1.0 - Frontend framework
- image 0.24 - Image processing
- winapi 0.3.9 - Windows API bindings
- once_cell 1.19.0 - Lazy initialization
- steamworks 0.11.0 - Steam integration
- parking_lot 0.12.3 - High-performance synchronization
### Build Commands
```bash
# Debug build
cargo build
# Navigate to the frontend directory
cd frontend
# Release build
cargo build --release
# Install npm dependencies
npm install
# Run in development mode (starts both frontend and backend)
npm run tauri dev
# Build for production
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
1. Place your cursor PNG files in the `assets` directory (PNG format only)
2. Run the application using `cargo run` or the compiled executable
3. The application will load all PNG files from the assets directory
4. Select a cursor from the list in the UI
5. The application will replace your system cursor with the selected image
6. When closing the window, the application minimizes to the system tray
7. To fully exit, use the system tray menu or press the exit hotkey
1. Make sure you have Node.js and npm installed
2. Navigate to the frontend directory: `cd frontend`
3. Install dependencies if you haven't already: `npm install`
4. Run the application in development mode: `npm run tauri dev`
5. The application will start with a React frontend and Rust backend
6. Use the UI to browse and select cursors
7. The application will replace your system cursor with the selected image
8. To exit, use the "Quit" button in the application or from the system tray
### Custom Cursors
The application comes with two sample cursors in the `assets` directory:
The application comes with sample cursors:
- `default.png` - A simple default cursor
- `icon.png` - An icon cursor
- `red.png` - A red-colored cursor
To add your own cursors:
1. Create PNG images with transparent backgrounds
2. Place them in the `assets` directory
1. Create PNG or CUR images with transparent backgrounds
2. Place them in the `frontend/src/cursors` directory
3. Restart the application to load the new cursors
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
- Ctrl+Shift+F12: Exit the application (restores original system cursor)
@@ -126,17 +131,42 @@ To add your own cursors:
- The application uses event-driven cursor updates instead of polling when possible
- 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:
- 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
### 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
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
- 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
@@ -147,6 +177,7 @@ Contributions to Cursor Rush are welcome! Here are some ways you can contribute:
- Submit pull requests with bug fixes or new features
- Help with documentation
- Contribute to macOS and Linux platform support
- Improve the Tauri integration
## License
+10 -2
View File
@@ -6,21 +6,29 @@ 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 = [] }
zip = { version = "4.0.0", features = ["zstd"] }
[dependencies]
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open"] }
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
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"
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]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
+60 -2
View File
@@ -1,3 +1,61 @@
fn main() {
tauri_build::build()
use std::{env, fs::{self, read_dir}, io::Write, path::PathBuf};
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

@@ -0,0 +1,3 @@
// External integrations module
pub mod steam;
@@ -0,0 +1,36 @@
// Steam integration
use steamworks::{AppId, Client, SingleClient};
#[allow(dead_code)]
pub struct Steam {
client: Client,
single: SingleClient,
}
impl Steam {
pub fn new() -> Self {
let (client, single) = Client::init_app(AppId(1430270)).unwrap();
Self { client, single }
}
#[allow(dead_code)]
pub fn drop_item(&self) {
// Placeholder for item drop functionality
// The actual implementation would use the Steam API to trigger an item drop
// In a real implementation with the correct API version, we would use:
// 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();
}
}
+250 -210
View File
@@ -1,21 +1,74 @@
use std::sync::Mutex;
use serde::{Serialize, Deserialize};
use tauri::{Manager, AppHandle};
use std::path::{Path, PathBuf};
use std::env;
use integrations::steam::Steam;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, LazyLock};
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 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
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursor {
name: String,
path: String,
frames: Vec<String>,
animation_speed: f32,
particles: Vec<ParticleConfig>,
}
// App state to track cursors
pub struct AppState {
cursors: Vec<Cursor>,
cursors: Arc<Vec<Cursor>>,
current_index: usize,
}
@@ -25,142 +78,60 @@ 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()
let app_state = state.0.lock();
(*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();
let app_state = state.0.lock();
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
#[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] ===");
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();
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())
}
Ok(())
}
// Command to go to the next cursor
#[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] ===");
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())
}
let mut r = CURSOR_CHANGED.subscribe();
CURSOR_CMD.send(CursorCmd::Next).ok();
let new_index = r.blocking_recv().unwrap();
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
Ok(new_index)
}
// 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),
}
// Use the helper function to perform cleanup
perform_app_cleanup();
// Exit the application
std::thread::spawn(move || {
@@ -168,96 +139,67 @@ fn quit_app(app_handle: AppHandle) {
});
}
// Command to minimize the window to tray
#[tauri::command]
fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> {
// Get the main window
let window = app_handle
.get_window("main")
.ok_or_else(|| "Failed to get main window".to_string())?;
// Hide the window
window
.hide()
.map_err(|e| format!("Failed to hide window: {}", e))?;
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
fn load_cursor_files() -> Vec<Cursor> {
fn load_cursor_files() -> Result<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
let mut zip = ZipArchive::new(
fs::OpenOptions::new().read(true).open(
env::current_exe()?
.parent()
.ok_or(anyhow!("failed to get parent"))?
.join("cursor_rush.dat"),
)?,
)?;
let mut def = zip.by_name("cursor_def.json")?;
let mut bytes = Vec::new();
std::io::copy(&mut def, &mut bytes)?;
Ok(serde_json::from_slice(&bytes)?)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
println!("Starting Cursor Rush application");
// Get LazyLock to create steam
let _ = STEAM.lock();
// Initialize platform-specific cursor handling
if let Err(e) = platform::initialize() {
@@ -265,27 +207,125 @@ pub fn run() {
}
// 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
let app_state = AppState {
cursors,
cursors: cursors,
current_index: 0,
};
// Create system tray menu
let next_cursor_item = CustomMenuItem::new("next_cursor".to_string(), "Next cursor");
let show_window_item = CustomMenuItem::new("show_window".to_string(), "Show window");
let quit_item = CustomMenuItem::new("quit".to_string(), "Quit");
let tray_menu = SystemTrayMenu::new()
.add_item(next_cursor_item)
.add_item(show_window_item)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(quit_item);
let system_tray = SystemTray::new().with_menu(tray_menu);
// This is one of the simplest ways to initialize Tauri
tauri::Builder::default()
.manage(AppStateWrapper(Mutex::new(app_state)))
.system_tray(system_tray)
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick { .. } => {
if let Some(window) = app.get_window("main") {
match window.is_visible() {
Ok(visible) => {
if visible {
let _ = window.set_focus();
} else {
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
if let Err(e) = window.set_focus() {
eprintln!("Failed to focus window: {}", e);
}
}
}
Err(e) => {
eprintln!("Failed to check window visibility: {}", e);
// Try to show it anyway
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
}
}
} else {
eprintln!("Main window not found");
}
}
SystemTrayEvent::MenuItemClick { id, .. } => {
match id.as_str() {
"next_cursor" => {
println!("Next cursor requested from tray");
CURSOR_CMD.send(CursorCmd::Next).ok();
}
"show_window" => {
if let Some(window) = app.get_window("main") {
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
if let Err(e) = window.set_focus() {
eprintln!("Failed to focus window: {}", e);
}
} else {
eprintln!("Main window not found");
}
}
"quit" => {
// Use the helper function to perform cleanup
perform_app_cleanup();
app.exit(0);
}
_ => {}
}
}
_ => {}
})
.on_window_event(move |event| {
match event.event() {
WindowEvent::CloseRequested { api, .. } => {
// Prevent the default close action
api.prevent_close();
// Get the window and hide it
let window = event.window();
// Hide the window instead of closing it
if let Err(e) = window.hide() {
eprintln!("Failed to hide window: {}", e);
}
}
_ => {}
}
})
.invoke_handler(tauri::generate_handler![
get_cursors,
get_current_cursor_index,
select_cursor,
next_cursor,
quit_app
quit_app,
minimize_to_tray,
temporarily_restore_cursor,
reapply_custom_cursor,
restore_default_cursors
])
.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
// The frontend will handle this based on the minimize-to-tray setting
Ok(())
})
// This matches exactly how it was before our changes
+3 -18
View File
@@ -1,20 +1,5 @@
// Linux platform implementation for cursor handling
use super::Result;
use super::stub_impl::create_platform_stubs;
// 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
}
// Generate stub implementations for Linux
create_platform_stubs!("Linux");
+3 -18
View File
@@ -1,20 +1,5 @@
// macOS platform implementation for cursor handling
use super::Result;
use super::stub_impl::create_platform_stubs;
// 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
}
// Generate stub implementations for macOS
create_platform_stubs!("macOS");
+23 -6
View File
@@ -3,6 +3,9 @@
// Common types for cursor handling
pub mod types;
// Shared stub implementations for unimplemented platforms
mod stub_impl;
// Windows implementation
#[cfg(target_os = "windows")]
mod windows;
@@ -15,12 +18,13 @@ mod macos;
#[cfg(target_os = "linux")]
mod linux;
// Error type for platform operations
pub type Result<T> = std::result::Result<T, String>;
use anyhow::Result;
use image::RgbaImage;
// Platform-agnostic interface for cursor handling
#[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")]
pub use self::macos::{set_cursor, restore_default_cursor, init};
@@ -41,9 +45,8 @@ pub fn initialize() -> Result<()> {
}
// Cross-platform implementation for setting cursor
pub fn set_system_cursor(path: &str) -> Result<()> {
println!("Setting system cursor: {}", path);
set_cursor(path)
pub fn set_system_cursor(image: &RgbaImage) -> Result<()> {
set_cursor(image)
}
// Cross-platform implementation for restoring default cursor
@@ -52,6 +55,20 @@ pub fn restore_system_cursor() -> Result<()> {
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
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
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>,
}
+267 -163
View File
@@ -1,5 +1,6 @@
// Windows platform cursor management
use ::image::RgbaImage;
use std::ptr::null_mut;
use std::sync::atomic::Ordering;
use winapi::{
@@ -15,33 +16,60 @@ use winapi::{
},
um::consoleapi::SetConsoleCtrlHandler,
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::types::*;
// 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;
// Device context setup result
struct DeviceContexts {
screen_dc: HDC,
color_dc: HDC,
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());
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");
@@ -49,7 +77,6 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
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");
@@ -58,11 +85,24 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
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
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.biWidth = width;
bitmap_info.bmiHeader.biHeight = -height; // 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;
@@ -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 ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
let color_bitmap = CreateDIBSection(
color_dc,
contexts.color_dc,
&bitmap_info,
DIB_RGB_COLORS,
&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() {
eprintln!("Failed to create color DIB section");
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
contexts.cleanup();
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 ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
let mask_bitmap = CreateDIBSection(
mask_dc,
contexts.mask_dc,
&bitmap_info,
DIB_RGB_COLORS,
&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() {
eprintln!("Failed to create mask DIB section");
DeleteObject(color_bitmap as _);
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
contexts.cleanup();
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 _);
let old_color_bitmap = SelectObject(contexts.color_dc, color_bitmap as _);
let old_mask_bitmap = SelectObject(contexts.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);
contexts.cleanup();
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
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)
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)
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
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;
icon_info.hbmMask = bitmaps.mask_bitmap;
icon_info.hbmColor = bitmaps.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;
@@ -198,22 +251,67 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
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
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
// 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();
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 {
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
@@ -232,57 +330,68 @@ pub fn cleanup() {
}
}
} 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.");
}
// Use the shared cursor restoration logic
restore_default_cursor_internal();
}
println!("System cursors restoration complete.");
}
// 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 {
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;
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
*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
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 ---
@@ -295,9 +404,11 @@ pub fn hide_system_cursor() -> bool {
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());
eprintln!(
"Failed to register control handler: {}",
std::io::Error::last_os_error()
);
}
}
});
@@ -312,17 +423,26 @@ pub fn hide_system_cursor() -> bool {
unsafe {
for &cursor_id in SYSTEM_CURSORS.iter() {
// 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() {
// Save the original cursor for restoration
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
} else {
eprintln!("Failed to save original cursor for ID: {}", cursor_id);
}
}
}
// Store original cursors for restoration
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
@@ -332,66 +452,38 @@ pub fn hide_system_cursor() -> bool {
}
// 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;
}
}
};
pub fn update_cursor_info(image: &RgbaImage) -> bool {
// 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 {
// 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)
};
let new_cursor = 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);
}
if let Some(mut cursor_guard) = safe_lock(&CUSTOM_CURSOR, "updating custom cursor") {
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
});
// Store the new cursor
*cursor_guard = Some(new_cursor_wrapper);
} else {
eprintln!("Failed to update custom cursor - mutex lock failed");
// Clean up the new cursor since we can't store it
DestroyIcon(new_cursor_wrapper.0);
return false;
}
if !success {
@@ -403,27 +495,41 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
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;
}
}
}
// 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
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)
];
// Apply to priority cursors first
// Apply to priority cursors first (with error logging)
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
if !apply_cursor_to_system_cursor(cursor, cursor_id, true) {
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() {
// 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
if !apply_cursor_to_system_cursor(cursor, cursor_id, false) {
success = false;
}
}
@@ -497,7 +579,29 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
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() {
cleanup();
}
+13 -18
View File
@@ -4,27 +4,16 @@ pub mod types;
mod cursor;
mod state;
use std::path::PathBuf;
use crate::platform::Result;
use image::RgbaImage;
use anyhow::{anyhow, 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,
};
pub fn set_cursor(image: &RgbaImage) -> Result<()> {
// 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));
if !cursor::update_cursor_info(image) {
return Err(anyhow!("Failed to update cursor"));
}
Ok(())
@@ -36,10 +25,16 @@ pub fn restore_default_cursor() -> Result<()> {
Ok(())
}
pub fn restore_default_cursor_temporarily() -> Result<()> {
println!("Windows platform: temporarily restoring default cursor");
cursor::restore_cursor_temporarily();
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());
return Err(anyhow!("Failed to initialize cursor system"));
}
Ok(())
}
@@ -9,9 +9,24 @@ use winapi::{
// --- Thread-Safe Handle Wrappers ---
// 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)]
pub struct SyncHCURSOR(pub HCURSOR);
unsafe impl Send for SyncHCURSOR {}
unsafe impl Sync for SyncHCURSOR {}
+14 -2
View File
@@ -14,7 +14,11 @@
"all": false,
"window": {
"all": false,
"startDragging": true
"startDragging": true,
"minimize": true,
"hide": true,
"show": true,
"close": true
},
"shell": {
"all": false,
@@ -27,6 +31,12 @@
]
}
},
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": false,
"menuOnLeftClick": false,
"title": "Cursor Rush"
},
"bundle": {
"active": true,
"identifier": "com.cursor-rush.dev",
@@ -47,13 +57,15 @@
},
"windows": [
{
"label": "main",
"title": "Cursor Rush",
"width": 310,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true
"transparent": true,
"visible": true
}
]
}
+104 -16
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { appWindow } from "@tauri-apps/api/window";
import "./App.css";
@@ -17,14 +17,65 @@ function App() {
const [activePage, setActivePage] = useState("cursors");
const [cursors, setCursors] = useState([]);
const [currentCursorIndex, setCurrentCursorIndex] = useState(0);
const [settings, setSettings] = useState({
minimizeToTrayOnClose: true,
startWithWindows: false,
autoApplyCursorOnSelection: true
});
// Load cursors on mount
// Function to minimize app to tray
const minimizeToTray = useCallback(async () => {
try {
// First hide the window to ensure it disappears immediately
await appWindow.hide();
// Then call the backend to ensure the tray is visible
await invoke("minimize_to_tray");
} catch (error) {
console.error("Failed to minimize to tray:", error);
// Fallback to window API if the invoke fails
try {
await appWindow.hide();
} catch (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
useEffect(() => {
const loadCursors = async () => {
try {
// Get cursors from the Tauri backend
const loadedCursors = await invoke("get_cursors");
console.log("Loaded cursors:", loadedCursors);
setCursors(loadedCursors || []);
// Get current cursor index
@@ -47,10 +98,56 @@ function App() {
loadCursors();
}, []);
// Set up window close event listener
useEffect(() => {
const setupCloseHandler = async () => {
const unlisten = await appWindow.onCloseRequested(async (event) => {
if (settings.minimizeToTrayOnClose) {
// Prevent the window from closing
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
try {
await minimizeToTray();
} catch (error) {
console.error('Failed to minimize to tray on close:', error);
}
} else {
// Let the window close normally, but we need to call quit_app to ensure proper cleanup
try {
await invoke("quit_app");
} catch (error) {
console.error("Failed to quit app:", error);
}
}
});
return unlisten;
};
let unlistenFn;
setupCloseHandler().then(unlisten => {
unlistenFn = unlisten;
}).catch(err => console.error('Failed to set up close handler:', err));
// Clean up event listener on unmount
return () => {
if (unlistenFn) {
unlistenFn();
}
};
}, [settings.minimizeToTrayOnClose, minimizeToTray]);
// Functions to handle cursor operations using Tauri backend
const selectCursor = async (index) => {
try {
console.log(`Selecting cursor at index ${index}`);
await invoke("select_cursor", { index });
setCurrentCursorIndex(index);
} catch (error) {
@@ -62,7 +159,6 @@ function App() {
if (cursors.length === 0) return;
try {
console.log('Switching to next cursor');
const newIndex = await invoke("next_cursor");
setCurrentCursorIndex(newIndex);
} catch (error) {
@@ -73,20 +169,9 @@ function App() {
}
};
// Function to minimize app to tray
const minimizeToTray = async () => {
try {
console.log('Minimizing app to tray');
await appWindow.hide();
} catch (error) {
console.error("Failed to minimize to tray:", error);
}
};
// Function to exit app
const quitApp = async () => {
try {
console.log('Quitting app');
await invoke("quit_app");
} catch (error) {
console.error("Failed to quit app:", error);
@@ -101,6 +186,7 @@ function App() {
setActivePage={setActivePage}
minimizeToTray={minimizeToTray}
quitApp={quitApp}
settings={settings}
/>
<Content
@@ -109,6 +195,8 @@ function App() {
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
settings={settings}
setSettings={setSettings}
/>
<Footer
+2 -2
View File
@@ -7,7 +7,7 @@ import Cursors from './content_modules/Cursors';
import Crates from './content_modules/Crates';
import Settings from './content_modules/Settings';
function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor }) {
function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor, settings, setSettings }) {
// Render the appropriate content based on active page
const renderContent = () => {
switch (activePage) {
@@ -29,7 +29,7 @@ function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCu
case 'crates':
return <Crates />;
case 'settings':
return <Settings />;
return <Settings settings={settings} setSettings={setSettings} />;
default:
return <Cursors
cursors={cursors}
+12 -1
View File
@@ -6,7 +6,8 @@
position: relative;
z-index: 10;
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 {
@@ -64,6 +65,16 @@
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-controls {
-webkit-app-region: no-drag; /* Override drag region for buttons */
+45 -3
View File
@@ -1,6 +1,8 @@
import React from 'react';
import './Header.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { appWindow } from '@tauri-apps/api/window';
import { invoke } from '@tauri-apps/api/tauri';
import {
faGripVertical,
faBoxOpen,
@@ -16,7 +18,7 @@ import {
faBagShopping
} from '@fortawesome/free-solid-svg-icons';
function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }) {
// Navigation buttons for each page with icons
const navItems = [
{ id: 'inventory', icon: faBagShopping, tooltip: 'Inventory' },
@@ -25,8 +27,40 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
{ id: 'settings', icon: faCog, tooltip: 'Settings' }
];
// Handle close button click based on settings
const handleCloseClick = () => {
if (settings && settings.minimizeToTrayOnClose) {
console.log('Close button clicked, minimizing to tray based on settings');
minimizeToTray();
} else {
console.log('Close button clicked, quitting application based on settings');
quitApp();
}
};
// 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 (
<header className="app-header" data-tauri-drag-region>
<header className="app-header" data-tauri-drag-region onMouseDown={handleMouseDown}>
<div className="tabs-container">
{navItems.map((item) => (
<div
@@ -38,11 +72,19 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
<FontAwesomeIcon icon={item.icon} />
</div>
))}
{/* Dedicated drag area */}
<div className="drag-area" title="Drag to move window"></div>
<div className="window-controls">
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
_
</button>
<button className="close-button" onClick={quitApp} title="Quit Application">
<button
className="close-button"
onClick={handleCloseClick}
title={settings && settings.minimizeToTrayOnClose ? "Minimize to Tray" : "Quit Application"}
>
×
</button>
</div>
@@ -29,7 +29,9 @@
.setting-item {
padding: 8px 0;
display: flex;
align-items: center;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.setting-item label {
@@ -73,3 +75,31 @@
font-size: 14px;
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,7 +1,29 @@
import React from 'react';
import { invoke } from "@tauri-apps/api/tauri";
import './Settings.css';
function Settings() {
function Settings({ settings, setSettings }) {
// Handler for checkbox changes
const handleSettingChange = (settingName) => {
setSettings({
...settings,
[settingName]: !settings[settingName]
});
};
// 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 (
<div className="settings-container">
<h2 className="content-title">Settings</h2>
@@ -10,13 +32,21 @@ function Settings() {
<h3>Application Settings</h3>
<div className="setting-item">
<label>
<input type="checkbox" />
<input
type="checkbox"
checked={settings.startWithWindows}
onChange={() => handleSettingChange('startWithWindows')}
/>
Start with Windows
</label>
</div>
<div className="setting-item">
<label>
<input type="checkbox" checked />
<input
type="checkbox"
checked={settings.minimizeToTrayOnClose}
onChange={() => handleSettingChange('minimizeToTrayOnClose')}
/>
Minimize to tray on close
</label>
</div>
@@ -26,10 +56,25 @@ function Settings() {
<h3>Cursor Settings</h3>
<div className="setting-item">
<label>
<input type="checkbox" checked />
<input
type="checkbox"
checked={settings.autoApplyCursorOnSelection}
onChange={() => handleSettingChange('autoApplyCursorOnSelection')}
/>
Auto-apply cursor on selection
</label>
</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 className="settings-group">
+1 -3
View File
@@ -3,7 +3,5 @@ import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>,
<App />
);