Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa039c7291 | ||
|
|
73634440cf | ||
|
|
eced2a561c | ||
|
|
61157a2402 | ||
|
|
67ee3dc8d1 | ||
|
|
7016e96673 | ||
|
|
09374eecb3 | ||
|
|
4f71789710 | ||
|
|
a9976cbbe5 | ||
|
|
bba65f2f97 | ||
|
|
c1b8160f4a | ||
|
|
16fc8949b3 | ||
|
|
b31aefe896 | ||
|
|
f60f058cf9 |
@@ -1,3 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
CODE_CLEANUP_TODO.md
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["frontend/src-tauri"]
|
||||||
|
resolver = "3"
|
||||||
@@ -42,23 +42,25 @@ frontend/
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Rust 1.70.0 or later
|
- Rust 1.56.0 or later (tested with 1.86.0)
|
||||||
- Cargo
|
- Cargo
|
||||||
- Node.js 16.0.0 or later
|
- Node.js 16.0.0 or later (tested with 20.11.0)
|
||||||
- npm 7.0.0 or later
|
- npm 7.0.0 or later (tested with 10.2.4)
|
||||||
- Platform-specific dependencies:
|
- Platform-specific dependencies:
|
||||||
- Windows: None (fully supported)
|
- Windows: Visual Studio Build Tools (fully supported)
|
||||||
- macOS: Development in progress
|
- macOS: Development in progress
|
||||||
- Linux: Development in progress
|
- Linux: Development in progress
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
The application uses the following major dependencies:
|
The application uses the following major dependencies:
|
||||||
- Tauri 1.6.0 - Cross-platform framework
|
- Tauri 1.8.3 - Cross-platform framework
|
||||||
- React - Frontend framework
|
- React 19.1.0 - Frontend framework
|
||||||
- image 0.24.7 - Image processing
|
- image 0.24 - Image processing
|
||||||
- winapi 0.3.9 - Windows API bindings
|
- winapi 0.3.9 - Windows API bindings
|
||||||
- once_cell 1.19.0 - Lazy initialization
|
- once_cell 1.19.0 - Lazy initialization
|
||||||
|
- steamworks 0.11.0 - Steam integration
|
||||||
|
- parking_lot 0.12.3 - High-performance synchronization
|
||||||
|
|
||||||
### Build Commands
|
### Build Commands
|
||||||
|
|
||||||
@@ -69,11 +71,15 @@ cd frontend
|
|||||||
# Install npm dependencies
|
# Install npm dependencies
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Run in development mode
|
# Run in development mode (starts both frontend and backend)
|
||||||
npm run tauri dev
|
npm run tauri dev
|
||||||
|
|
||||||
# Build for production
|
# Build for production
|
||||||
npm run tauri build
|
npm run tauri build
|
||||||
|
|
||||||
|
# Alternative: Run frontend and backend separately for debugging
|
||||||
|
# Terminal 1: npm run dev (starts Vite dev server)
|
||||||
|
# Terminal 2: npm run tauri dev --no-dev-server (starts Tauri without dev server)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -95,11 +101,16 @@ The application comes with sample cursors:
|
|||||||
- `red.png` - A red-colored cursor
|
- `red.png` - A red-colored cursor
|
||||||
|
|
||||||
To add your own cursors:
|
To add your own cursors:
|
||||||
1. Create PNG images with transparent backgrounds
|
1. Create PNG or CUR images with transparent backgrounds
|
||||||
2. Place them in the `frontend/src/cursors` directory
|
2. Place them in the `frontend/src/cursors` directory
|
||||||
3. Restart the application to load the new cursors
|
3. Restart the application to load the new cursors
|
||||||
4. The cursor name in the UI will be derived from the filename
|
4. The cursor name in the UI will be derived from the filename
|
||||||
|
|
||||||
|
**Note**: The application automatically scans multiple directories for cursor files:
|
||||||
|
- `frontend/src/cursors/` (recommended location)
|
||||||
|
- `frontend/src-tauri/src/cursors/`
|
||||||
|
- `frontend/src-tauri/cursors/`
|
||||||
|
|
||||||
## Hotkeys
|
## Hotkeys
|
||||||
|
|
||||||
- Ctrl+Shift+F12: Exit the application (restores original system cursor)
|
- Ctrl+Shift+F12: Exit the application (restores original system cursor)
|
||||||
@@ -120,17 +131,42 @@ To add your own cursors:
|
|||||||
- The application uses event-driven cursor updates instead of polling when possible
|
- The application uses event-driven cursor updates instead of polling when possible
|
||||||
- Cursor images are cached to improve performance when switching between cursors
|
- Cursor images are cached to improve performance when switching between cursors
|
||||||
|
|
||||||
## Previously Fixed Issues
|
## Known Issues & Status
|
||||||
|
|
||||||
|
### Recently Fixed Issues
|
||||||
The following issues have been addressed in recent updates:
|
The following issues have been addressed in recent updates:
|
||||||
- Task Manager compatibility: The application now properly handles elevated applications
|
- Task Manager compatibility: The application now properly handles elevated applications
|
||||||
- System tray menu stability: Fixed issues with the tray menu disappearing when mousing over it
|
- System tray menu stability: Fixed issues with the tray menu disappearing when mousing over it
|
||||||
|
|
||||||
|
### Current Known Issues
|
||||||
|
- Some system tray menu interactions may still be inconsistent on certain Windows versions
|
||||||
|
- Z-ordering issues where custom cursor may hide behind system tray menus due to Windows UI prioritization
|
||||||
|
- Performance may be affected when Task Manager or other elevated applications are open due to Windows security restrictions
|
||||||
|
|
||||||
## Steam Integration
|
## Steam Integration
|
||||||
|
|
||||||
The application includes integration with the Steam API for future gameified features:
|
The application includes integration with the Steam API (AppID: 1430270):
|
||||||
|
- Shows as an active game in Steam for time tracking purposes
|
||||||
|
- Steam integration initializes automatically when the application starts
|
||||||
- Item drop functionality is implemented but not yet active
|
- Item drop functionality is implemented but not yet active
|
||||||
- Steam must be running for this functionality to work
|
- Steam must be running for the integration to work properly
|
||||||
|
- The application will appear in your Steam library as "running" while active
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Issues
|
||||||
|
- **Missing Visual C++ Redistributable**: Install Visual Studio Build Tools or Visual C++ Redistributable packages
|
||||||
|
- **Node.js library issues**: Reinstall Node.js (version 16.0.0 or later) and ensure all dependencies are properly installed
|
||||||
|
- **Rust compilation errors**: Ensure Rust 1.56.0 or later is installed and up to date
|
||||||
|
|
||||||
|
### Runtime Issues
|
||||||
|
- **Application won't start**: Check that all dependencies are installed and Steam is running (if using Steam integration)
|
||||||
|
- **Cursor not changing**: Verify cursor files exist in the correct directory and have proper file permissions
|
||||||
|
- **System tray issues**: Try restarting the application or check Windows notification area settings
|
||||||
|
|
||||||
|
### Development Issues
|
||||||
|
- **Tauri dev server not starting**: Ensure port 5173 is available or specify a different port
|
||||||
|
- **Hot reload not working**: Check that the Vite dev server is running properly in the background
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ authors = ["you"]
|
|||||||
license = ""
|
license = ""
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.56"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.5.2", features = [] }
|
tauri-build = { version = "1.5.2", features = [] }
|
||||||
|
zip = { version = "4.0.0", features = ["zstd"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
|
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
|
||||||
@@ -21,7 +21,14 @@ image = "0.24"
|
|||||||
# Ensure "consoleapi" is in this features list
|
# Ensure "consoleapi" is in this features list
|
||||||
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
|
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
steamworks = "0.10.0"
|
steamworks = "0.11.0"
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
rand = "0.9.1"
|
||||||
|
tokio = { version = "1.45.1", features = ["macros", "parking_lot", "rt-multi-thread", "time"] }
|
||||||
|
zip = { version = "4.0.0", features = ["zstd"] }
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
futures = "0.3.31"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
# DO NOT REMOVE!!
|
# DO NOT REMOVE!!
|
||||||
|
|||||||
@@ -1,3 +1,61 @@
|
|||||||
fn main() {
|
use std::{env, fs::{self, read_dir}, io::Write, path::PathBuf};
|
||||||
tauri_build::build()
|
|
||||||
|
use zip::{write::SimpleFileOptions, ZipWriter};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
tauri_build::build();
|
||||||
|
|
||||||
|
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||||
|
|
||||||
|
let profile_target_dir = out_dir
|
||||||
|
.ancestors()
|
||||||
|
.nth(3)
|
||||||
|
.ok_or("Failed to determine profile target directory from OUT_DIR")?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:info=Build script running for target OS: {}",
|
||||||
|
env::var("CARGO_CFG_TARGET_OS").unwrap_or_default()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"cargo:info=Final libraries will be copied to: {}",
|
||||||
|
profile_target_dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Define libraries and their source locations based on OS ---
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS")?;
|
||||||
|
|
||||||
|
let source_subdir = if target_os == "windows" {
|
||||||
|
"lib/win64"
|
||||||
|
} else if target_os == "linux" {
|
||||||
|
"lib/linux64"
|
||||||
|
} else if target_os == "macos" {
|
||||||
|
"lib/osx"
|
||||||
|
} else {
|
||||||
|
return Err("Unsupported target".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
for f in read_dir(source_subdir)? {
|
||||||
|
let f = f?;
|
||||||
|
println!("cargo:rerun-if-changed={}", f.path().display());
|
||||||
|
fs::copy(f.path(), profile_target_dir.join(f.file_name()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build data file
|
||||||
|
println!("cargo:rerun-if-changed=src/cursors");
|
||||||
|
println!("cargo:rerun-if-changed=src/cursor_def.json");
|
||||||
|
|
||||||
|
let mut zip = ZipWriter::new(fs::OpenOptions::new().write(true).create(true).open(profile_target_dir.join("cursor_rush.dat"))?);
|
||||||
|
zip.start_file("cursor_def.json", SimpleFileOptions::default())?;
|
||||||
|
zip.write_all(&fs::read("src/cursor_def.json")?)?;
|
||||||
|
|
||||||
|
for f in read_dir("src/cursors")? {
|
||||||
|
let f = f?;
|
||||||
|
zip.start_file(f.file_name().to_string_lossy(), SimpleFileOptions::default())?;
|
||||||
|
zip.write_all(&fs::read(f.path())?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.finish()?;
|
||||||
|
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
@@ -18,9 +18,19 @@ impl Steam {
|
|||||||
pub fn drop_item(&self) {
|
pub fn drop_item(&self) {
|
||||||
// Placeholder for item drop functionality
|
// Placeholder for item drop functionality
|
||||||
// The actual implementation would use the Steam API to trigger an item drop
|
// The actual implementation would use the Steam API to trigger an item drop
|
||||||
println!("Steam item drop triggered (placeholder)");
|
|
||||||
|
|
||||||
// In a real implementation with the correct API version, we would use:
|
// In a real implementation with the correct API version, we would use:
|
||||||
// self.client.get_inventory().trigger_item_drop(item_def_id);
|
// self.client.get_inventory().trigger_item_drop(item_def_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
// The steamworks crate handles cleanup automatically when the client is dropped
|
||||||
|
// But we can add any additional cleanup logic here if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Steam {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,74 @@
|
|||||||
use std::sync::Mutex;
|
use integrations::steam::Steam;
|
||||||
use serde::{Serialize, Deserialize};
|
use parking_lot::Mutex;
|
||||||
use tauri::{Manager, AppHandle, CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent, WindowEvent};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::{Path, PathBuf};
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::env;
|
use std::thread::spawn;
|
||||||
|
use std::{env, fs};
|
||||||
|
use tauri::{
|
||||||
|
AppHandle, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
|
||||||
|
SystemTrayMenuItem, WindowEvent,
|
||||||
|
};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
mod platform;
|
|
||||||
mod integrations;
|
mod integrations;
|
||||||
|
mod platform;
|
||||||
|
mod animator;
|
||||||
|
|
||||||
|
static STEAM: LazyLock<Mutex<Steam>> = LazyLock::new(|| Mutex::new(Steam::new()));
|
||||||
|
static CURSOR_CMD: LazyLock<broadcast::Sender<CursorCmd>> =
|
||||||
|
LazyLock::new(|| broadcast::channel(5).0);
|
||||||
|
static CURSOR_CHANGED: LazyLock<broadcast::Sender<usize>> =
|
||||||
|
LazyLock::new(|| broadcast::channel(5).0);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CursorCmd {
|
||||||
|
Select(usize),
|
||||||
|
Next,
|
||||||
|
Reapply,
|
||||||
|
Restore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum RandomRange {
|
||||||
|
Constant(f32),
|
||||||
|
Linear(f32, f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RandomRange {
|
||||||
|
pub fn next(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
RandomRange::Constant(_) => todo!(),
|
||||||
|
RandomRange::Linear(_, _) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ParticleConfig {
|
||||||
|
frames: Vec<String>,
|
||||||
|
animation_speed: f32,
|
||||||
|
gravity_x: RandomRange,
|
||||||
|
gravity_y: RandomRange,
|
||||||
|
rate: f32,
|
||||||
|
fade_in: f32,
|
||||||
|
fade_out: f32,
|
||||||
|
duration: RandomRange,
|
||||||
|
}
|
||||||
|
|
||||||
// Define cursor structure
|
// Define cursor structure
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
name: String,
|
name: String,
|
||||||
path: String,
|
frames: Vec<String>,
|
||||||
|
animation_speed: f32,
|
||||||
|
particles: Vec<ParticleConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// App state to track cursors
|
// App state to track cursors
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
cursors: Vec<Cursor>,
|
cursors: Arc<Vec<Cursor>>,
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,142 +78,60 @@ pub struct AppStateWrapper(Mutex<AppState>);
|
|||||||
// Command to get all cursors
|
// Command to get all cursors
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
|
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
|
||||||
let app_state = state.0.lock().unwrap();
|
let app_state = state.0.lock();
|
||||||
app_state.cursors.clone()
|
(*app_state.cursors).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to get current cursor index
|
// Command to get current cursor index
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
|
fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
|
||||||
let app_state = state.0.lock().unwrap();
|
let app_state = state.0.lock();
|
||||||
app_state.current_index
|
app_state.current_index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to perform application cleanup (Steam shutdown and cursor restoration)
|
||||||
|
fn perform_app_cleanup() {
|
||||||
|
// Clean up Steam integration
|
||||||
|
{
|
||||||
|
let steam = STEAM.lock();
|
||||||
|
steam.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up and restore default cursor before exit
|
||||||
|
match platform::restore_system_cursor() {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Command to select a cursor
|
// Command to select a cursor
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> {
|
fn select_cursor(index: usize, app_handle: AppHandle) -> Result<(), String> {
|
||||||
println!("=== RESOURCE DEBUG [select_cursor] ===");
|
println!("=== RESOURCE DEBUG [select_cursor] ===");
|
||||||
|
let mut r = CURSOR_CHANGED.subscribe();
|
||||||
|
CURSOR_CMD.send(CursorCmd::Select(index)).ok();
|
||||||
|
let new_index = r.blocking_recv().unwrap();
|
||||||
|
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
|
||||||
|
|
||||||
let mut app_state = state.0.lock().unwrap();
|
Ok(())
|
||||||
if index < app_state.cursors.len() {
|
|
||||||
app_state.current_index = index;
|
|
||||||
|
|
||||||
let cursor = &app_state.cursors[index];
|
|
||||||
println!("Cursor selected: {}", cursor.name);
|
|
||||||
println!("Original path: {}", cursor.path);
|
|
||||||
|
|
||||||
// Get the resource directory
|
|
||||||
let resource_dir = app_handle.path_resolver().resource_dir();
|
|
||||||
println!("Resource dir: {:?}", resource_dir);
|
|
||||||
|
|
||||||
// List contents of resource dir
|
|
||||||
if let Some(res_dir) = &resource_dir {
|
|
||||||
println!("Listing contents of resource dir: {}", res_dir.display());
|
|
||||||
if let Ok(entries) = std::fs::read_dir(res_dir) {
|
|
||||||
for (i, entry) in entries.filter_map(Result::ok).enumerate() {
|
|
||||||
println!(" {}: {}", i, entry.path().display());
|
|
||||||
|
|
||||||
// List subdirectories
|
|
||||||
if entry.path().is_dir() {
|
|
||||||
if let Ok(subentries) = std::fs::read_dir(entry.path()) {
|
|
||||||
for (j, subentry) in subentries.filter_map(Result::ok).enumerate() {
|
|
||||||
println!(" {}.{}: {}", i, j, subentry.path().display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("Failed to read resource directory");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let filename = Path::new(&cursor.path)
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
|
|
||||||
|
|
||||||
println!("Extracted filename: {}", filename);
|
|
||||||
|
|
||||||
// Try using the actual file path directly
|
|
||||||
println!("Using direct file path: {}", cursor.path);
|
|
||||||
|
|
||||||
// Verify the file exists
|
|
||||||
if Path::new(&cursor.path).exists() {
|
|
||||||
println!("File exists, applying cursor...");
|
|
||||||
|
|
||||||
if let Err(e) = platform::set_system_cursor(&cursor.path) {
|
|
||||||
println!("Error applying cursor: {}", e);
|
|
||||||
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit an event to notify the frontend
|
|
||||||
let _ = app_handle.emit_all("cursor_changed", index); // Ignore error
|
|
||||||
|
|
||||||
println!("Successfully applied cursor using actual file path");
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
println!("File doesn't exist: {}", cursor.path);
|
|
||||||
return Err(format!("Cursor file does not exist: {}", cursor.path));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err("Index out of bounds".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to go to the next cursor
|
// Command to go to the next cursor
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> {
|
fn next_cursor(app_handle: AppHandle) -> Result<usize, String> {
|
||||||
println!("=== RESOURCE DEBUG [next_cursor] ===");
|
println!("=== RESOURCE DEBUG [next_cursor] ===");
|
||||||
|
let mut r = CURSOR_CHANGED.subscribe();
|
||||||
let mut app_state = state.0.lock().unwrap();
|
CURSOR_CMD.send(CursorCmd::Next).ok();
|
||||||
if !app_state.cursors.is_empty() {
|
let new_index = r.blocking_recv().unwrap();
|
||||||
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
|
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
|
||||||
|
Ok(new_index)
|
||||||
let cursor = &app_state.cursors[app_state.current_index];
|
|
||||||
println!("Next cursor: {} ({})", cursor.name, cursor.path);
|
|
||||||
|
|
||||||
let filename = Path::new(&cursor.path)
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
|
|
||||||
|
|
||||||
// Try both resource paths like in select_cursor
|
|
||||||
println!("Extracted filename: {}", filename);
|
|
||||||
|
|
||||||
// Try using the actual file path directly
|
|
||||||
println!("Using direct file path: {}", cursor.path);
|
|
||||||
|
|
||||||
// Verify the file exists
|
|
||||||
if Path::new(&cursor.path).exists() {
|
|
||||||
println!("File exists, applying cursor...");
|
|
||||||
|
|
||||||
if let Err(e) = platform::set_system_cursor(&cursor.path) {
|
|
||||||
println!("Error applying cursor: {}", e);
|
|
||||||
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit an event to notify the frontend
|
|
||||||
let _ = app_handle.emit_all("cursor_changed", app_state.current_index); // Ignore error
|
|
||||||
|
|
||||||
println!("Successfully applied cursor using actual file path");
|
|
||||||
return Ok(app_state.current_index);
|
|
||||||
} else {
|
|
||||||
println!("File doesn't exist: {}", cursor.path);
|
|
||||||
return Err(format!("Cursor file does not exist: {}", cursor.path));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err("No cursors available".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to quit the application
|
// Command to quit the application
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn quit_app(app_handle: AppHandle) {
|
fn quit_app(app_handle: AppHandle) {
|
||||||
// Clean up and restore default cursor before exit
|
// Use the helper function to perform cleanup
|
||||||
match platform::restore_system_cursor() {
|
perform_app_cleanup();
|
||||||
Ok(_) => println!("Default cursor restored"),
|
|
||||||
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit the application
|
// Exit the application
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -172,111 +142,64 @@ fn quit_app(app_handle: AppHandle) {
|
|||||||
// Command to minimize the window to tray
|
// Command to minimize the window to tray
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> {
|
fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> {
|
||||||
println!("Minimizing to tray");
|
|
||||||
|
|
||||||
// Get the main window
|
// Get the main window
|
||||||
let window = app_handle.get_window("main")
|
let window = app_handle
|
||||||
|
.get_window("main")
|
||||||
.ok_or_else(|| "Failed to get main window".to_string())?;
|
.ok_or_else(|| "Failed to get main window".to_string())?;
|
||||||
|
|
||||||
// Hide the window
|
// Hide the window
|
||||||
window.hide().map_err(|e| format!("Failed to hide window: {}", e))?;
|
window
|
||||||
|
.hide()
|
||||||
// The tray should already be visible as configured in tauri.conf.json
|
.map_err(|e| format!("Failed to hide window: {}", e))?;
|
||||||
println!("Window hidden, application should now be accessible from the system tray");
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Command to temporarily restore system cursor (for drag operations)
|
||||||
|
#[tauri::command]
|
||||||
|
fn temporarily_restore_cursor() -> Result<(), String> {
|
||||||
|
match platform::restore_system_cursor_temporarily() {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => Err(format!("Failed to restore system cursor: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command to re-apply custom cursor (after drag operations)
|
||||||
|
#[tauri::command]
|
||||||
|
fn reapply_custom_cursor() -> Result<(), String> {
|
||||||
|
CURSOR_CMD.send(CursorCmd::Reapply).ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command to manually restore default cursors
|
||||||
|
#[tauri::command]
|
||||||
|
fn restore_default_cursors() -> Result<(), String> {
|
||||||
|
println!("Manual restore default cursors command received");
|
||||||
|
CURSOR_CMD.send(CursorCmd::Restore).ok();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// Function to load cursor files from src/cursors directory
|
// Function to load cursor files from src/cursors directory
|
||||||
fn load_cursor_files() -> Vec<Cursor> {
|
fn load_cursor_files() -> Result<Vec<Cursor>> {
|
||||||
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
|
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
|
||||||
let mut cursors = Vec::new();
|
let mut zip = ZipArchive::new(
|
||||||
|
fs::OpenOptions::new().read(true).open(
|
||||||
// Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR)
|
env::current_exe()?
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
.parent()
|
||||||
println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display());
|
.ok_or(anyhow!("failed to get parent"))?
|
||||||
|
.join("cursor_rush.dat"),
|
||||||
// Check multiple possible locations
|
)?,
|
||||||
let possible_cursor_dirs = vec![
|
)?;
|
||||||
manifest_dir.join("../src/cursors"),
|
let mut def = zip.by_name("cursor_def.json")?;
|
||||||
manifest_dir.join("src/cursors"),
|
let mut bytes = Vec::new();
|
||||||
manifest_dir.join("cursors"),
|
std::io::copy(&mut def, &mut bytes)?;
|
||||||
manifest_dir.clone(), // Check manifest dir itself
|
Ok(serde_json::from_slice(&bytes)?)
|
||||||
];
|
|
||||||
|
|
||||||
println!("CHECKING FOR PNG/CUR FILES IN MULTIPLE DIRECTORIES:");
|
|
||||||
for (i, cursors_dir) in possible_cursor_dirs.iter().enumerate() {
|
|
||||||
println!("Checking dir {}: {}", i+1, cursors_dir.display());
|
|
||||||
println!(" Directory exists: {}", cursors_dir.exists());
|
|
||||||
|
|
||||||
if cursors_dir.exists() {
|
|
||||||
println!(" Looking for cursor files in this directory...");
|
|
||||||
|
|
||||||
if let Ok(entries) = std::fs::read_dir(cursors_dir) {
|
|
||||||
for entry in entries.filter_map(Result::ok) {
|
|
||||||
let path = entry.path();
|
|
||||||
println!(" Found: {}", path.display());
|
|
||||||
|
|
||||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "png" || ext == "cur") {
|
|
||||||
let name = path.file_stem()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("Unknown")
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
println!(" ADDING CURSOR: {} at path {}", name, path.display());
|
|
||||||
cursors.push(Cursor {
|
|
||||||
name,
|
|
||||||
path: path.to_string_lossy().to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cursors.is_empty() {
|
|
||||||
println!(" Found {} cursor files in this directory.", cursors.len());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found no cursors, use mock data with paths relative to what resolve_resource expects
|
|
||||||
if cursors.is_empty() {
|
|
||||||
println!("No cursors found in any directory. Using mock data instead.");
|
|
||||||
println!("IMPORTANT: This is probably the source of your problem!");
|
|
||||||
println!("The app could not find your cursor PNG files. Check that they exist in one of these directories:");
|
|
||||||
for dir in &possible_cursor_dirs {
|
|
||||||
println!(" {}", dir.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock data paths should reflect their eventual resource path structure
|
|
||||||
cursors.push(Cursor {
|
|
||||||
name: "Default".to_string(),
|
|
||||||
path: "src/cursors/default.png".to_string(), // Filename will be 'default.png'
|
|
||||||
});
|
|
||||||
|
|
||||||
cursors.push(Cursor {
|
|
||||||
name: "Icon".to_string(),
|
|
||||||
path: "src/cursors/icon.png".to_string(), // Filename will be 'icon.png'
|
|
||||||
});
|
|
||||||
|
|
||||||
cursors.push(Cursor {
|
|
||||||
name: "Red".to_string(),
|
|
||||||
path: "src/cursors/red.png".to_string(), // Filename will be 'red.png'
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("Created {} mock cursor entries", cursors.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Loaded {} cursor files", cursors.len());
|
|
||||||
|
|
||||||
// Sort cursors by name for consistent ordering
|
|
||||||
cursors.sort_by(|a, b| a.name.cmp(&b.name));
|
|
||||||
cursors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
println!("Starting Cursor Rush application");
|
// Get LazyLock to create steam
|
||||||
|
let _ = STEAM.lock();
|
||||||
|
|
||||||
// Initialize platform-specific cursor handling
|
// Initialize platform-specific cursor handling
|
||||||
if let Err(e) = platform::initialize() {
|
if let Err(e) = platform::initialize() {
|
||||||
@@ -284,11 +207,17 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load cursors from assets directory
|
// Load cursors from assets directory
|
||||||
let cursors = load_cursor_files();
|
let cursors = Arc::new(load_cursor_files().expect("failed to load cursors"));
|
||||||
|
|
||||||
|
{
|
||||||
|
let cursors = cursors.clone();
|
||||||
|
spawn(move ||animator::animate_loop(cursors));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create app state
|
// Create app state
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
cursors,
|
cursors: cursors,
|
||||||
current_index: 0,
|
current_index: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,15 +240,12 @@ pub fn run() {
|
|||||||
.system_tray(system_tray)
|
.system_tray(system_tray)
|
||||||
.on_system_tray_event(|app, event| match event {
|
.on_system_tray_event(|app, event| match event {
|
||||||
SystemTrayEvent::LeftClick { .. } => {
|
SystemTrayEvent::LeftClick { .. } => {
|
||||||
println!("System tray received a left click");
|
|
||||||
if let Some(window) = app.get_window("main") {
|
if let Some(window) = app.get_window("main") {
|
||||||
match window.is_visible() {
|
match window.is_visible() {
|
||||||
Ok(visible) => {
|
Ok(visible) => {
|
||||||
if visible {
|
if visible {
|
||||||
println!("Window is already visible, focusing it");
|
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
} else {
|
} else {
|
||||||
println!("Window is hidden, showing it");
|
|
||||||
if let Err(e) = window.show() {
|
if let Err(e) = window.show() {
|
||||||
eprintln!("Failed to show window: {}", e);
|
eprintln!("Failed to show window: {}", e);
|
||||||
}
|
}
|
||||||
@@ -327,7 +253,7 @@ pub fn run() {
|
|||||||
eprintln!("Failed to focus window: {}", e);
|
eprintln!("Failed to focus window: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to check window visibility: {}", e);
|
eprintln!("Failed to check window visibility: {}", e);
|
||||||
// Try to show it anyway
|
// Try to show it anyway
|
||||||
@@ -341,29 +267,12 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SystemTrayEvent::MenuItemClick { id, .. } => {
|
SystemTrayEvent::MenuItemClick { id, .. } => {
|
||||||
println!("Menu item clicked: {}", id);
|
|
||||||
match id.as_str() {
|
match id.as_str() {
|
||||||
"next_cursor" => {
|
"next_cursor" => {
|
||||||
println!("Next cursor requested from tray");
|
println!("Next cursor requested from tray");
|
||||||
let state = app.state::<AppStateWrapper>();
|
CURSOR_CMD.send(CursorCmd::Next).ok();
|
||||||
let mut app_state = state.0.lock().unwrap();
|
|
||||||
|
|
||||||
if !app_state.cursors.is_empty() {
|
|
||||||
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
|
|
||||||
let cursor = &app_state.cursors[app_state.current_index];
|
|
||||||
|
|
||||||
if Path::new(&cursor.path).exists() {
|
|
||||||
if let Err(e) = platform::set_system_cursor(&cursor.path) {
|
|
||||||
eprintln!("Error applying cursor from tray: {}", e);
|
|
||||||
} else {
|
|
||||||
// Emit an event to notify the frontend
|
|
||||||
let _ = app.emit_all("cursor_changed", app_state.current_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"show_window" => {
|
"show_window" => {
|
||||||
println!("Show window requested from tray");
|
|
||||||
if let Some(window) = app.get_window("main") {
|
if let Some(window) = app.get_window("main") {
|
||||||
if let Err(e) = window.show() {
|
if let Err(e) = window.show() {
|
||||||
eprintln!("Failed to show window: {}", e);
|
eprintln!("Failed to show window: {}", e);
|
||||||
@@ -376,12 +285,9 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
println!("Quit requested from tray");
|
// Use the helper function to perform cleanup
|
||||||
// Clean up and restore default cursor before exit
|
perform_app_cleanup();
|
||||||
match platform::restore_system_cursor() {
|
|
||||||
Ok(_) => println!("Default cursor restored"),
|
|
||||||
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
|
|
||||||
}
|
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -397,13 +303,10 @@ pub fn run() {
|
|||||||
|
|
||||||
// Get the window and hide it
|
// Get the window and hide it
|
||||||
let window = event.window();
|
let window = event.window();
|
||||||
println!("Window close requested in backend handler, hiding window");
|
|
||||||
|
|
||||||
// Hide the window instead of closing it
|
// Hide the window instead of closing it
|
||||||
if let Err(e) = window.hide() {
|
if let Err(e) = window.hide() {
|
||||||
eprintln!("Failed to hide window: {}", e);
|
eprintln!("Failed to hide window: {}", e);
|
||||||
} else {
|
|
||||||
println!("Window hidden successfully, app is now in system tray");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -415,15 +318,14 @@ pub fn run() {
|
|||||||
select_cursor,
|
select_cursor,
|
||||||
next_cursor,
|
next_cursor,
|
||||||
quit_app,
|
quit_app,
|
||||||
minimize_to_tray
|
minimize_to_tray,
|
||||||
|
temporarily_restore_cursor,
|
||||||
|
reapply_custom_cursor,
|
||||||
|
restore_default_cursors
|
||||||
])
|
])
|
||||||
.setup(|_app| {
|
.setup(|_app| {
|
||||||
// Register for events or perform additional setup
|
|
||||||
println!("Tauri application setup complete");
|
|
||||||
|
|
||||||
// We're not setting up a window close handler here anymore
|
// We're not setting up a window close handler here anymore
|
||||||
// The frontend will handle this based on the minimize-to-tray setting
|
// The frontend will handle this based on the minimize-to-tray setting
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
// This matches exactly how it was before our changes
|
// This matches exactly how it was before our changes
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
// Linux platform implementation for cursor handling
|
// Linux platform implementation for cursor handling
|
||||||
use super::Result;
|
use super::stub_impl::create_platform_stubs;
|
||||||
|
|
||||||
// Initialize cursor handling
|
// Generate stub implementations for Linux
|
||||||
pub fn init() -> Result<()> {
|
create_platform_stubs!("Linux");
|
||||||
println!("Linux cursor initialization not yet implemented");
|
|
||||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cursor(path: &str) -> Result<()> {
|
|
||||||
// Linux cursor implementation would go here
|
|
||||||
println!("Linux cursor setting not yet implemented for path: {}", path);
|
|
||||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restore_default_cursor() -> Result<()> {
|
|
||||||
// Linux cursor restoration would go here
|
|
||||||
println!("Linux cursor restoration not yet implemented");
|
|
||||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
// macOS platform implementation for cursor handling
|
// macOS platform implementation for cursor handling
|
||||||
use super::Result;
|
use super::stub_impl::create_platform_stubs;
|
||||||
|
|
||||||
// Initialize cursor handling
|
// Generate stub implementations for macOS
|
||||||
pub fn init() -> Result<()> {
|
create_platform_stubs!("macOS");
|
||||||
println!("macOS cursor initialization not yet implemented");
|
|
||||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cursor(path: &str) -> Result<()> {
|
|
||||||
// macOS cursor implementation would go here
|
|
||||||
println!("macOS cursor setting not yet implemented for path: {}", path);
|
|
||||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restore_default_cursor() -> Result<()> {
|
|
||||||
// macOS cursor restoration would go here
|
|
||||||
println!("macOS cursor restoration not yet implemented");
|
|
||||||
Ok(()) // Return OK for now to not block platform-agnostic code
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
// Common types for cursor handling
|
// Common types for cursor handling
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
|
// Shared stub implementations for unimplemented platforms
|
||||||
|
mod stub_impl;
|
||||||
|
|
||||||
// Windows implementation
|
// Windows implementation
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
mod windows;
|
mod windows;
|
||||||
@@ -15,12 +18,13 @@ mod macos;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod linux;
|
mod linux;
|
||||||
|
|
||||||
// Error type for platform operations
|
use anyhow::Result;
|
||||||
pub type Result<T> = std::result::Result<T, String>;
|
|
||||||
|
use image::RgbaImage;
|
||||||
|
|
||||||
// Platform-agnostic interface for cursor handling
|
// Platform-agnostic interface for cursor handling
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use self::windows::{set_cursor, restore_default_cursor, init};
|
pub use self::windows::{set_cursor, restore_default_cursor, restore_default_cursor_temporarily, init};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use self::macos::{set_cursor, restore_default_cursor, init};
|
pub use self::macos::{set_cursor, restore_default_cursor, init};
|
||||||
@@ -41,9 +45,8 @@ pub fn initialize() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cross-platform implementation for setting cursor
|
// Cross-platform implementation for setting cursor
|
||||||
pub fn set_system_cursor(path: &str) -> Result<()> {
|
pub fn set_system_cursor(image: &RgbaImage) -> Result<()> {
|
||||||
println!("Setting system cursor: {}", path);
|
set_cursor(image)
|
||||||
set_cursor(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-platform implementation for restoring default cursor
|
// Cross-platform implementation for restoring default cursor
|
||||||
@@ -52,6 +55,20 @@ pub fn restore_system_cursor() -> Result<()> {
|
|||||||
restore_default_cursor()
|
restore_default_cursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cross-platform implementation for temporarily restoring default cursor
|
||||||
|
pub fn restore_system_cursor_temporarily() -> Result<()> {
|
||||||
|
println!("Temporarily restoring system cursor");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
restore_default_cursor_temporarily()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
// For non-Windows platforms, just use the regular restore for now
|
||||||
|
restore_default_cursor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Default implementations for non-supported platforms
|
// Default implementations for non-supported platforms
|
||||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||||
pub fn set_cursor(_path: &str) -> Result<()> {
|
pub fn set_cursor(_path: &str) -> Result<()> {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Shared stub implementations for unimplemented platforms (macOS, Linux)
|
||||||
|
|
||||||
|
/// Creates a stub implementation for a platform that doesn't have cursor support yet
|
||||||
|
macro_rules! create_platform_stubs {
|
||||||
|
($platform_name:expr) => {
|
||||||
|
use super::Result;
|
||||||
|
use image::RgbaImage;
|
||||||
|
|
||||||
|
// Initialize cursor handling
|
||||||
|
pub fn init() -> Result<()> {
|
||||||
|
println!("{} cursor initialization not yet implemented", $platform_name);
|
||||||
|
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cursor from image data
|
||||||
|
pub fn set_cursor(_image: &RgbaImage) -> Result<()> {
|
||||||
|
println!("{} cursor setting not yet implemented", $platform_name);
|
||||||
|
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore default cursor
|
||||||
|
pub fn restore_default_cursor() -> Result<()> {
|
||||||
|
println!("{} cursor restoration not yet implemented", $platform_name);
|
||||||
|
Ok(()) // Return OK for now to not block platform-agnostic code
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the macro for use by platform modules
|
||||||
|
pub(crate) use create_platform_stubs;
|
||||||
@@ -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
|
// Windows platform cursor management
|
||||||
|
|
||||||
|
use ::image::RgbaImage;
|
||||||
use std::ptr::null_mut;
|
use std::ptr::null_mut;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use winapi::{
|
use winapi::{
|
||||||
@@ -15,33 +16,60 @@ use winapi::{
|
|||||||
},
|
},
|
||||||
um::consoleapi::SetConsoleCtrlHandler,
|
um::consoleapi::SetConsoleCtrlHandler,
|
||||||
shared::minwindef::{TRUE, FALSE, DWORD},
|
shared::minwindef::{TRUE, FALSE, DWORD},
|
||||||
|
shared::windef::{HDC, HBITMAP, HGDIOBJ},
|
||||||
};
|
};
|
||||||
use ::image::{RgbaImage, open as image_open};
|
|
||||||
|
|
||||||
use super::types::*;
|
|
||||||
use super::state::*;
|
use super::state::*;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
// Control handler function for console events (Ctrl+C, close, etc.)
|
// Control handler function for console events (Ctrl+C, close, etc.)
|
||||||
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
|
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
|
||||||
println!("Control event received, attempting cleanup...");
|
|
||||||
cleanup(); // Call cleanup logic
|
cleanup(); // Call cleanup logic
|
||||||
FALSE // Allow other handlers (like default exit) to run
|
FALSE // Allow other handlers (like default exit) to run
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cursor from an RGBA image
|
// Device context setup result
|
||||||
unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
struct DeviceContexts {
|
||||||
let (width, height) = image.dimensions();
|
screen_dc: HDC,
|
||||||
let width_i32 = width as i32;
|
color_dc: HDC,
|
||||||
let height_i32 = height as i32;
|
mask_dc: HDC,
|
||||||
|
}
|
||||||
|
|
||||||
// Create device context and bitmap for the color data
|
impl DeviceContexts {
|
||||||
|
unsafe fn cleanup(&self) {
|
||||||
|
DeleteDC(self.mask_dc);
|
||||||
|
DeleteDC(self.color_dc);
|
||||||
|
ReleaseDC(null_mut(), self.screen_dc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitmap creation result
|
||||||
|
struct CursorBitmaps {
|
||||||
|
color_bitmap: HBITMAP,
|
||||||
|
mask_bitmap: HBITMAP,
|
||||||
|
color_bits_ptr: *mut winapi::ctypes::c_void,
|
||||||
|
mask_bits_ptr: *mut winapi::ctypes::c_void,
|
||||||
|
old_color_bitmap: HGDIOBJ,
|
||||||
|
old_mask_bitmap: HGDIOBJ,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorBitmaps {
|
||||||
|
unsafe fn cleanup(&self, contexts: &DeviceContexts) {
|
||||||
|
SelectObject(contexts.color_dc, self.old_color_bitmap);
|
||||||
|
SelectObject(contexts.mask_dc, self.old_mask_bitmap);
|
||||||
|
DeleteObject(self.color_bitmap as _);
|
||||||
|
DeleteObject(self.mask_bitmap as _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup device contexts for cursor creation
|
||||||
|
unsafe fn setup_device_contexts() -> Option<DeviceContexts> {
|
||||||
let screen_dc = GetDC(null_mut());
|
let screen_dc = GetDC(null_mut());
|
||||||
if screen_dc.is_null() {
|
if screen_dc.is_null() {
|
||||||
eprintln!("Failed to get screen DC");
|
eprintln!("Failed to get screen DC");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a compatible DC for our bitmap
|
|
||||||
let color_dc = CreateCompatibleDC(screen_dc);
|
let color_dc = CreateCompatibleDC(screen_dc);
|
||||||
if color_dc.is_null() {
|
if color_dc.is_null() {
|
||||||
eprintln!("Failed to create compatible DC for color");
|
eprintln!("Failed to create compatible DC for color");
|
||||||
@@ -49,7 +77,6 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a compatible DC for our mask bitmap
|
|
||||||
let mask_dc = CreateCompatibleDC(screen_dc);
|
let mask_dc = CreateCompatibleDC(screen_dc);
|
||||||
if mask_dc.is_null() {
|
if mask_dc.is_null() {
|
||||||
eprintln!("Failed to create compatible DC for mask");
|
eprintln!("Failed to create compatible DC for mask");
|
||||||
@@ -58,11 +85,24 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(DeviceContexts {
|
||||||
|
screen_dc,
|
||||||
|
color_dc,
|
||||||
|
mask_dc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bitmaps for cursor creation
|
||||||
|
unsafe fn create_cursor_bitmaps(
|
||||||
|
contexts: &DeviceContexts,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Option<CursorBitmaps> {
|
||||||
// Set up BITMAPINFO structure for DIB
|
// Set up BITMAPINFO structure for DIB
|
||||||
let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
|
let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
|
||||||
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
|
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
|
||||||
bitmap_info.bmiHeader.biWidth = width_i32;
|
bitmap_info.bmiHeader.biWidth = width;
|
||||||
bitmap_info.bmiHeader.biHeight = -height_i32; // Negative for top-down DIB
|
bitmap_info.bmiHeader.biHeight = -height; // Negative for top-down DIB
|
||||||
bitmap_info.bmiHeader.biPlanes = 1;
|
bitmap_info.bmiHeader.biPlanes = 1;
|
||||||
bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA)
|
bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA)
|
||||||
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||||
@@ -71,7 +111,7 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
let mut color_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
|
let mut color_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
|
||||||
let mut ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
|
let mut ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
|
||||||
let color_bitmap = CreateDIBSection(
|
let color_bitmap = CreateDIBSection(
|
||||||
color_dc,
|
contexts.color_dc,
|
||||||
&bitmap_info,
|
&bitmap_info,
|
||||||
DIB_RGB_COLORS,
|
DIB_RGB_COLORS,
|
||||||
&mut ppv_bits_color,
|
&mut ppv_bits_color,
|
||||||
@@ -82,9 +122,7 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
|
|
||||||
if color_bitmap.is_null() || color_bits_ptr.is_null() {
|
if color_bitmap.is_null() || color_bits_ptr.is_null() {
|
||||||
eprintln!("Failed to create color DIB section");
|
eprintln!("Failed to create color DIB section");
|
||||||
DeleteDC(mask_dc);
|
contexts.cleanup();
|
||||||
DeleteDC(color_dc);
|
|
||||||
ReleaseDC(null_mut(), screen_dc);
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +131,7 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
let mut mask_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
|
let mut mask_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
|
||||||
let mut ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
|
let mut ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
|
||||||
let mask_bitmap = CreateDIBSection(
|
let mask_bitmap = CreateDIBSection(
|
||||||
mask_dc,
|
contexts.mask_dc,
|
||||||
&bitmap_info,
|
&bitmap_info,
|
||||||
DIB_RGB_COLORS,
|
DIB_RGB_COLORS,
|
||||||
&mut ppv_bits,
|
&mut ppv_bits,
|
||||||
@@ -105,32 +143,51 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
if mask_bitmap.is_null() || mask_bits_ptr.is_null() {
|
if mask_bitmap.is_null() || mask_bits_ptr.is_null() {
|
||||||
eprintln!("Failed to create mask DIB section");
|
eprintln!("Failed to create mask DIB section");
|
||||||
DeleteObject(color_bitmap as _);
|
DeleteObject(color_bitmap as _);
|
||||||
DeleteDC(mask_dc);
|
contexts.cleanup();
|
||||||
DeleteDC(color_dc);
|
|
||||||
ReleaseDC(null_mut(), screen_dc);
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select the bitmaps into the DCs
|
// Select the bitmaps into the DCs
|
||||||
let old_color_bitmap = SelectObject(color_dc, color_bitmap as _);
|
let old_color_bitmap = SelectObject(contexts.color_dc, color_bitmap as _);
|
||||||
let old_mask_bitmap = SelectObject(mask_dc, mask_bitmap as _);
|
let old_mask_bitmap = SelectObject(contexts.mask_dc, mask_bitmap as _);
|
||||||
|
|
||||||
if old_color_bitmap.is_null() || old_mask_bitmap.is_null() {
|
if old_color_bitmap.is_null() || old_mask_bitmap.is_null() {
|
||||||
eprintln!("Failed to select bitmaps into DCs");
|
eprintln!("Failed to select bitmaps into DCs");
|
||||||
DeleteObject(mask_bitmap as _);
|
DeleteObject(mask_bitmap as _);
|
||||||
DeleteObject(color_bitmap as _);
|
DeleteObject(color_bitmap as _);
|
||||||
DeleteDC(mask_dc);
|
contexts.cleanup();
|
||||||
DeleteDC(color_dc);
|
|
||||||
ReleaseDC(null_mut(), screen_dc);
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(CursorBitmaps {
|
||||||
|
color_bitmap,
|
||||||
|
mask_bitmap,
|
||||||
|
color_bits_ptr,
|
||||||
|
mask_bits_ptr,
|
||||||
|
old_color_bitmap,
|
||||||
|
old_mask_bitmap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process image data and populate bitmaps
|
||||||
|
unsafe fn process_image_data(
|
||||||
|
image: &RgbaImage,
|
||||||
|
bitmaps: &CursorBitmaps,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) {
|
||||||
// Copy the image data to the color DIB, converting RGBA to BGRA
|
// Copy the image data to the color DIB, converting RGBA to BGRA
|
||||||
let color_bits = std::slice::from_raw_parts_mut(color_bits_ptr as *mut u8, (width * height * 4) as usize);
|
let color_bits = std::slice::from_raw_parts_mut(
|
||||||
|
bitmaps.color_bits_ptr as *mut u8,
|
||||||
|
(width * height * 4) as usize,
|
||||||
|
);
|
||||||
|
|
||||||
// Calculate mask stride in bytes (1 bit per pixel, padded to DWORD)
|
// Calculate mask stride in bytes (1 bit per pixel, padded to DWORD)
|
||||||
let mask_stride = ((width + 31) / 32) * 4;
|
let mask_stride = ((width + 31) / 32) * 4;
|
||||||
let mask_bits = std::slice::from_raw_parts_mut(mask_bits_ptr as *mut u8, (mask_stride * height) as usize);
|
let mask_bits = std::slice::from_raw_parts_mut(
|
||||||
|
bitmaps.mask_bits_ptr as *mut u8,
|
||||||
|
(mask_stride * height) as usize,
|
||||||
|
);
|
||||||
|
|
||||||
// Clear mask bits (all transparent initially)
|
// Clear mask bits (all transparent initially)
|
||||||
for i in 0..(mask_stride * height) as usize {
|
for i in 0..(mask_stride * height) as usize {
|
||||||
@@ -166,29 +223,25 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create cursor from prepared bitmaps
|
||||||
|
unsafe fn create_cursor_from_bitmaps(
|
||||||
|
bitmaps: &CursorBitmaps,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
) -> Option<SyncHCURSOR> {
|
||||||
// Create ICONINFO structure
|
// Create ICONINFO structure
|
||||||
let mut icon_info: ICONINFO = std::mem::zeroed();
|
let mut icon_info: ICONINFO = std::mem::zeroed();
|
||||||
icon_info.fIcon = FALSE; // This is a cursor, not an icon
|
icon_info.fIcon = FALSE; // This is a cursor, not an icon
|
||||||
icon_info.xHotspot = width as DWORD / 2; // Center hotspot
|
icon_info.xHotspot = width as DWORD / 2; // Center hotspot
|
||||||
icon_info.yHotspot = height as DWORD / 2;
|
icon_info.yHotspot = height as DWORD / 2;
|
||||||
icon_info.hbmMask = mask_bitmap;
|
icon_info.hbmMask = bitmaps.mask_bitmap;
|
||||||
icon_info.hbmColor = color_bitmap;
|
icon_info.hbmColor = bitmaps.color_bitmap;
|
||||||
|
|
||||||
// Create the cursor
|
// Create the cursor
|
||||||
let cursor = CreateIconIndirect(&mut icon_info as *mut _);
|
let cursor = CreateIconIndirect(&mut icon_info as *mut _);
|
||||||
|
|
||||||
// Clean up resources
|
|
||||||
SelectObject(color_dc, old_color_bitmap);
|
|
||||||
SelectObject(mask_dc, old_mask_bitmap);
|
|
||||||
DeleteDC(color_dc);
|
|
||||||
DeleteDC(mask_dc);
|
|
||||||
ReleaseDC(null_mut(), screen_dc);
|
|
||||||
|
|
||||||
// Windows makes a copy of the bitmaps for the cursor, so we can delete them
|
|
||||||
DeleteObject(color_bitmap as _);
|
|
||||||
DeleteObject(mask_bitmap as _);
|
|
||||||
|
|
||||||
if cursor.is_null() {
|
if cursor.is_null() {
|
||||||
eprintln!("Failed to create cursor");
|
eprintln!("Failed to create cursor");
|
||||||
return None;
|
return None;
|
||||||
@@ -198,22 +251,67 @@ unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
|||||||
Some(SyncHCURSOR(cursor))
|
Some(SyncHCURSOR(cursor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a cursor from an RGBA image
|
||||||
|
unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let width_i32 = width as i32;
|
||||||
|
let height_i32 = height as i32;
|
||||||
|
|
||||||
|
let contexts = setup_device_contexts()?;
|
||||||
|
let bitmaps = create_cursor_bitmaps(&contexts, width_i32, height_i32)?;
|
||||||
|
|
||||||
|
process_image_data(image, &bitmaps, width, height);
|
||||||
|
|
||||||
|
let cursor = create_cursor_from_bitmaps(&bitmaps, width, height);
|
||||||
|
|
||||||
|
bitmaps.cleanup(&contexts);
|
||||||
|
contexts.cleanup();
|
||||||
|
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to safely lock a mutex with error handling
|
||||||
|
fn safe_lock<'a, T>(mutex: &'a std::sync::Mutex<T>, operation_name: &str) -> Option<std::sync::MutexGuard<'a, T>> {
|
||||||
|
match mutex.lock() {
|
||||||
|
Ok(guard) => Some(guard),
|
||||||
|
Err(poisoned) => {
|
||||||
|
eprintln!("Mutex poisoned during {}: {}", operation_name, poisoned);
|
||||||
|
// For cleanup operations, we can still try to recover the data
|
||||||
|
Some(poisoned.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup function to restore cursors and clean up resources
|
// Cleanup function to restore cursors and clean up resources
|
||||||
pub fn cleanup() {
|
pub fn cleanup() {
|
||||||
println!("Running cursor cleanup...");
|
// Check if we have original cursors saved before attempting restoration
|
||||||
|
let _has_original_cursors = {
|
||||||
|
if let Some(guard) = safe_lock(&ORIGINAL_CURSORS, "checking original cursors") {
|
||||||
|
guard.is_some()
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to check original cursors during cleanup");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Restore system cursors if they were hidden
|
// Restore system cursors if they were hidden
|
||||||
// Use swap to ensure it runs only once
|
// Use swap to ensure it runs only once
|
||||||
if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) {
|
if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) {
|
||||||
println!("Restoring system cursors...");
|
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// Try multiple approaches to ensure cursors are restored
|
// Try multiple approaches to ensure cursors are restored
|
||||||
|
|
||||||
// 1. First try to restore original cursors if we saved them
|
// 1. First try to restore original cursors if we saved them
|
||||||
let original_cursors_opt = ORIGINAL_CURSORS.lock().unwrap().take();
|
let original_cursors_opt = {
|
||||||
|
if let Some(mut guard) = safe_lock(&ORIGINAL_CURSORS, "restoring original cursors") {
|
||||||
|
guard.take()
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to access original cursors for restoration");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(original_cursors) = original_cursors_opt {
|
if let Some(original_cursors) = original_cursors_opt {
|
||||||
println!("Restoring {} saved original cursors", original_cursors.len());
|
|
||||||
for (cursor_id, cursor) in original_cursors {
|
for (cursor_id, cursor) in original_cursors {
|
||||||
if !cursor.0.is_null() {
|
if !cursor.0.is_null() {
|
||||||
// Make a copy of the original cursor
|
// Make a copy of the original cursor
|
||||||
@@ -232,57 +330,68 @@ pub fn cleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("No saved original cursors found");
|
|
||||||
|
// Try to restore each system cursor to its default
|
||||||
|
for &cursor_id in SYSTEM_CURSORS.iter() {
|
||||||
|
let default_cursor =
|
||||||
|
LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16));
|
||||||
|
if !default_cursor.is_null() {
|
||||||
|
let cursor_copy = CopyIcon(default_cursor);
|
||||||
|
if !cursor_copy.is_null() {
|
||||||
|
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
|
||||||
|
eprintln!("Failed to restore default cursor for ID: {}", cursor_id);
|
||||||
|
DestroyIcon(cursor_copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Force Windows to reload default cursor settings
|
// Use the shared cursor restoration logic
|
||||||
println!("Forcing Windows to reload default cursor settings");
|
restore_default_cursor_internal();
|
||||||
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
|
|
||||||
|
|
||||||
// 3. Also try the A version for maximum compatibility
|
|
||||||
super::state::reset_system_cursors();
|
|
||||||
|
|
||||||
// 4. Reset to default arrow cursor
|
|
||||||
let arrow_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
|
|
||||||
if !arrow_cursor.is_null() {
|
|
||||||
SetCursor(arrow_cursor);
|
|
||||||
println!("Reset to default arrow cursor.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("System cursors restoration complete.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy the custom cursor if it exists
|
// Destroy the custom cursor if it exists
|
||||||
let custom_cursor_to_destroy = CUSTOM_CURSOR.lock().unwrap().take();
|
let custom_cursor_to_destroy = if let Some(mut guard) = safe_lock(&CUSTOM_CURSOR, "destroying custom cursor") {
|
||||||
|
guard.take()
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to access custom cursor for cleanup");
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(cursor_wrapper) = custom_cursor_to_destroy {
|
if let Some(cursor_wrapper) = custom_cursor_to_destroy {
|
||||||
unsafe {
|
unsafe {
|
||||||
if !cursor_wrapper.0.is_null() {
|
if !cursor_wrapper.0.is_null() {
|
||||||
DestroyIcon(cursor_wrapper.0);
|
DestroyIcon(cursor_wrapper.0);
|
||||||
println!("Custom cursor destroyed during cleanup.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the last known cursor position
|
// Reset the last known cursor position
|
||||||
*LAST_CURSOR_POS.lock().unwrap() = None;
|
if let Some(mut guard) = safe_lock(&LAST_CURSOR_POS, "resetting cursor position") {
|
||||||
|
*guard = None;
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to reset cursor position during cleanup");
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the current cursor image
|
// Clear the current cursor image
|
||||||
*CURRENT_CURSOR_IMAGE.lock().unwrap() = None;
|
if let Some(mut guard) = safe_lock(&CURRENT_CURSOR_IMAGE, "clearing cursor image") {
|
||||||
|
*guard = None;
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to clear cursor image during cleanup");
|
||||||
|
}
|
||||||
|
|
||||||
// Unregister control handler
|
// Unregister control handler
|
||||||
if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) {
|
if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) {
|
||||||
unsafe {
|
unsafe {
|
||||||
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 {
|
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 {
|
||||||
CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst);
|
CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst);
|
||||||
println!("Control handler unregistered.");
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to unregister control handler.");
|
eprintln!("Failed to unregister control handler.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Cursor cleanup complete.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Public API Functions ---
|
// --- Public API Functions ---
|
||||||
@@ -295,9 +404,11 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
unsafe {
|
unsafe {
|
||||||
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
|
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
|
||||||
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
|
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
|
||||||
println!("Control handler registered for clean exit.");
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to register control handler: {}", std::io::Error::last_os_error());
|
eprintln!(
|
||||||
|
"Failed to register control handler: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -312,17 +423,26 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
unsafe {
|
unsafe {
|
||||||
for &cursor_id in SYSTEM_CURSORS.iter() {
|
for &cursor_id in SYSTEM_CURSORS.iter() {
|
||||||
// Get the original cursor
|
// Get the original cursor
|
||||||
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
|
let original_cursor =
|
||||||
|
CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
|
||||||
if !original_cursor.is_null() {
|
if !original_cursor.is_null() {
|
||||||
// Save the original cursor for restoration
|
// Save the original cursor for restoration
|
||||||
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
|
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to save original cursor for ID: {}", cursor_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store original cursors for restoration
|
// Store original cursors for restoration
|
||||||
if !original_cursors.is_empty() {
|
if !original_cursors.is_empty() {
|
||||||
*ORIGINAL_CURSORS.lock().unwrap() = Some(original_cursors);
|
if let Some(mut guard) = safe_lock(&ORIGINAL_CURSORS, "storing original cursors") {
|
||||||
|
*guard = Some(original_cursors);
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to store original cursors for restoration");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("No original cursors were saved!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as hidden - this is now just a flag to indicate we've saved the original cursors
|
// Mark as hidden - this is now just a flag to indicate we've saved the original cursors
|
||||||
@@ -332,66 +452,38 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the cursor image using CursorInfo (with caching)
|
// Update the cursor image using CursorInfo (with caching)
|
||||||
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool {
|
pub fn update_cursor_info(image: &RgbaImage) -> bool {
|
||||||
println!("Updating cursor to: {}", cursor_info.name);
|
|
||||||
|
|
||||||
// Use cached image if available, otherwise load from disk
|
|
||||||
let image: image::RgbaImage = if let Some(ref cached_image) = cursor_info.image {
|
|
||||||
println!("Using cached image for cursor: {}", cursor_info.name);
|
|
||||||
let img_clone: image::RgbaImage = cached_image.clone();
|
|
||||||
img_clone
|
|
||||||
} else {
|
|
||||||
println!("Loading image from disk for cursor: {}", cursor_info.name);
|
|
||||||
match image_open(&cursor_info.path) {
|
|
||||||
Ok(img) => {
|
|
||||||
let rgba = img.to_rgba8();
|
|
||||||
// Cache the image for future use
|
|
||||||
// Make sure to specify the type to avoid type inference issues
|
|
||||||
let image_copy: image::RgbaImage = rgba.clone();
|
|
||||||
cursor_info.image = Some(image_copy);
|
|
||||||
rgba
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to load cursor image: {}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the image in the global state for reference
|
// Store the image in the global state for reference
|
||||||
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone());
|
if let Some(mut guard) = safe_lock(&CURRENT_CURSOR_IMAGE, "storing cursor image") {
|
||||||
|
*guard = Some(image.clone());
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to store cursor image in global state");
|
||||||
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// Create a new cursor first before destroying the old one
|
// Create a new cursor first before destroying the old one
|
||||||
// This minimizes the time when no custom cursor is active
|
// This minimizes the time when no custom cursor is active
|
||||||
let new_cursor = if let Some(ref cached_image) = cursor_info.image {
|
let new_cursor = create_cursor_from_image(&image);
|
||||||
// Always create a fresh cursor from the image to avoid invalid handle issues
|
|
||||||
create_cursor_from_image(cached_image)
|
|
||||||
} else {
|
|
||||||
create_cursor_from_image(&image)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(new_cursor_wrapper) = new_cursor {
|
if let Some(new_cursor_wrapper) = new_cursor {
|
||||||
// Apply the new cursor to all system cursors first
|
// Apply the new cursor to all system cursors first
|
||||||
let success = apply_cursor_to_all_system_cursors(new_cursor_wrapper.0);
|
let success = apply_cursor_to_all_system_cursors(new_cursor_wrapper.0);
|
||||||
|
|
||||||
// Now that the new cursor is applied, we can clean up the old one
|
// Now that the new cursor is applied, we can clean up the old one
|
||||||
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
|
if let Some(mut cursor_guard) = safe_lock(&CUSTOM_CURSOR, "updating custom cursor") {
|
||||||
if let Some(old_cursor) = cursor_guard.take() {
|
if let Some(old_cursor) = cursor_guard.take() {
|
||||||
// Only destroy the old cursor after the new one is applied
|
// Only destroy the old cursor after the new one is applied
|
||||||
DestroyIcon(old_cursor.0);
|
DestroyIcon(old_cursor.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the new cursor
|
// Store the new cursor
|
||||||
*cursor_guard = Some(new_cursor_wrapper);
|
*cursor_guard = Some(new_cursor_wrapper);
|
||||||
|
} else {
|
||||||
// Update the cached cursor in CursorInfo
|
eprintln!("Failed to update custom cursor - mutex lock failed");
|
||||||
#[cfg(windows)]
|
// Clean up the new cursor since we can't store it
|
||||||
{
|
DestroyIcon(new_cursor_wrapper.0);
|
||||||
// Convert SyncHCURSOR to WinCursorWrapper for storage in cursor_info
|
return false;
|
||||||
cursor_info.win_cursor = Some(crate::platform::types::WinCursorWrapper {
|
|
||||||
handle: new_cursor_wrapper.0
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
@@ -403,27 +495,41 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to create cursor from image");
|
eprintln!("Failed to create cursor from image");
|
||||||
|
|
||||||
// If we failed to create a new cursor, try to use the cached one as a fallback
|
|
||||||
#[cfg(windows)]
|
|
||||||
if let Some(cached_cursor) = &cursor_info.win_cursor {
|
|
||||||
if !cached_cursor.handle.is_null() {
|
|
||||||
println!("Falling back to cached cursor as a last resort");
|
|
||||||
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
|
|
||||||
// Convert WinCursorWrapper to SyncHCURSOR
|
|
||||||
let sync_cursor = super::types::SyncHCURSOR(cached_cursor.handle);
|
|
||||||
*cursor_guard = Some(sync_cursor);
|
|
||||||
SetCursor(cached_cursor.handle);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to apply a cursor to a specific system cursor ID
|
||||||
|
unsafe fn apply_cursor_to_system_cursor(cursor: winapi::shared::windef::HCURSOR, cursor_id: DWORD, log_errors: bool) -> bool {
|
||||||
|
// Make a copy of the cursor
|
||||||
|
let cursor_copy = CopyIcon(cursor);
|
||||||
|
if cursor_copy.is_null() {
|
||||||
|
if log_errors {
|
||||||
|
let error = std::io::Error::last_os_error();
|
||||||
|
eprintln!(
|
||||||
|
"Failed to copy cursor for ID: {} - Error: {}",
|
||||||
|
cursor_id, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the system cursor
|
||||||
|
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
|
||||||
|
if log_errors {
|
||||||
|
let error = std::io::Error::last_os_error();
|
||||||
|
eprintln!(
|
||||||
|
"Failed to set cursor for ID: {} - Error: {}",
|
||||||
|
cursor_id, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
// Apply a cursor to all system cursors
|
// Apply a cursor to all system cursors
|
||||||
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
|
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
|
||||||
@@ -446,45 +552,21 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
|
|||||||
32649, // IDC_HAND (links)
|
32649, // IDC_HAND (links)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Apply to priority cursors first
|
// Apply to priority cursors first (with error logging)
|
||||||
for &cursor_id in priority_cursors.iter() {
|
for &cursor_id in priority_cursors.iter() {
|
||||||
// Make a copy of the cursor
|
if !apply_cursor_to_system_cursor(cursor, cursor_id, true) {
|
||||||
let cursor_copy = CopyIcon(cursor);
|
|
||||||
if cursor_copy.is_null() {
|
|
||||||
let error = std::io::Error::last_os_error();
|
|
||||||
eprintln!("Failed to copy cursor for priority ID: {} - Error: {}", cursor_id, error);
|
|
||||||
success = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the system cursor
|
|
||||||
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
|
|
||||||
let error = std::io::Error::last_os_error();
|
|
||||||
eprintln!("Failed to set cursor for priority ID: {} - Error: {}", cursor_id, error);
|
|
||||||
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
|
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now apply to the remaining cursors
|
// Now apply to the remaining cursors (without error logging to reduce spam)
|
||||||
for &cursor_id in SYSTEM_CURSORS.iter() {
|
for &cursor_id in SYSTEM_CURSORS.iter() {
|
||||||
// Skip the priority cursors we already processed
|
// Skip the priority cursors we already processed
|
||||||
if priority_cursors.contains(&cursor_id) {
|
if priority_cursors.contains(&cursor_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a copy of the cursor
|
if !apply_cursor_to_system_cursor(cursor, cursor_id, false) {
|
||||||
let cursor_copy = CopyIcon(cursor);
|
|
||||||
if cursor_copy.is_null() {
|
|
||||||
// Don't log errors for non-priority cursors to reduce console spam
|
|
||||||
success = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the system cursor
|
|
||||||
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
|
|
||||||
// Don't log errors for non-priority cursors to reduce console spam
|
|
||||||
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
|
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,7 +579,29 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
|
|||||||
success
|
success
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the system cursor
|
// Helper function for common cursor restoration logic
|
||||||
|
unsafe fn restore_default_cursor_internal() {
|
||||||
|
// Force Windows to reload default cursor settings
|
||||||
|
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
|
||||||
|
|
||||||
|
// Also try the A version for maximum compatibility
|
||||||
|
super::state::reset_system_cursors();
|
||||||
|
|
||||||
|
// Reset to default arrow cursor
|
||||||
|
let arrow_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
|
||||||
|
if !arrow_cursor.is_null() {
|
||||||
|
SetCursor(arrow_cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the system cursor temporarily (without affecting cleanup state)
|
||||||
|
pub fn restore_cursor_temporarily() {
|
||||||
|
unsafe {
|
||||||
|
restore_default_cursor_internal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the system cursor permanently (full cleanup)
|
||||||
pub fn restore_cursor() {
|
pub fn restore_cursor() {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,27 +4,16 @@ pub mod types;
|
|||||||
mod cursor;
|
mod cursor;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use image::RgbaImage;
|
||||||
use crate::platform::Result;
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
// Functions expected by the platform-agnostic interface
|
// Functions expected by the platform-agnostic interface
|
||||||
pub fn set_cursor(path: &str) -> Result<()> {
|
pub fn set_cursor(image: &RgbaImage) -> Result<()> {
|
||||||
println!("Windows platform: setting cursor to {}", path);
|
|
||||||
|
|
||||||
// Create a cursor info struct to pass to update_cursor_info
|
|
||||||
let mut cursor_info = crate::platform::types::CursorInfo {
|
|
||||||
name: path.split('\\').last().unwrap_or("unknown").to_string(),
|
|
||||||
path: PathBuf::from(path),
|
|
||||||
image: None,
|
|
||||||
hotspot_x: 0, // Default hotspot at top-left
|
|
||||||
hotspot_y: 0,
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
win_cursor: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the existing update_cursor_info function to handle loading and applying the cursor
|
// Use the existing update_cursor_info function to handle loading and applying the cursor
|
||||||
if !cursor::update_cursor_info(&mut cursor_info) {
|
if !cursor::update_cursor_info(image) {
|
||||||
return Err(format!("Failed to update cursor from {}", path));
|
return Err(anyhow!("Failed to update cursor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -36,10 +25,16 @@ pub fn restore_default_cursor() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn restore_default_cursor_temporarily() -> Result<()> {
|
||||||
|
println!("Windows platform: temporarily restoring default cursor");
|
||||||
|
cursor::restore_cursor_temporarily();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init() -> Result<()> {
|
pub fn init() -> Result<()> {
|
||||||
println!("Windows platform: initializing cursor system");
|
println!("Windows platform: initializing cursor system");
|
||||||
if !cursor::hide_system_cursor() {
|
if !cursor::hide_system_cursor() {
|
||||||
return Err("Failed to initialize cursor system".to_string());
|
return Err(anyhow!("Failed to initialize cursor system"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,24 @@ use winapi::{
|
|||||||
// --- Thread-Safe Handle Wrappers ---
|
// --- Thread-Safe Handle Wrappers ---
|
||||||
// These wrappers make raw Windows handles safe to share between threads
|
// These wrappers make raw Windows handles safe to share between threads
|
||||||
|
|
||||||
// Wrapper for HCURSOR (cursor handle)
|
// Wrapper for HCURSOR (cursor handle) that implements Send + Sync
|
||||||
|
//
|
||||||
|
// SAFETY: HCURSOR is a Windows handle that represents a cursor resource.
|
||||||
|
// Windows cursor handles are:
|
||||||
|
// 1. Reference-counted by the Windows kernel
|
||||||
|
// 2. Safe to share between threads (multiple threads can hold the same handle)
|
||||||
|
// 3. Safe to send between threads (the handle value itself is just a pointer-sized integer)
|
||||||
|
// 4. Immutable once created (cursor resources don't change after creation)
|
||||||
|
//
|
||||||
|
// The Windows API documentation confirms that cursor handles can be used from any thread,
|
||||||
|
// and the kernel manages the underlying resource lifetime through reference counting.
|
||||||
|
// Therefore, it's safe to implement Send + Sync for this wrapper.
|
||||||
|
//
|
||||||
|
// NOTE: The caller is responsible for ensuring proper cleanup (calling DestroyIcon)
|
||||||
|
// to avoid resource leaks, but this doesn't affect thread safety.
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct SyncHCURSOR(pub HCURSOR);
|
pub struct SyncHCURSOR(pub HCURSOR);
|
||||||
|
|
||||||
unsafe impl Send for SyncHCURSOR {}
|
unsafe impl Send for SyncHCURSOR {}
|
||||||
unsafe impl Sync for SyncHCURSOR {}
|
unsafe impl Sync for SyncHCURSOR {}
|
||||||
|
|
||||||
|
|||||||
@@ -26,34 +26,56 @@ function App() {
|
|||||||
// Function to minimize app to tray
|
// Function to minimize app to tray
|
||||||
const minimizeToTray = useCallback(async () => {
|
const minimizeToTray = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Minimizing app to tray');
|
|
||||||
// First hide the window to ensure it disappears immediately
|
// First hide the window to ensure it disappears immediately
|
||||||
await appWindow.hide();
|
await appWindow.hide();
|
||||||
console.log('Window hidden, now ensuring tray is visible');
|
|
||||||
|
|
||||||
// Then call the backend to ensure the tray is visible
|
// Then call the backend to ensure the tray is visible
|
||||||
await invoke("minimize_to_tray");
|
await invoke("minimize_to_tray");
|
||||||
console.log('minimize_to_tray command completed successfully');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to minimize to tray:", error);
|
console.error("Failed to minimize to tray:", error);
|
||||||
// Fallback to window API if the invoke fails
|
// Fallback to window API if the invoke fails
|
||||||
try {
|
try {
|
||||||
console.log('Trying fallback hide');
|
|
||||||
await appWindow.hide();
|
await appWindow.hide();
|
||||||
console.log('Fallback hide succeeded');
|
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error("Fallback hide also failed:", fallbackError);
|
console.error("Fallback hide also failed:", fallbackError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Function to handle window focus/show events to re-apply cursor
|
||||||
|
useEffect(() => {
|
||||||
|
const setupFocusHandler = async () => {
|
||||||
|
const unlistenFocus = await appWindow.onFocusChanged(async ({ payload: focused }) => {
|
||||||
|
if (focused) {
|
||||||
|
try {
|
||||||
|
await invoke("reapply_custom_cursor");
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to re-apply cursor on focus:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return unlistenFocus;
|
||||||
|
};
|
||||||
|
|
||||||
|
let unlistenFocusFn;
|
||||||
|
setupFocusHandler().then(unlisten => {
|
||||||
|
unlistenFocusFn = unlisten;
|
||||||
|
}).catch(err => console.error('Failed to set up focus handler:', err));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unlistenFocusFn) {
|
||||||
|
unlistenFocusFn();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Load cursors on mount and set up event listeners
|
// Load cursors on mount and set up event listeners
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCursors = async () => {
|
const loadCursors = async () => {
|
||||||
try {
|
try {
|
||||||
// Get cursors from the Tauri backend
|
// Get cursors from the Tauri backend
|
||||||
const loadedCursors = await invoke("get_cursors");
|
const loadedCursors = await invoke("get_cursors");
|
||||||
console.log("Loaded cursors:", loadedCursors);
|
|
||||||
setCursors(loadedCursors || []);
|
setCursors(loadedCursors || []);
|
||||||
|
|
||||||
// Get current cursor index
|
// Get current cursor index
|
||||||
@@ -80,23 +102,24 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setupCloseHandler = async () => {
|
const setupCloseHandler = async () => {
|
||||||
const unlisten = await appWindow.onCloseRequested(async (event) => {
|
const unlisten = await appWindow.onCloseRequested(async (event) => {
|
||||||
console.log('Window close requested');
|
|
||||||
|
|
||||||
if (settings.minimizeToTrayOnClose) {
|
if (settings.minimizeToTrayOnClose) {
|
||||||
console.log('Minimize to tray setting is enabled, preventing close and hiding instead');
|
|
||||||
// Prevent the window from closing
|
// Prevent the window from closing
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Temporarily restore cursor before hiding to tray
|
||||||
|
try {
|
||||||
|
await invoke("temporarily_restore_cursor");
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore cursor before hiding:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Hide the window instead
|
// Hide the window instead
|
||||||
try {
|
try {
|
||||||
console.log('Calling minimizeToTray function');
|
|
||||||
await minimizeToTray();
|
await minimizeToTray();
|
||||||
console.log('Window should now be hidden and app should be in system tray');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to minimize to tray on close:', error);
|
console.error('Failed to minimize to tray on close:', error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Minimize to tray setting is disabled, allowing window to close');
|
|
||||||
// Let the window close normally, but we need to call quit_app to ensure proper cleanup
|
// Let the window close normally, but we need to call quit_app to ensure proper cleanup
|
||||||
try {
|
try {
|
||||||
await invoke("quit_app");
|
await invoke("quit_app");
|
||||||
@@ -112,7 +135,6 @@ function App() {
|
|||||||
let unlistenFn;
|
let unlistenFn;
|
||||||
setupCloseHandler().then(unlisten => {
|
setupCloseHandler().then(unlisten => {
|
||||||
unlistenFn = unlisten;
|
unlistenFn = unlisten;
|
||||||
console.log('Close handler set up successfully');
|
|
||||||
}).catch(err => console.error('Failed to set up close handler:', err));
|
}).catch(err => console.error('Failed to set up close handler:', err));
|
||||||
|
|
||||||
// Clean up event listener on unmount
|
// Clean up event listener on unmount
|
||||||
@@ -126,7 +148,6 @@ function App() {
|
|||||||
// Functions to handle cursor operations using Tauri backend
|
// Functions to handle cursor operations using Tauri backend
|
||||||
const selectCursor = async (index) => {
|
const selectCursor = async (index) => {
|
||||||
try {
|
try {
|
||||||
console.log(`Selecting cursor at index ${index}`);
|
|
||||||
await invoke("select_cursor", { index });
|
await invoke("select_cursor", { index });
|
||||||
setCurrentCursorIndex(index);
|
setCurrentCursorIndex(index);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -138,7 +159,6 @@ function App() {
|
|||||||
if (cursors.length === 0) return;
|
if (cursors.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Switching to next cursor');
|
|
||||||
const newIndex = await invoke("next_cursor");
|
const newIndex = await invoke("next_cursor");
|
||||||
setCurrentCursorIndex(newIndex);
|
setCurrentCursorIndex(newIndex);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -152,7 +172,6 @@ function App() {
|
|||||||
// Function to exit app
|
// Function to exit app
|
||||||
const quitApp = async () => {
|
const quitApp = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Quitting app');
|
|
||||||
await invoke("quit_app");
|
await invoke("quit_app");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to quit app:", error);
|
console.error("Failed to quit app:", error);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: transparent; /* Reverted to original */
|
background-color: transparent; /* Reverted to original */
|
||||||
/* cursor: move; */ /* Removed, should be handled by drag region or default */
|
-webkit-app-region: drag; /* Ensure the header is draggable */
|
||||||
|
/* cursor: move; */ /* Let the system handle cursor during drag operations */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
@@ -64,6 +65,16 @@
|
|||||||
color: rgba(205, 214, 244, 0.8);
|
color: rgba(205, 214, 244, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dedicated drag area */
|
||||||
|
.drag-area {
|
||||||
|
flex: 1; /* Take up remaining space */
|
||||||
|
height: 100%;
|
||||||
|
min-width: 50px; /* Ensure there's always some draggable space */
|
||||||
|
-webkit-app-region: drag; /* Explicitly set as draggable */
|
||||||
|
/* cursor: move; */ /* Let the system handle cursor during drag operations */
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Window control buttons */
|
/* Window control buttons */
|
||||||
.window-controls {
|
.window-controls {
|
||||||
-webkit-app-region: no-drag; /* Override drag region for buttons */
|
-webkit-app-region: no-drag; /* Override drag region for buttons */
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { appWindow } from '@tauri-apps/api/window';
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import {
|
import {
|
||||||
faGripVertical,
|
faGripVertical,
|
||||||
faBoxOpen,
|
faBoxOpen,
|
||||||
@@ -36,8 +38,29 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle window dragging programmatically as a fallback
|
||||||
|
const handleMouseDown = async (e) => {
|
||||||
|
// Only start dragging on left mouse button and if not clicking on interactive elements
|
||||||
|
if (e.button === 0 && !e.target.closest('.tab, .window-controls')) {
|
||||||
|
try {
|
||||||
|
// Temporarily restore system cursor for dragging
|
||||||
|
await invoke('temporarily_restore_cursor');
|
||||||
|
await appWindow.startDragging();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start window dragging:', error);
|
||||||
|
} finally {
|
||||||
|
// Re-apply custom cursor after dragging
|
||||||
|
try {
|
||||||
|
await invoke('reapply_custom_cursor');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to re-apply custom cursor:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="app-header" data-tauri-drag-region>
|
<header className="app-header" data-tauri-drag-region onMouseDown={handleMouseDown}>
|
||||||
<div className="tabs-container">
|
<div className="tabs-container">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
@@ -49,6 +72,10 @@ function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }
|
|||||||
<FontAwesomeIcon icon={item.icon} />
|
<FontAwesomeIcon icon={item.icon} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Dedicated drag area */}
|
||||||
|
<div className="drag-area" title="Drag to move window"></div>
|
||||||
|
|
||||||
<div className="window-controls">
|
<div className="window-controls">
|
||||||
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
|
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
|
||||||
_
|
_
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
.setting-item {
|
.setting-item {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-item label {
|
.setting-item label {
|
||||||
@@ -73,3 +75,31 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.restore-button {
|
||||||
|
background-color: var(--accent-color, #89b4fa);
|
||||||
|
color: var(--bg-color, #1e1e2e);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restore-button:hover {
|
||||||
|
background-color: var(--accent-hover, #74c7ec);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.restore-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-description {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-color, #cdd6f4);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { invoke } from "@tauri-apps/api/tauri";
|
||||||
import './Settings.css';
|
import './Settings.css';
|
||||||
|
|
||||||
function Settings({ settings, setSettings }) {
|
function Settings({ settings, setSettings }) {
|
||||||
@@ -10,6 +11,19 @@ function Settings({ settings, setSettings }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handler for restoring default cursors
|
||||||
|
const handleRestoreDefaultCursors = async () => {
|
||||||
|
try {
|
||||||
|
console.log('Manually restoring default cursors');
|
||||||
|
await invoke("restore_default_cursors");
|
||||||
|
console.log('Default cursors restored successfully');
|
||||||
|
alert('Default cursors restored successfully!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore default cursors:', error);
|
||||||
|
alert('Failed to restore default cursors: ' + error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-container">
|
<div className="settings-container">
|
||||||
<h2 className="content-title">Settings</h2>
|
<h2 className="content-title">Settings</h2>
|
||||||
@@ -50,6 +64,17 @@ function Settings({ settings, setSettings }) {
|
|||||||
Auto-apply cursor on selection
|
Auto-apply cursor on selection
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="setting-item">
|
||||||
|
<button
|
||||||
|
onClick={handleRestoreDefaultCursors}
|
||||||
|
className="restore-button"
|
||||||
|
>
|
||||||
|
Restore Default Cursors
|
||||||
|
</button>
|
||||||
|
<p className="setting-description">
|
||||||
|
Manually restore the system's default cursors if they're not working properly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-group">
|
<div className="settings-group">
|
||||||
|
|||||||
@@ -3,7 +3,5 @@ import ReactDOM from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<App />
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
);
|
||||||
|
|||||||