Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa039c7291 | ||
|
|
73634440cf | ||
|
|
eced2a561c | ||
|
|
61157a2402 | ||
|
|
67ee3dc8d1 | ||
|
|
7016e96673 | ||
|
|
09374eecb3 | ||
|
|
4f71789710 | ||
|
|
a9976cbbe5 | ||
|
|
bba65f2f97 | ||
|
|
c1b8160f4a | ||
|
|
16fc8949b3 | ||
|
|
b31aefe896 | ||
|
|
f60f058cf9 | ||
|
|
9f733ed39b |
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
CODE_CLEANUP_TODO.md
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
[workspace]
|
||||
members = ["frontend/src-tauri"]
|
||||
resolver = "3"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!!
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
println!("System cursors restoration complete.");
|
||||
// Use the shared cursor restoration logic
|
||||
restore_default_cursor_internal();
|
||||
}
|
||||
}
|
||||
|
||||
// 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,51 +452,26 @@ 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(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);
|
||||
@@ -384,14 +479,11 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
|
||||
|
||||
// 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
|
||||
});
|
||||
} 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();
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||