15 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
95 changed files with 1298 additions and 590 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"
+66 -68
View File
@@ -16,66 +16,51 @@ A cross-platform application for customizing your cursor with gameified logic.
## 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
- npm 7.0.0 or later
- 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:
- Tauri 1.6.0 - Cross-platform framework
- React - Frontend framework
- image 0.24.7 - Image processing
- 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
@@ -86,11 +71,15 @@ cd frontend
# Install npm dependencies
npm install
# Run in development mode
# 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
@@ -112,11 +101,16 @@ The application comes with sample cursors:
- `red.png` - A red-colored cursor
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
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)
@@ -137,38 +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
## Project Structure (Tauri Version)
## Troubleshooting
```
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
│ │ └── platform/ # Platform-specific code
│ │ ├── mod.rs # Platform module exports
│ │ ├── windows/ # Windows-specific code
│ │ ├── macos/ # macOS-specific code
│ │ └── linux/ # Linux-specific code
│ └── Cargo.toml # Rust dependencies
└── package.json # npm dependencies
```
### 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
+11 -3
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"] }
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();
}
}
+254 -214
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,267 +78,254 @@ 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 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())
}
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
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 || {
app_handle.exit(0);
});
}
// 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() {
eprintln!("Failed to initialize platform: {}", e);
}
// 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
}
]
}
+112 -24
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,16 +17,67 @@ 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
const currentIndex = await invoke("get_current_cursor_index");
setCurrentCursorIndex(currentIndex);
@@ -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) {
@@ -60,9 +157,8 @@ function App() {
const nextCursor = async () => {
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);
@@ -96,22 +181,25 @@ function App() {
return (
<div className="modal-container">
<div className="modal">
<Header
activePage={activePage}
<Header
activePage={activePage}
setActivePage={setActivePage}
minimizeToTray={minimizeToTray}
quitApp={quitApp}
settings={settings}
/>
<Content
<Content
activePage={activePage}
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
settings={settings}
setSettings={setSettings}
/>
<Footer
<Footer
version="0.1.0"
status={cursors.length > 0 ? `${cursors.length} cursors loaded` : "No cursors found"}
/>
+6 -6
View File
@@ -7,19 +7,19 @@ 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) {
case 'inventory':
return <Inventory
return <Inventory
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
/>;
case 'cursors':
return (
<Cursors
<Cursors
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
@@ -29,9 +29,9 @@ function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCu
case 'crates':
return <Crates />;
case 'settings':
return <Settings />;
return <Settings settings={settings} setSettings={setSettings} />;
default:
return <Cursors
return <Cursors
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
@@ -39,7 +39,7 @@ function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCu
/>;
}
};
return (
<div className="app-content">
{renderContent()}
+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 */
+53 -11
View File
@@ -1,12 +1,14 @@
import React from 'react';
import './Header.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faGripVertical,
faBoxOpen,
faMousePointer,
faBoxes,
faCog,
import { appWindow } from '@tauri-apps/api/window';
import { invoke } from '@tauri-apps/api/tauri';
import {
faGripVertical,
faBoxOpen,
faMousePointer,
faBoxes,
faCog,
faBoxArchive,
faBox,
faWarehouse,
@@ -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' },
@@ -24,12 +26,44 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp }) {
{ id: 'crates', icon: faBoxOpen, tooltip: 'Crates' },
{ 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
<div
key={item.id}
className={`tab ${activePage === item.id ? 'active' : ''}`}
onClick={() => setActivePage(item.id)}
@@ -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,26 +32,49 @@ 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>
</div>
<div className="settings-group">
<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 />
);