Author SHA1 Message Date
Joby 9f733ed39b Integrate Tauri frontend with categorization system and Steam integration 2025-05-17 21:07:31 -05:00
Joby 0133b61944 updated readme with run directions 2025-05-17 17:25:50 -05:00
Hemeleh 20592954f7 tauri integration 2025-05-16 17:44:04 +02:00
Hemeleh 06b2f0ffaf tauri integration 2025-05-16 17:38:33 +02:00
Joby 2e71cd8b9e Updated README 2025-05-11 18:52:52 -05:00
Joby a92ef1e07c Fix cursor swapping issues and minimize cursor flashing 2025-05-11 18:48:04 -05:00
Joby b3d85106a8 Implement cursor asset caching for improved performance
Key improvements:
1. Add image caching to CursorInfo struct to avoid repeated disk reads and decoding
2. Implement Windows cursor handle caching to minimize GDI resource allocation
3. Update all code paths to use cached resources for better performance
4. Remove deprecated/unused functions for cleaner codebase

This change significantly reduces CPU usage and improves cursor switching performance.
2025-05-11 18:37:23 -05:00
Joby 34b0d700a6 Replace busy-wait hotkey listener with efficient event-driven approach 2025-05-11 18:21:50 -05:00
Joby 0e6841bd8a Clean up dead code and fix warnings while preserving Steam drop functionality and cross-platform skeleton code 2025-05-11 18:11:58 -05:00
Joby 9f6f3442dc unclean implimentation of windows cursor set as image 2025-05-10 23:42:25 -05:00
Joby a8d8d2ea0b Fixed close hotkey not working so it works now 2025-05-09 22:10:03 -05:00
Joby 49226cf5c5 Reorganize codebase into a more modular structure 2025-05-09 21:47:38 -05:00
Joby f464052bda Remove old files that have been moved to modular structure 2025-05-09 21:37:29 -05:00
Joby cf1c90856f Reorganize codebase into modular structure 2025-05-09 21:37:15 -05:00
74 changed files with 7674 additions and 1885 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"git.ignoreLimitWarning": true
}
-27
View File
@@ -1,27 +0,0 @@
[package]
name = "cursor_rush"
version = "0.1.0"
edition = "2021"
[dependencies]
iced = { version = "0.13.1", default-features = true, features = ["image", "advanced"] } # GUI, bundles wgpu + winit
tray-icon = "0.20.1" # cross‑platform system tray
image = "0.24.7" # Image processing
walkdir = "2.4.0" # Directory traversal
global-hotkey = "0.4.2" # Global hotkey registration
lazy_static = "1.4.0" # Thread-safe static initialization
steamworks = { version = "0.11.0", features = ["raw-bindings"] }
anyhow = "1.0.98"
once_cell = "1.18.0" # Thread-safe lazy initialized global variables
# Platform-specific dependencies
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser", "wingdi", "consoleapi"] } # Windows API
ctor = "0.2.4" # Constructor/destructor attributes
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7" # Objective-C bindings
cocoa = "0.24.1" # macOS Cocoa bindings
[target.'cfg(target_os = "linux")'.dependencies]
x11 = { version = "2.21.0", features = ["xlib"] } # X11 bindings
+148
View File
@@ -0,0 +1,148 @@
# Cursor Rush
A cross-platform application for customizing your cursor with gameified logic.
## Features
- Replace the system cursor with custom PNG images
- Minimize to system tray for background operation
- Windows support (macOS and Linux support in development)
- Optimized rendering with low CPU usage
- System tray integration with context menu
- Global hotkey support
- Steam integration for item drops
- Tauri-based frontend with React
## Project Structure
```
frontend/
├── src/ # React frontend code
│ ├── App.jsx # Main React component
│ ├── components/ # UI components
│ └── cursors/ # Cursor image files
├── src-tauri/ # Rust backend code
│ ├── src/ # Rust source code
│ │ ├── lib.rs # Main library code
│ │ ├── main.rs # Application entry point
│ │ ├── integrations/ # External integrations
│ │ │ ├── mod.rs # Integrations module exports
│ │ │ └── steam.rs # Steam integration
│ │ └── platform/ # Platform-specific code
│ │ ├── mod.rs # Platform module exports
│ │ ├── types.rs # Common type definitions
│ │ ├── windows/ # Windows-specific code
│ │ ├── macos/ # macOS-specific code
│ │ └── linux/ # Linux-specific code
│ └── Cargo.toml # Rust dependencies
└── package.json # npm dependencies
```
## Building
### Prerequisites
- Rust 1.70.0 or later
- Cargo
- Node.js 16.0.0 or later
- npm 7.0.0 or later
- Platform-specific dependencies:
- Windows: None (fully supported)
- macOS: Development in progress
- Linux: Development in progress
### Dependencies
The application uses the following major dependencies:
- Tauri 1.6.0 - Cross-platform framework
- React - Frontend framework
- image 0.24.7 - Image processing
- winapi 0.3.9 - Windows API bindings
- once_cell 1.19.0 - Lazy initialization
### Build Commands
```bash
# Navigate to the frontend directory
cd frontend
# Install npm dependencies
npm install
# Run in development mode
npm run tauri dev
# Build for production
npm run tauri build
```
## Usage
1. Make sure you have Node.js and npm installed
2. Navigate to the frontend directory: `cd frontend`
3. Install dependencies if you haven't already: `npm install`
4. Run the application in development mode: `npm run tauri dev`
5. The application will start with a React frontend and Rust backend
6. Use the UI to browse and select cursors
7. The application will replace your system cursor with the selected image
8. To exit, use the "Quit" button in the application or from the system tray
### Custom Cursors
The application comes with sample cursors:
- `default.png` - A simple default cursor
- `icon.png` - An icon cursor
- `red.png` - A red-colored cursor
To add your own cursors:
1. Create PNG images with transparent backgrounds
2. Place them in the `frontend/src/cursors` directory
3. Restart the application to load the new cursors
4. The cursor name in the UI will be derived from the filename
## Hotkeys
- Ctrl+Shift+F12: Exit the application (restores original system cursor)
- If this hotkey fails, the application will try to use Ctrl+Alt+F12 or F12 as fallbacks
## System Tray
- Left-click: Show the application window
- Right-click: Open the context menu with the following options:
- Next cursor: Switch to the next cursor in the list
- Show window: Show the application window
- Quit: Exit the application and restore the original cursor
## Performance
- The application uses optimized rendering techniques to minimize CPU usage
- On Windows, it uses the Windows API to efficiently replace system cursors
- The application uses event-driven cursor updates instead of polling when possible
- Cursor images are cached to improve performance when switching between cursors
## Previously Fixed Issues
The following issues have been addressed in recent updates:
- Task Manager compatibility: The application now properly handles elevated applications
- System tray menu stability: Fixed issues with the tray menu disappearing when mousing over it
## Steam Integration
The application includes integration with the Steam API for future gameified features:
- Item drop functionality is implemented but not yet active
- Steam must be running for this functionality to work
## Contributing
Contributions to Cursor Rush are welcome! Here are some ways you can contribute:
- Report bugs and issues
- Suggest new features or improvements
- Submit pull requests with bug fixes or new features
- Help with documentation
- Contribute to macOS and Linux platform support
- Improve the Tauri integration
## License
This project is currently in development and does not yet have a specified license. A license will be added in a future update.
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+12
View File
@@ -0,0 +1,12 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+33
View File
@@ -0,0 +1,33 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+3758
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@tauri-apps/api": "^1.6.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@tauri-apps/cli": "^1.6.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.5"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "cursor-rush"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.56"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.2", features = [] }
[dependencies]
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
image = "0.24"
# Ensure "consoleapi" is in this features list
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
once_cell = "1.19.0"
steamworks = "0.10.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

@@ -0,0 +1,3 @@
// External integrations module
pub mod steam;
@@ -0,0 +1,26 @@
// Steam integration
use steamworks::{AppId, Client, SingleClient};
#[allow(dead_code)]
pub struct Steam {
client: Client,
single: SingleClient,
}
impl Steam {
pub fn new() -> Self {
let (client, single) = Client::init_app(AppId(1430270)).unwrap();
Self { client, single }
}
#[allow(dead_code)]
pub fn drop_item(&self) {
// Placeholder for item drop functionality
// The actual implementation would use the Steam API to trigger an item drop
println!("Steam item drop triggered (placeholder)");
// In a real implementation with the correct API version, we would use:
// self.client.get_inventory().trigger_item_drop(item_def_id);
}
}
+432
View File
@@ -0,0 +1,432 @@
use std::sync::Mutex;
use serde::{Serialize, Deserialize};
use tauri::{Manager, AppHandle, CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent, WindowEvent};
use std::path::{Path, PathBuf};
use std::env;
mod platform;
mod integrations;
// Define cursor structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursor {
name: String,
path: String,
}
// App state to track cursors
pub struct AppState {
cursors: Vec<Cursor>,
current_index: usize,
}
// Thread-safe app state wrapper
pub struct AppStateWrapper(Mutex<AppState>);
// Command to get all cursors
#[tauri::command]
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
let app_state = state.0.lock().unwrap();
app_state.cursors.clone()
}
// Command to get current cursor index
#[tauri::command]
fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
let app_state = state.0.lock().unwrap();
app_state.current_index
}
// Command to select a cursor
#[tauri::command]
fn select_cursor(index: usize, state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<(), String> {
println!("=== RESOURCE DEBUG [select_cursor] ===");
let mut app_state = state.0.lock().unwrap();
if index < app_state.cursors.len() {
app_state.current_index = index;
let cursor = &app_state.cursors[index];
println!("Cursor selected: {}", cursor.name);
println!("Original path: {}", cursor.path);
// Get the resource directory
let resource_dir = app_handle.path_resolver().resource_dir();
println!("Resource dir: {:?}", resource_dir);
// List contents of resource dir
if let Some(res_dir) = &resource_dir {
println!("Listing contents of resource dir: {}", res_dir.display());
if let Ok(entries) = std::fs::read_dir(res_dir) {
for (i, entry) in entries.filter_map(Result::ok).enumerate() {
println!(" {}: {}", i, entry.path().display());
// List subdirectories
if entry.path().is_dir() {
if let Ok(subentries) = std::fs::read_dir(entry.path()) {
for (j, subentry) in subentries.filter_map(Result::ok).enumerate() {
println!(" {}.{}: {}", i, j, subentry.path().display());
}
}
}
}
} else {
println!("Failed to read resource directory");
}
}
let filename = Path::new(&cursor.path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
println!("Extracted filename: {}", filename);
// Try using the actual file path directly
println!("Using direct file path: {}", cursor.path);
// Verify the file exists
if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
}
// Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", index); // Ignore error
println!("Successfully applied cursor using actual file path");
return Ok(());
} else {
println!("File doesn't exist: {}", cursor.path);
return Err(format!("Cursor file does not exist: {}", cursor.path));
}
} else {
Err("Index out of bounds".to_string())
}
}
// Command to go to the next cursor
#[tauri::command]
fn next_cursor(state: tauri::State<AppStateWrapper>, app_handle: AppHandle) -> Result<usize, String> {
println!("=== RESOURCE DEBUG [next_cursor] ===");
let mut app_state = state.0.lock().unwrap();
if !app_state.cursors.is_empty() {
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
let cursor = &app_state.cursors[app_state.current_index];
println!("Next cursor: {} ({})", cursor.name, cursor.path);
let filename = Path::new(&cursor.path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
// Try both resource paths like in select_cursor
println!("Extracted filename: {}", filename);
// Try using the actual file path directly
println!("Using direct file path: {}", cursor.path);
// Verify the file exists
if Path::new(&cursor.path).exists() {
println!("File exists, applying cursor...");
if let Err(e) = platform::set_system_cursor(&cursor.path) {
println!("Error applying cursor: {}", e);
return Err(format!("Failed to apply cursor '{}': {}", cursor.name, e));
}
// Emit an event to notify the frontend
let _ = app_handle.emit_all("cursor_changed", app_state.current_index); // Ignore error
println!("Successfully applied cursor using actual file path");
return Ok(app_state.current_index);
} else {
println!("File doesn't exist: {}", cursor.path);
return Err(format!("Cursor file does not exist: {}", cursor.path));
}
} else {
Err("No cursors available".to_string())
}
}
// Command to quit the application
#[tauri::command]
fn quit_app(app_handle: AppHandle) {
// Clean up and restore default cursor before exit
match platform::restore_system_cursor() {
Ok(_) => println!("Default cursor restored"),
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
}
// Exit the application
std::thread::spawn(move || {
app_handle.exit(0);
});
}
// Command to minimize the window to tray
#[tauri::command]
fn minimize_to_tray(app_handle: AppHandle) -> Result<(), String> {
println!("Minimizing to tray");
// Get the main window
let window = app_handle.get_window("main")
.ok_or_else(|| "Failed to get main window".to_string())?;
// Hide the window
window.hide().map_err(|e| format!("Failed to hide window: {}", e))?;
// The tray should already be visible as configured in tauri.conf.json
println!("Window hidden, application should now be accessible from the system tray");
Ok(())
}
// Function to load cursor files from src/cursors directory
fn load_cursor_files() -> Vec<Cursor> {
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
let mut cursors = Vec::new();
// Determine the path to src/cursors relative to src-tauri (CARGO_MANIFEST_DIR)
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
println!("CARGO_MANIFEST_DIR: {}", manifest_dir.display());
// Check multiple possible locations
let possible_cursor_dirs = vec![
manifest_dir.join("../src/cursors"),
manifest_dir.join("src/cursors"),
manifest_dir.join("cursors"),
manifest_dir.clone(), // Check manifest dir itself
];
println!("CHECKING FOR PNG/CUR FILES IN MULTIPLE DIRECTORIES:");
for (i, cursors_dir) in possible_cursor_dirs.iter().enumerate() {
println!("Checking dir {}: {}", i+1, cursors_dir.display());
println!(" Directory exists: {}", cursors_dir.exists());
if cursors_dir.exists() {
println!(" Looking for cursor files in this directory...");
if let Ok(entries) = std::fs::read_dir(cursors_dir) {
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
println!(" Found: {}", path.display());
if path.is_file() && path.extension().map_or(false, |ext| ext == "png" || ext == "cur") {
let name = path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Unknown")
.to_string();
println!(" ADDING CURSOR: {} at path {}", name, path.display());
cursors.push(Cursor {
name,
path: path.to_string_lossy().to_string(),
});
}
}
}
if !cursors.is_empty() {
println!(" Found {} cursor files in this directory.", cursors.len());
break;
}
}
}
// If we found no cursors, use mock data with paths relative to what resolve_resource expects
if cursors.is_empty() {
println!("No cursors found in any directory. Using mock data instead.");
println!("IMPORTANT: This is probably the source of your problem!");
println!("The app could not find your cursor PNG files. Check that they exist in one of these directories:");
for dir in &possible_cursor_dirs {
println!(" {}", dir.display());
}
// Mock data paths should reflect their eventual resource path structure
cursors.push(Cursor {
name: "Default".to_string(),
path: "src/cursors/default.png".to_string(), // Filename will be 'default.png'
});
cursors.push(Cursor {
name: "Icon".to_string(),
path: "src/cursors/icon.png".to_string(), // Filename will be 'icon.png'
});
cursors.push(Cursor {
name: "Red".to_string(),
path: "src/cursors/red.png".to_string(), // Filename will be 'red.png'
});
println!("Created {} mock cursor entries", cursors.len());
}
println!("Loaded {} cursor files", cursors.len());
// Sort cursors by name for consistent ordering
cursors.sort_by(|a, b| a.name.cmp(&b.name));
cursors
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
println!("Starting Cursor Rush application");
// Initialize platform-specific cursor handling
if let Err(e) = platform::initialize() {
eprintln!("Failed to initialize platform: {}", e);
}
// Load cursors from assets directory
let cursors = load_cursor_files();
// Create app state
let app_state = AppState {
cursors,
current_index: 0,
};
// Create system tray menu
let next_cursor_item = CustomMenuItem::new("next_cursor".to_string(), "Next cursor");
let show_window_item = CustomMenuItem::new("show_window".to_string(), "Show window");
let quit_item = CustomMenuItem::new("quit".to_string(), "Quit");
let tray_menu = SystemTrayMenu::new()
.add_item(next_cursor_item)
.add_item(show_window_item)
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(quit_item);
let system_tray = SystemTray::new().with_menu(tray_menu);
// This is one of the simplest ways to initialize Tauri
tauri::Builder::default()
.manage(AppStateWrapper(Mutex::new(app_state)))
.system_tray(system_tray)
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::LeftClick { .. } => {
println!("System tray received a left click");
if let Some(window) = app.get_window("main") {
match window.is_visible() {
Ok(visible) => {
if visible {
println!("Window is already visible, focusing it");
let _ = window.set_focus();
} else {
println!("Window is hidden, showing it");
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
if let Err(e) = window.set_focus() {
eprintln!("Failed to focus window: {}", e);
}
}
},
Err(e) => {
eprintln!("Failed to check window visibility: {}", e);
// Try to show it anyway
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
}
}
} else {
eprintln!("Main window not found");
}
}
SystemTrayEvent::MenuItemClick { id, .. } => {
println!("Menu item clicked: {}", id);
match id.as_str() {
"next_cursor" => {
println!("Next cursor requested from tray");
let state = app.state::<AppStateWrapper>();
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" => {
println!("Show window requested from tray");
if let Some(window) = app.get_window("main") {
if let Err(e) = window.show() {
eprintln!("Failed to show window: {}", e);
}
if let Err(e) = window.set_focus() {
eprintln!("Failed to focus window: {}", e);
}
} else {
eprintln!("Main window not found");
}
}
"quit" => {
println!("Quit requested from tray");
// Clean up and restore default cursor before exit
match platform::restore_system_cursor() {
Ok(_) => println!("Default cursor restored"),
Err(e) => eprintln!("Failed to restore default cursor: {}", e),
}
app.exit(0);
}
_ => {}
}
}
_ => {}
})
.on_window_event(move |event| {
match event.event() {
WindowEvent::CloseRequested { api, .. } => {
// Prevent the default close action
api.prevent_close();
// Get the window and hide it
let window = event.window();
println!("Window close requested in backend handler, hiding window");
// Hide the window instead of closing it
if let Err(e) = window.hide() {
eprintln!("Failed to hide window: {}", e);
} else {
println!("Window hidden successfully, app is now in system tray");
}
}
_ => {}
}
})
.invoke_handler(tauri::generate_handler![
get_cursors,
get_current_cursor_index,
select_cursor,
next_cursor,
quit_app,
minimize_to_tray
])
.setup(|_app| {
// Register for events or perform additional setup
println!("Tauri application setup complete");
// We're not setting up a window close handler here anymore
// The frontend will handle this based on the minimize-to-tray setting
Ok(())
})
// This matches exactly how it was before our changes
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cursor_rush::run() // Corrected line
}
+20
View File
@@ -0,0 +1,20 @@
// Linux platform implementation for cursor handling
use super::Result;
// Initialize cursor handling
pub fn init() -> Result<()> {
println!("Linux cursor initialization not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn set_cursor(path: &str) -> Result<()> {
// Linux cursor implementation would go here
println!("Linux cursor setting not yet implemented for path: {}", path);
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn restore_default_cursor() -> Result<()> {
// Linux cursor restoration would go here
println!("Linux cursor restoration not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
+20
View File
@@ -0,0 +1,20 @@
// macOS platform implementation for cursor handling
use super::Result;
// Initialize cursor handling
pub fn init() -> Result<()> {
println!("macOS cursor initialization not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn set_cursor(path: &str) -> Result<()> {
// macOS cursor implementation would go here
println!("macOS cursor setting not yet implemented for path: {}", path);
Ok(()) // Return OK for now to not block platform-agnostic code
}
pub fn restore_default_cursor() -> Result<()> {
// macOS cursor restoration would go here
println!("macOS cursor restoration not yet implemented");
Ok(()) // Return OK for now to not block platform-agnostic code
}
+64
View File
@@ -0,0 +1,64 @@
// Platform-specific cursor handling
// Common types for cursor handling
pub mod types;
// Windows implementation
#[cfg(target_os = "windows")]
mod windows;
// macOS implementation
#[cfg(target_os = "macos")]
mod macos;
// Linux implementation
#[cfg(target_os = "linux")]
mod linux;
// Error type for platform operations
pub type Result<T> = std::result::Result<T, String>;
// Platform-agnostic interface for cursor handling
#[cfg(target_os = "windows")]
pub use self::windows::{set_cursor, restore_default_cursor, init};
#[cfg(target_os = "macos")]
pub use self::macos::{set_cursor, restore_default_cursor, init};
#[cfg(target_os = "linux")]
pub use self::linux::{set_cursor, restore_default_cursor, init};
// Initialize platform-specific cursor handling
pub fn initialize() -> Result<()> {
#[cfg(target_os = "windows")]
{
init()
}
#[cfg(not(target_os = "windows"))]
{
Ok(())
}
}
// Cross-platform implementation for setting cursor
pub fn set_system_cursor(path: &str) -> Result<()> {
println!("Setting system cursor: {}", path);
set_cursor(path)
}
// Cross-platform implementation for restoring default cursor
pub fn restore_system_cursor() -> Result<()> {
println!("Restoring system cursor");
restore_default_cursor()
}
// Default implementations for non-supported platforms
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn set_cursor(_path: &str) -> Result<()> {
Err("Cursor customization not supported on this platform".to_string())
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn restore_default_cursor() -> Result<()> {
Err("Cursor restoration not supported on this platform".to_string())
}
+26
View File
@@ -0,0 +1,26 @@
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>,
}
@@ -0,0 +1,512 @@
// Windows platform cursor management
use std::ptr::null_mut;
use std::sync::atomic::Ordering;
use winapi::{
um::winuser::{
GetDC, ReleaseDC, LoadCursorW, SetSystemCursor, CopyIcon,
SystemParametersInfoW, SPI_SETCURSORS, SetCursor,
MAKEINTRESOURCEW, IDC_ARROW, CreateIconIndirect, ICONINFO,
DestroyIcon,
},
um::wingdi::{
CreateCompatibleDC, DeleteDC, SelectObject, DeleteObject,
BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, BI_RGB, CreateDIBSection,
},
um::consoleapi::SetConsoleCtrlHandler,
shared::minwindef::{TRUE, FALSE, DWORD},
};
use ::image::{RgbaImage, open as image_open};
use super::types::*;
use super::state::*;
// Control handler function for console events (Ctrl+C, close, etc.)
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
println!("Control event received, attempting cleanup...");
cleanup(); // Call cleanup logic
FALSE // Allow other handlers (like default exit) to run
}
// Create a cursor from an RGBA image
unsafe fn create_cursor_from_image(image: &RgbaImage) -> Option<SyncHCURSOR> {
let (width, height) = image.dimensions();
let width_i32 = width as i32;
let height_i32 = height as i32;
// Create device context and bitmap for the color data
let screen_dc = GetDC(null_mut());
if screen_dc.is_null() {
eprintln!("Failed to get screen DC");
return None;
}
// Create a compatible DC for our bitmap
let color_dc = CreateCompatibleDC(screen_dc);
if color_dc.is_null() {
eprintln!("Failed to create compatible DC for color");
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Create a compatible DC for our mask bitmap
let mask_dc = CreateCompatibleDC(screen_dc);
if mask_dc.is_null() {
eprintln!("Failed to create compatible DC for mask");
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Set up BITMAPINFO structure for DIB
let mut bitmap_info: BITMAPINFO = std::mem::zeroed();
bitmap_info.bmiHeader.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
bitmap_info.bmiHeader.biWidth = width_i32;
bitmap_info.bmiHeader.biHeight = -height_i32; // Negative for top-down DIB
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = 32; // 32 bits per pixel (BGRA)
bitmap_info.bmiHeader.biCompression = BI_RGB;
// Create the color DIB section
let mut color_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
let mut ppv_bits_color: *mut winapi::ctypes::c_void = color_bits_ptr;
let color_bitmap = CreateDIBSection(
color_dc,
&bitmap_info,
DIB_RGB_COLORS,
&mut ppv_bits_color,
null_mut(),
0,
);
color_bits_ptr = ppv_bits_color;
if color_bitmap.is_null() || color_bits_ptr.is_null() {
eprintln!("Failed to create color DIB section");
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Create the mask DIB section (1-bit monochrome)
bitmap_info.bmiHeader.biBitCount = 1; // 1 bit per pixel for mask
let mut mask_bits_ptr: *mut winapi::ctypes::c_void = null_mut();
let mut ppv_bits: *mut winapi::ctypes::c_void = mask_bits_ptr;
let mask_bitmap = CreateDIBSection(
mask_dc,
&bitmap_info,
DIB_RGB_COLORS,
&mut ppv_bits,
null_mut(),
0,
);
mask_bits_ptr = ppv_bits;
if mask_bitmap.is_null() || mask_bits_ptr.is_null() {
eprintln!("Failed to create mask DIB section");
DeleteObject(color_bitmap as _);
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// Select the bitmaps into the DCs
let old_color_bitmap = SelectObject(color_dc, color_bitmap as _);
let old_mask_bitmap = SelectObject(mask_dc, mask_bitmap as _);
if old_color_bitmap.is_null() || old_mask_bitmap.is_null() {
eprintln!("Failed to select bitmaps into DCs");
DeleteObject(mask_bitmap as _);
DeleteObject(color_bitmap as _);
DeleteDC(mask_dc);
DeleteDC(color_dc);
ReleaseDC(null_mut(), screen_dc);
return None;
}
// 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);
// Calculate mask stride in bytes (1 bit per pixel, padded to DWORD)
let mask_stride = ((width + 31) / 32) * 4;
let mask_bits = std::slice::from_raw_parts_mut(mask_bits_ptr as *mut u8, (mask_stride * height) as usize);
// Clear mask bits (all transparent initially)
for i in 0..(mask_stride * height) as usize {
mask_bits[i] = 0xFF; // All bits set = transparent
}
// Process image pixels
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y);
let color_idx = ((y * width + x) * 4) as usize;
// Get RGBA components
let r = pixel[0];
let g = pixel[1];
let b = pixel[2];
let a = pixel[3];
// Set color data (BGR order)
color_bits[color_idx + 0] = b;
color_bits[color_idx + 1] = g;
color_bits[color_idx + 2] = r;
color_bits[color_idx + 3] = a;
// Update mask bit if pixel is not transparent
if a > 0 {
// Calculate bit position in mask
let byte_idx = (y * mask_stride + (x / 8)) as usize;
let bit_idx = 7 - (x % 8); // Bits are stored MSB first
// Clear the bit (0 = opaque in AND mask)
mask_bits[byte_idx] &= !(1 << bit_idx);
}
}
}
// Create ICONINFO structure
let mut icon_info: ICONINFO = std::mem::zeroed();
icon_info.fIcon = FALSE; // This is a cursor, not an icon
icon_info.xHotspot = width as DWORD / 2; // Center hotspot
icon_info.yHotspot = height as DWORD / 2;
icon_info.hbmMask = mask_bitmap;
icon_info.hbmColor = color_bitmap;
// Create the cursor
let cursor = CreateIconIndirect(&mut icon_info as *mut _);
// Clean up resources
SelectObject(color_dc, old_color_bitmap);
SelectObject(mask_dc, old_mask_bitmap);
DeleteDC(color_dc);
DeleteDC(mask_dc);
ReleaseDC(null_mut(), screen_dc);
// Windows makes a copy of the bitmaps for the cursor, so we can delete them
DeleteObject(color_bitmap as _);
DeleteObject(mask_bitmap as _);
if cursor.is_null() {
eprintln!("Failed to create cursor");
return None;
}
// Return the cursor wrapped in our thread-safe type
Some(SyncHCURSOR(cursor))
}
// Cleanup function to restore cursors and clean up resources
pub fn cleanup() {
println!("Running cursor cleanup...");
// Restore system cursors if they were hidden
// Use swap to ensure it runs only once
if CURSOR_HIDDEN.swap(false, Ordering::SeqCst) {
println!("Restoring system cursors...");
unsafe {
// Try multiple approaches to ensure cursors are restored
// 1. First try to restore original cursors if we saved them
let original_cursors_opt = ORIGINAL_CURSORS.lock().unwrap().take();
if let Some(original_cursors) = original_cursors_opt {
println!("Restoring {} saved original cursors", original_cursors.len());
for (cursor_id, cursor) in original_cursors {
if !cursor.0.is_null() {
// Make a copy of the original cursor
let cursor_copy = CopyIcon(cursor.0);
if !cursor_copy.is_null() {
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
eprintln!("Failed to restore original cursor for ID: {}", cursor_id);
DestroyIcon(cursor_copy);
}
} else {
eprintln!("Failed to copy original cursor for ID: {}", cursor_id);
}
// Destroy the original cursor we saved
DestroyIcon(cursor.0);
}
}
} else {
println!("No saved original cursors found");
}
// 2. Force Windows to reload default cursor settings
println!("Forcing Windows to reload default cursor settings");
SystemParametersInfoW(SPI_SETCURSORS, 0, null_mut(), 0);
// 3. Also try the A version for maximum compatibility
super::state::reset_system_cursors();
// 4. Reset to default arrow cursor
let arrow_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(IDC_ARROW as u16));
if !arrow_cursor.is_null() {
SetCursor(arrow_cursor);
println!("Reset to default arrow cursor.");
}
}
println!("System cursors restoration complete.");
}
// Destroy the custom cursor if it exists
let custom_cursor_to_destroy = CUSTOM_CURSOR.lock().unwrap().take();
if let Some(cursor_wrapper) = custom_cursor_to_destroy {
unsafe {
if !cursor_wrapper.0.is_null() {
DestroyIcon(cursor_wrapper.0);
println!("Custom cursor destroyed during cleanup.");
}
}
}
// Reset the last known cursor position
*LAST_CURSOR_POS.lock().unwrap() = None;
// Clear the current cursor image
*CURRENT_CURSOR_IMAGE.lock().unwrap() = None;
// Unregister control handler
if CTRL_HANDLER_REGISTERED.load(Ordering::SeqCst) {
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), FALSE) != 0 {
CTRL_HANDLER_REGISTERED.store(false, Ordering::SeqCst);
println!("Control handler unregistered.");
} else {
eprintln!("Failed to unregister control handler.");
}
}
}
println!("Cursor cleanup complete.");
}
// --- Public API Functions ---
// Hide the system cursor - now just initializes the control handler
pub fn hide_system_cursor() -> bool {
// Initialize once
INIT.get_or_init(|| {
// Register control handler for clean exit
unsafe {
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
println!("Control handler registered for clean exit.");
} else {
eprintln!("Failed to register control handler: {}", std::io::Error::last_os_error());
}
}
});
// Check if already hidden
if CURSOR_HIDDEN.load(Ordering::SeqCst) {
return true; // Already hidden
}
// Save original cursors for restoration
let mut original_cursors = Vec::new();
unsafe {
for &cursor_id in SYSTEM_CURSORS.iter() {
// Get the original cursor
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
if !original_cursor.is_null() {
// Save the original cursor for restoration
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
}
}
}
// Store original cursors for restoration
if !original_cursors.is_empty() {
*ORIGINAL_CURSORS.lock().unwrap() = Some(original_cursors);
}
// Mark as hidden - this is now just a flag to indicate we've saved the original cursors
CURSOR_HIDDEN.store(true, Ordering::SeqCst);
true
}
// Update the cursor image using CursorInfo (with caching)
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool {
println!("Updating cursor to: {}", cursor_info.name);
// Use cached image if available, otherwise load from disk
let image: image::RgbaImage = if let Some(ref cached_image) = cursor_info.image {
println!("Using cached image for cursor: {}", cursor_info.name);
let img_clone: image::RgbaImage = cached_image.clone();
img_clone
} else {
println!("Loading image from disk for cursor: {}", cursor_info.name);
match image_open(&cursor_info.path) {
Ok(img) => {
let rgba = img.to_rgba8();
// Cache the image for future use
// Make sure to specify the type to avoid type inference issues
let image_copy: image::RgbaImage = rgba.clone();
cursor_info.image = Some(image_copy);
rgba
},
Err(e) => {
eprintln!("Failed to load cursor image: {}", e);
return false;
}
}
};
// Store the image in the global state for reference
*CURRENT_CURSOR_IMAGE.lock().unwrap() = Some(image.clone());
unsafe {
// Create a new cursor first before destroying the old one
// This minimizes the time when no custom cursor is active
let new_cursor = if let Some(ref cached_image) = cursor_info.image {
// Always create a fresh cursor from the image to avoid invalid handle issues
create_cursor_from_image(cached_image)
} else {
create_cursor_from_image(&image)
};
if let Some(new_cursor_wrapper) = new_cursor {
// Apply the new cursor to all system cursors first
let success = apply_cursor_to_all_system_cursors(new_cursor_wrapper.0);
// Now that the new cursor is applied, we can clean up the old one
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
if let Some(old_cursor) = cursor_guard.take() {
// Only destroy the old cursor after the new one is applied
DestroyIcon(old_cursor.0);
}
// Store the new cursor
*cursor_guard = Some(new_cursor_wrapper);
// Update the cached cursor in CursorInfo
#[cfg(windows)]
{
// Convert SyncHCURSOR to WinCursorWrapper for storage in cursor_info
cursor_info.win_cursor = Some(crate::platform::types::WinCursorWrapper {
handle: new_cursor_wrapper.0
});
}
if !success {
eprintln!("Failed to apply cursor to all system cursors");
// Even if we failed to set all system cursors, try to at least set the current cursor
SetCursor(new_cursor_wrapper.0);
}
return true;
} else {
eprintln!("Failed to create cursor from image");
// If we failed to create a new cursor, try to use the cached one as a fallback
#[cfg(windows)]
if let Some(cached_cursor) = &cursor_info.win_cursor {
if !cached_cursor.handle.is_null() {
println!("Falling back to cached cursor as a last resort");
let mut cursor_guard = CUSTOM_CURSOR.lock().unwrap();
// Convert WinCursorWrapper to SyncHCURSOR
let sync_cursor = super::types::SyncHCURSOR(cached_cursor.handle);
*cursor_guard = Some(sync_cursor);
SetCursor(cached_cursor.handle);
return true;
}
}
return false;
}
}
}
// Apply a cursor to all system cursors
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
let mut success = true;
// First, ensure the cursor is valid
if cursor.is_null() {
eprintln!("Cannot apply null cursor to system cursors");
return false;
}
// Immediately set the current cursor to minimize flashing
SetCursor(cursor);
// Apply the cursor to all system cursor types
// Start with the most commonly used cursors first
let priority_cursors = [
32512, // IDC_ARROW (most important)
32513, // IDC_IBEAM (text cursor)
32649, // IDC_HAND (links)
];
// Apply to priority cursors first
for &cursor_id in priority_cursors.iter() {
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor);
if cursor_copy.is_null() {
let error = std::io::Error::last_os_error();
eprintln!("Failed to copy cursor for priority ID: {} - Error: {}", cursor_id, error);
success = false;
continue;
}
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
let error = std::io::Error::last_os_error();
eprintln!("Failed to set cursor for priority ID: {} - Error: {}", cursor_id, error);
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
success = false;
}
}
// Now apply to the remaining cursors
for &cursor_id in SYSTEM_CURSORS.iter() {
// Skip the priority cursors we already processed
if priority_cursors.contains(&cursor_id) {
continue;
}
// Make a copy of the cursor
let cursor_copy = CopyIcon(cursor);
if cursor_copy.is_null() {
// Don't log errors for non-priority cursors to reduce console spam
success = false;
continue;
}
// Set the system cursor
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
// Don't log errors for non-priority cursors to reduce console spam
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
success = false;
}
}
// If we had any failures, set the current cursor again as a fallback
if !success {
SetCursor(cursor);
}
success
}
// Restore the system cursor
pub fn restore_cursor() {
cleanup();
}
// Function to handle mouse movement and update the cursor
// Kept for potential future use but currently not needed with SetSystemCursor approach
#[allow(dead_code)]
pub fn handle_mouse_move() {
// With SetSystemCursor, we don't need to constantly update the cursor on mouse move
// This function is kept for API compatibility, but it's now a no-op
// System cursors are already set to our custom cursor
}
@@ -0,0 +1,45 @@
// Windows platform implementation module
pub mod types;
mod cursor;
mod state;
use std::path::PathBuf;
use crate::platform::Result;
// Functions expected by the platform-agnostic interface
pub fn set_cursor(path: &str) -> Result<()> {
println!("Windows platform: setting cursor to {}", path);
// Create a cursor info struct to pass to update_cursor_info
let mut cursor_info = crate::platform::types::CursorInfo {
name: path.split('\\').last().unwrap_or("unknown").to_string(),
path: PathBuf::from(path),
image: None,
hotspot_x: 0, // Default hotspot at top-left
hotspot_y: 0,
#[cfg(target_os = "windows")]
win_cursor: None,
};
// Use the existing update_cursor_info function to handle loading and applying the cursor
if !cursor::update_cursor_info(&mut cursor_info) {
return Err(format!("Failed to update cursor from {}", path));
}
Ok(())
}
pub fn restore_default_cursor() -> Result<()> {
println!("Windows platform: restoring default cursor");
cursor::restore_cursor();
Ok(())
}
pub fn init() -> Result<()> {
println!("Windows platform: initializing cursor system");
if !cursor::hide_system_cursor() {
return Err("Failed to initialize cursor system".to_string());
}
Ok(())
}
@@ -0,0 +1,57 @@
// Windows platform state management
use std::sync::{Mutex, atomic::AtomicBool};
use winapi::{
shared::minwindef::DWORD,
shared::windef::POINT,
};
use ::image::RgbaImage;
use once_cell::sync::{Lazy, OnceCell};
use super::types::*;
// --- Thread-Safe Static Variables ---
// Use AtomicBool for simple flags
pub static CURSOR_HIDDEN: AtomicBool = AtomicBool::new(false);
pub static CTRL_HANDLER_REGISTERED: AtomicBool = AtomicBool::new(false);
// Use Lazy<Mutex<T>> for complex Option types
pub static ORIGINAL_CURSORS: Lazy<Mutex<Option<Vec<(DWORD, SyncHCURSOR)>>>> = Lazy::new(|| Mutex::new(None));
pub static CURRENT_CURSOR_IMAGE: Lazy<Mutex<Option<RgbaImage>>> = Lazy::new(|| Mutex::new(None));
pub static LAST_CURSOR_POS: Lazy<Mutex<Option<POINT>>> = Lazy::new(|| Mutex::new(None));
// Use Lazy<Mutex<Option<Handle>>> for WinAPI handles with our thread-safe wrappers
pub static CUSTOM_CURSOR: Lazy<Mutex<Option<SyncHCURSOR>>> = Lazy::new(|| Mutex::new(None));
// Note: Legacy variables for window-based cursor approach and mouse hook tracking
// have been removed as part of code cleanup
// Use OnceCell for INIT to avoid `static mut`
pub static INIT: OnceCell<()> = OnceCell::new();
// Reset system cursors using the Windows API
pub fn reset_system_cursors() {
unsafe {
// Use both versions for maximum compatibility
winapi::um::winuser::SystemParametersInfoW(
winapi::um::winuser::SPI_SETCURSORS,
0,
std::ptr::null_mut(),
0
);
// Import the Windows API function to reset system cursors
#[link(name = "user32")]
extern "system" {
fn SystemParametersInfoA(uiAction: u32, uiParam: u32, pvParam: *mut std::ffi::c_void, fWinIni: u32) -> i32;
}
SystemParametersInfoA(
SPI_SETCURSORS_A,
0,
std::ptr::null_mut(),
SPIF_UPDATEINIFILE | SPIF_SENDCHANGE
);
}
}
@@ -0,0 +1,40 @@
// Windows platform types and constants
use winapi::{
shared::windef::HCURSOR,
shared::minwindef::DWORD,
};
// --- Thread-Safe Handle Wrappers ---
// These wrappers make raw Windows handles safe to share between threads
// Wrapper for HCURSOR (cursor handle)
#[derive(Copy, Clone, Debug)]
pub struct SyncHCURSOR(pub HCURSOR);
unsafe impl Send for SyncHCURSOR {}
unsafe impl Sync for SyncHCURSOR {}
// --- Constants ---
// Constants for SystemParametersInfo
pub const SPI_SETCURSORS_A: u32 = 0x0057;
pub const SPIF_UPDATEINIFILE: u32 = 0x01;
pub const SPIF_SENDCHANGE: u32 = 0x02;
// Note: Constants for window-based cursor approach have been removed as part of code cleanup
// Define all system cursor IDs we'll replace
pub static SYSTEM_CURSORS: [DWORD; 11] = [
32512, // IDC_ARROW
32513, // IDC_IBEAM
32515, // IDC_CROSS
32514, // IDC_WAIT
32649, // IDC_HAND
32650, // IDC_APPSTARTING
32648, // IDC_NO
32646, // IDC_SIZEALL
32645, // IDC_SIZENS
32644, // IDC_SIZEWE
32642, // IDC_SIZENWSE
];
+72
View File
@@ -0,0 +1,72 @@
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:5173",
"distDir": "../dist"
},
"package": {
"productName": "cursor-rush",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false,
"window": {
"all": false,
"startDragging": true,
"minimize": true,
"hide": true,
"show": true,
"close": true
},
"shell": {
"all": false,
"open": true
},
"fs": {
"all": false,
"scope": [
"$APP/cursors/*"
]
}
},
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": false,
"menuOnLeftClick": false,
"title": "Cursor Rush"
},
"bundle": {
"active": true,
"identifier": "com.cursor-rush.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"targets": "all"
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"label": "main",
"title": "Cursor Rush",
"width": 310,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true,
"visible": true
}
]
}
}
+102
View File
@@ -0,0 +1,102 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-color: rgba(24, 24, 37, 0.85);
--nav-bg: rgba(40, 40, 56, 0.4);
--nav-active: rgba(65, 66, 88, 0.7);
--border-color: rgba(49, 50, 68, 0.6);
--text-color: #cdd6f4;
--accent-color: #89b4fa;
--button-bg: rgba(65, 66, 88, 0.6);
--button-hover: rgba(70, 71, 90, 0.8);
--button-active: rgba(137, 180, 250, 0.8);
--tab-bg: rgba(40, 40, 56, 0.4);
--tab-active: rgba(65, 66, 88, 0.7);
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: transparent;
color: var(--text-color);
overflow: hidden;
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
}
/* Modal Container - Draggable Area */
.modal-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
background-color: transparent;
}
/* Modal - Main App Window */
.modal {
width: 100%;
height: 100%;
background-color: var(--bg-color);
backdrop-filter: blur(10px);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
}
/* Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--nav-bg);
}
::-webkit-scrollbar-thumb {
background: var(--nav-bg);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--nav-active);
}
/* Buttons */
button {
background-color: var(--button-bg);
color: var(--text-color);
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
button:hover {
background-color: var(--button-hover);
}
button:active {
background-color: var(--button-active);
color: var(--bg-color);
}
/* Images */
img {
max-width: 100%;
height: auto;
}
+192
View File
@@ -0,0 +1,192 @@
import { useState, useEffect, useCallback } from "react";
import { invoke } from "@tauri-apps/api/tauri";
import { appWindow } from "@tauri-apps/api/window";
import "./App.css";
// Import cursor images directly
import defaultCursor from "./cursors/default.png";
import iconCursor from "./cursors/icon.png";
import redCursor from "./cursors/red.png";
// Components
import Header from "./components/Header";
import Content from "./components/Content";
import Footer from "./components/Footer";
function App() {
const [activePage, setActivePage] = useState("cursors");
const [cursors, setCursors] = useState([]);
const [currentCursorIndex, setCurrentCursorIndex] = useState(0);
const [settings, setSettings] = useState({
minimizeToTrayOnClose: true,
startWithWindows: false,
autoApplyCursorOnSelection: true
});
// Function to minimize app to tray
const minimizeToTray = useCallback(async () => {
try {
console.log('Minimizing app to tray');
// First hide the window to ensure it disappears immediately
await appWindow.hide();
console.log('Window hidden, now ensuring tray is visible');
// Then call the backend to ensure the tray is visible
await invoke("minimize_to_tray");
console.log('minimize_to_tray command completed successfully');
} catch (error) {
console.error("Failed to minimize to tray:", error);
// Fallback to window API if the invoke fails
try {
console.log('Trying fallback hide');
await appWindow.hide();
console.log('Fallback hide succeeded');
} catch (fallbackError) {
console.error("Fallback hide also failed:", fallbackError);
}
}
}, []);
// Load cursors on mount and set up event listeners
useEffect(() => {
const loadCursors = async () => {
try {
// Get cursors from the Tauri backend
const loadedCursors = await invoke("get_cursors");
console.log("Loaded cursors:", loadedCursors);
setCursors(loadedCursors || []);
// Get current cursor index
const currentIndex = await invoke("get_current_cursor_index");
setCurrentCursorIndex(currentIndex);
} catch (error) {
console.error("Failed to load cursors:", error);
// Use the imported cursor images directly
setCursors([
{ name: "default", path: defaultCursor },
{ name: "icon", path: iconCursor },
{ name: "red", path: redCursor },
// We don't have blue and gold cursors imported since they don't exist yet
// These will be excluded for now
]);
setCurrentCursorIndex(0);
}
};
loadCursors();
}, []);
// Set up window close event listener
useEffect(() => {
const setupCloseHandler = async () => {
const unlisten = await appWindow.onCloseRequested(async (event) => {
console.log('Window close requested');
if (settings.minimizeToTrayOnClose) {
console.log('Minimize to tray setting is enabled, preventing close and hiding instead');
// Prevent the window from closing
event.preventDefault();
// Hide the window instead
try {
console.log('Calling minimizeToTray function');
await minimizeToTray();
console.log('Window should now be hidden and app should be in system tray');
} catch (error) {
console.error('Failed to minimize to tray on close:', error);
}
} 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
try {
await invoke("quit_app");
} catch (error) {
console.error("Failed to quit app:", error);
}
}
});
return unlisten;
};
let unlistenFn;
setupCloseHandler().then(unlisten => {
unlistenFn = unlisten;
console.log('Close handler set up successfully');
}).catch(err => console.error('Failed to set up close handler:', err));
// Clean up event listener on unmount
return () => {
if (unlistenFn) {
unlistenFn();
}
};
}, [settings.minimizeToTrayOnClose, minimizeToTray]);
// Functions to handle cursor operations using Tauri backend
const selectCursor = async (index) => {
try {
console.log(`Selecting cursor at index ${index}`);
await invoke("select_cursor", { index });
setCurrentCursorIndex(index);
} catch (error) {
console.error("Failed to select cursor:", error);
}
};
const nextCursor = async () => {
if (cursors.length === 0) return;
try {
console.log('Switching to next cursor');
const newIndex = await invoke("next_cursor");
setCurrentCursorIndex(newIndex);
} catch (error) {
console.error("Failed to switch to next cursor:", error);
// Fallback if backend fails
const newIndex = (currentCursorIndex + 1) % cursors.length;
setCurrentCursorIndex(newIndex);
}
};
// Function to exit app
const quitApp = async () => {
try {
console.log('Quitting app');
await invoke("quit_app");
} catch (error) {
console.error("Failed to quit app:", error);
}
};
return (
<div className="modal-container">
<div className="modal">
<Header
activePage={activePage}
setActivePage={setActivePage}
minimizeToTray={minimizeToTray}
quitApp={quitApp}
settings={settings}
/>
<Content
activePage={activePage}
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
settings={settings}
setSettings={setSettings}
/>
<Footer
version="0.1.0"
status={cursors.length > 0 ? `${cursors.length} cursors loaded` : "No cursors found"}
/>
</div>
</div>
);
}
export default App;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+14
View File
@@ -0,0 +1,14 @@
/* Content Component Styles */
.app-content {
flex: 1;
overflow-y: auto;
padding: 1vh;
border-top: 2px solid rgba(49, 50, 68, 0.6);
}
.content-title {
font-size: 18px;
color: var(--accent-color, #89b4fa);
margin-bottom: 15px;
font-weight: 500;
}
+50
View File
@@ -0,0 +1,50 @@
import React from 'react';
import './Content.css';
// Import content modules
import Inventory from './content_modules/Inventory';
import Cursors from './content_modules/Cursors';
import Crates from './content_modules/Crates';
import Settings from './content_modules/Settings';
function Content({ activePage, cursors, currentCursorIndex, selectCursor, nextCursor, settings, setSettings }) {
// Render the appropriate content based on active page
const renderContent = () => {
switch (activePage) {
case 'inventory':
return <Inventory
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
/>;
case 'cursors':
return (
<Cursors
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
/>
);
case 'crates':
return <Crates />;
case 'settings':
return <Settings settings={settings} setSettings={setSettings} />;
default:
return <Cursors
cursors={cursors}
currentCursorIndex={currentCursorIndex}
selectCursor={selectCursor}
nextCursor={nextCursor}
/>;
}
};
return (
<div className="app-content">
{renderContent()}
</div>
);
}
export default Content;
+21
View File
@@ -0,0 +1,21 @@
/* Footer Component Styles */
.app-footer {
width: 100%;
background-color: var(--footer-bg, #181825);
padding: 8px 15px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--border-color, #313244);
font-size: 12px;
}
.app-status {
color: var(--text-color, #cdd6f4);
opacity: 0.8;
}
.app-version {
color: var(--text-color, #cdd6f4);
opacity: 0.6;
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import './Footer.css';
function Footer({ version, status }) {
return (
<footer className="app-footer">
<div className="app-status">
{status}
</div>
<div className="app-version">
v{version}
</div>
</footer>
);
}
export default Footer;
+100
View File
@@ -0,0 +1,100 @@
/* Header Component Styles */
.app-header {
width: 100%;
user-select: none;
height: 40px;
position: relative;
z-index: 10;
background-color: transparent; /* Reverted to original */
/* cursor: move; */ /* Removed, should be handled by drag region or default */
}
.tabs-container {
display: flex;
align-items: center;
height: 100%;
position: relative;
padding-right: 10px;
background-color: transparent;
}
/* Chrome-like tabs */
.tab {
-webkit-app-region: no-drag; /* Override drag region for clickable tabs */
cursor: pointer; /* Ensure tabs have a pointer cursor */
height: 36px;
width: 42px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
font-size: 14px;
margin-right: 5px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
color: var(--text-color);
background-color: transparent;
transition: all 0.2s ease;
cursor: pointer;
margin-top: 4px;
}
.tab:hover:not(.active):not(.drag-handle) {
background-color: var(--tab-bg);
transform: translateY(-2px);
}
.tab.active {
background-color: var(--tab-active);
color: var(--accent-color);
z-index: 1;
height: 40px;
margin-top: 0;
box-shadow: 0 0 10px rgba(137, 180, 250, 0.3);
}
/* Special drag handle tab */
.tab.drag-handle {
width: 30px;
background-color: transparent;
color: rgba(205, 214, 244, 0.5);
}
.tab.drag-handle:hover {
color: rgba(205, 214, 244, 0.8);
}
/* Window control buttons */
.window-controls {
-webkit-app-region: no-drag; /* Override drag region for buttons */
cursor: default; /* Keep default cursor for controls area */
display: flex;
gap: 8px;
margin-left: auto;
margin-right: 10px;
}
.minimize-button,
.close-button {
background: none;
border: none;
font-size: 16px;
line-height: 1;
color: var(--text-color);
cursor: pointer;
padding: 2px 8px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.minimize-button:hover,
.close-button:hover {
background-color: var(--button-hover);
}
.close-button:hover {
color: #f38ba8;
}
+69
View File
@@ -0,0 +1,69 @@
import React from 'react';
import './Header.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faGripVertical,
faBoxOpen,
faMousePointer,
faBoxes,
faCog,
faBoxArchive,
faBox,
faWarehouse,
faGraduationCap,
faCubesStacked,
faBorderAll,
faBagShopping
} from '@fortawesome/free-solid-svg-icons';
function Header({ activePage, setActivePage, minimizeToTray, quitApp, settings }) {
// Navigation buttons for each page with icons
const navItems = [
{ id: 'inventory', icon: faBagShopping, tooltip: 'Inventory' },
{ id: 'cursors', icon: faMousePointer, tooltip: 'Cursors' },
{ id: 'crates', icon: faBoxOpen, tooltip: 'Crates' },
{ id: 'settings', icon: faCog, tooltip: 'Settings' }
];
// Handle close button click based on settings
const handleCloseClick = () => {
if (settings && settings.minimizeToTrayOnClose) {
console.log('Close button clicked, minimizing to tray based on settings');
minimizeToTray();
} else {
console.log('Close button clicked, quitting application based on settings');
quitApp();
}
};
return (
<header className="app-header" data-tauri-drag-region>
<div className="tabs-container">
{navItems.map((item) => (
<div
key={item.id}
className={`tab ${activePage === item.id ? 'active' : ''}`}
onClick={() => setActivePage(item.id)}
title={item.tooltip}
>
<FontAwesomeIcon icon={item.icon} />
</div>
))}
<div className="window-controls">
<button className="minimize-button" onClick={minimizeToTray} title="Minimize to Tray">
_
</button>
<button
className="close-button"
onClick={handleCloseClick}
title={settings && settings.minimizeToTrayOnClose ? "Minimize to Tray" : "Quit Application"}
>
×
</button>
</div>
</div>
</header>
);
}
export default Header;
@@ -0,0 +1,351 @@
/* Crates Module Styles */
.crates-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Current crate section (top part) */
.current-crate-section {
height: 60%;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 2px solid rgba(49, 50, 68, 0.6);
padding: 10px;
}
.current-crate {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: rgba(30, 30, 46, 0.3);
border-radius: 8px;
padding: 15px;
border: 1px solid rgba(49, 50, 68, 0.4);
position: relative;
overflow: hidden;
}
/* Color accents for different crate types */
.current-crate::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
opacity: 0.8;
}
.current-crate.common::before { background-color: #a6adc8; }
.current-crate.uncommon::before { background-color: #a6e3a1; }
.current-crate.rare::before { background-color: #89b4fa; }
.current-crate.epic::before { background-color: #cba6f7; }
.current-crate.legendary::before {
background: linear-gradient(90deg, #f9e2af, #fab387, #f9e2af);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { background-position: -100px; }
100% { background-position: 100px; }
}
.crate-navigation {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
}
.nav-button {
background-color: rgba(49, 50, 68, 0.6);
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-color);
cursor: pointer;
transition: all 0.2s;
}
.nav-button:hover {
background-color: rgba(65, 66, 88, 0.8);
transform: scale(1.1);
}
.nav-button:disabled {
opacity: 0.3;
cursor: not-allowed;
transform: none;
}
.crate-display {
display: flex;
align-items: center;
gap: 20px;
flex: 1;
padding: 0 20px;
}
.crate-image {
display: flex;
justify-content: center;
align-items: center;
width: 100px;
height: 100px;
background-color: rgba(24, 24, 37, 0.5);
border-radius: 12px;
transition: all 0.3s;
border: 1px solid rgba(49, 50, 68, 0.6);
}
.crate-icon {
font-size: 48px;
transition: all 0.3s;
}
/* Crate color styling */
.current-crate.common .crate-icon { color: #a6adc8; text-shadow: 0 0 10px rgba(166, 173, 200, 0.4); }
.current-crate.uncommon .crate-icon { color: #a6e3a1; text-shadow: 0 0 10px rgba(166, 227, 161, 0.4); }
.current-crate.rare .crate-icon { color: #89b4fa; text-shadow: 0 0 10px rgba(137, 180, 250, 0.4); }
.current-crate.epic .crate-icon { color: #cba6f7; text-shadow: 0 0 10px rgba(203, 166, 247, 0.4); }
.current-crate.legendary .crate-icon { color: #f9e2af; text-shadow: 0 0 15px rgba(249, 226, 175, 0.6); animation: pulse 2s infinite; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* Opening animation */
.crate-image.opening {
animation: shake 0.5s infinite;
}
@keyframes shake {
0% { transform: translateX(0); }
25% { transform: translateX(-5px); }
50% { transform: translateX(5px); }
75% { transform: translateX(-5px); }
100% { transform: translateX(0); }
}
.crate-info {
display: flex;
flex-direction: column;
flex: 1;
}
.crate-name {
font-size: 24px;
font-weight: 600;
margin: 0 0 5px 0;
}
.current-crate.common .crate-name { color: #a6adc8; }
.current-crate.uncommon .crate-name { color: #a6e3a1; }
.current-crate.rare .crate-name { color: #89b4fa; }
.current-crate.epic .crate-name { color: #cba6f7; }
.current-crate.legendary .crate-name { color: #f9e2af; }
.crate-description {
font-size: 14px;
opacity: 0.8;
margin: 0 0 10px 0;
}
.crate-stats {
font-size: 14px;
}
.crate-actions {
display: flex;
justify-content: center;
margin-top: 15px;
}
.open-button {
width: 100%;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
min-width: 150px;
justify-content: center;
background-color: rgba(49, 50, 68, 0.7);
color: var(--text-color);
}
.open-button:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
}
.open-button:active:not(:disabled) {
transform: translateY(0);
}
.open-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Button colors by rarity */
.open-button.common:hover:not(:disabled) { background-color: #a6adc8; color: #1e1e2e; }
.open-button.uncommon:hover:not(:disabled) { background-color: #a6e3a1; color: #1e1e2e; }
.open-button.rare:hover:not(:disabled) { background-color: #89b4fa; color: #1e1e2e; }
.open-button.epic:hover:not(:disabled) { background-color: #cba6f7; color: #1e1e2e; }
.open-button.legendary:hover:not(:disabled) { background-color: #f9e2af; color: #1e1e2e; }
/* Crates inventory section (bottom part) */
.crates-inventory-section {
height: 40%;
display: flex;
flex-direction: column;
padding: 10px;
overflow-y: auto;
}
.inventory-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.inventory-header h3 {
font-size: 14px;
font-weight: 600;
margin: 0;
opacity: 0.9;
}
.total-crates {
font-size: 12px;
opacity: 0.7;
}
.crates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
padding-bottom: 15px;
}
.crate-item {
background-color: rgba(49, 50, 68, 0.4);
border-radius: 8px;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
position: relative;
gap: 8px;
}
.crate-item::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
/* Crate item colors */
.crate-item.common::after { background-color: #a6adc8; }
.crate-item.uncommon::after { background-color: #a6e3a1; }
.crate-item.rare::after { background-color: #89b4fa; }
.crate-item.epic::after { background-color: #cba6f7; }
.crate-item.legendary::after {
background: linear-gradient(90deg, #f9e2af, #fab387, #f9e2af);
background-size: 200% auto;
animation: shimmer 2s infinite;
}
.crate-item:hover:not(.empty) {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.crate-item.selected {
border-color: var(--accent-color);
background-color: rgba(49, 50, 68, 0.6);
box-shadow: 0 0 10px rgba(137, 180, 250, 0.4);
}
.crate-item.empty {
opacity: 0.4;
cursor: not-allowed;
}
.crate-item-icon {
font-size: 24px;
position: relative;
}
.crate-item.common .crate-item-icon { color: #a6adc8; }
.crate-item.uncommon .crate-item-icon { color: #a6e3a1; }
.crate-item.rare .crate-item-icon { color: #89b4fa; }
.crate-item.epic .crate-item-icon { color: #cba6f7; }
.crate-item.legendary .crate-item-icon { color: #f9e2af; }
.crate-count {
position: absolute;
top: -5px;
right: -10px;
background-color: rgba(30, 30, 46, 0.9);
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid currentColor;
}
.crate-item-name {
font-size: 11px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
/* Responsive adjustments */
@media (max-width: 500px) {
.crate-display {
flex-direction: column;
text-align: center;
gap: 10px;
}
.crate-info {
text-align: center;
}
}
@@ -0,0 +1,133 @@
import React, { useState } from 'react';
import './Crates.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBox, faBoxOpen, faArrowRight, faArrowLeft, faUnlock } from '@fortawesome/free-solid-svg-icons';
function Crates() {
// Mock crate data - would come from backend
const crateTypes = [
{ id: 'common', name: 'Common Crate', count: 5, color: '#a6adc8', description: 'Contains common cursors' },
{ id: 'uncommon', name: 'Uncommon Crate', count: 3, color: '#a6e3a1', description: 'Contains uncommon and common cursors' },
{ id: 'rare', name: 'Rare Crate', count: 2, color: '#89b4fa', description: 'Contains rare and uncommon cursors' },
{ id: 'epic', name: 'Epic Crate', count: 1, color: '#cba6f7', description: 'Contains epic, rare and uncommon cursors' },
{ id: 'legendary', name: 'Legendary Crate', count: 0, color: '#f9e2af', description: 'Contains legendary and epic cursors' }
];
const [selectedCrateIndex, setSelectedCrateIndex] = useState(0);
const [isOpening, setIsOpening] = useState(false);
const selectedCrate = crateTypes[selectedCrateIndex];
// Navigate through available crates
const navigateCrate = (direction) => {
// Find next available crate with count > 0
let newIndex = selectedCrateIndex;
let attempts = 0;
while (attempts < crateTypes.length) {
newIndex = (newIndex + direction + crateTypes.length) % crateTypes.length;
if (crateTypes[newIndex].count > 0) {
break;
}
attempts++;
}
if (crateTypes[newIndex].count > 0) {
setSelectedCrateIndex(newIndex);
}
};
// Simulate opening a crate
const openCrate = () => {
if (selectedCrate.count <= 0) return;
setIsOpening(true);
// Simulate API call and animation time
setTimeout(() => {
setIsOpening(false);
}, 2000);
};
return (
<div className="crates-container">
{/* Current selected crate section */}
<div className="current-crate-section">
<div className={`current-crate ${selectedCrate.id}`}>
<div className="crate-navigation">
<button
className="nav-button"
onClick={() => navigateCrate(-1)}
disabled={crateTypes.filter(c => c.count > 0).length <= 1}
>
<FontAwesomeIcon icon={faArrowLeft} />
</button>
<div className="crate-display">
<div className={`crate-image ${isOpening ? 'opening' : ''}`}>
<FontAwesomeIcon
icon={isOpening ? faBoxOpen : faBox}
className="crate-icon"
/>
</div>
<div className="crate-info">
<h2 className="crate-name">{selectedCrate.name}</h2>
<div className="crate-stats">
<span>You have: <strong>{selectedCrate.count}</strong></span>
</div>
</div>
</div>
<button
className="nav-button"
onClick={() => navigateCrate(1)}
disabled={crateTypes.filter(c => c.count > 0).length <= 1}
>
<FontAwesomeIcon icon={faArrowRight} />
</button>
</div>
<div className="crate-actions">
<button
className={`open-button ${selectedCrate.id}`}
onClick={openCrate}
disabled={selectedCrate.count === 0 || isOpening}
>
{isOpening ? 'Opening...' : (
<>
<FontAwesomeIcon icon={faUnlock} /> Open Crate
</>
)}
</button>
</div>
</div>
</div>
{/* Crates inventory section */}
<div className="crates-inventory-section">
<div className="inventory-header">
<h3>Your Crates</h3>
<span className="total-crates">
{crateTypes.reduce((total, crate) => total + crate.count, 0)} total
</span>
</div>
<div className="crates-grid">
{crateTypes.map((crate, index) => (
<div
key={crate.id}
className={`crate-item ${crate.id} ${index === selectedCrateIndex ? 'selected' : ''} ${crate.count === 0 ? 'empty' : ''}`}
onClick={() => crate.count > 0 && setSelectedCrateIndex(index)}
>
<div className="crate-item-icon">
<FontAwesomeIcon icon={faBox} />
{crate.count > 1 && <span className="crate-count">{crate.count}</span>}
</div>
<div className="crate-item-name">{crate.name}</div>
</div>
))}
</div>
</div>
</div>
);
}
export default Crates;
@@ -0,0 +1,358 @@
/* Cursors Module Styles */
.cursors-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Equipped cursor banner */
.equipped-cursor-banner {
height: 25%;
min-height: 120px;
max-height: 150px;
display: flex;
background-color: rgba(30, 30, 46, 0.3);
border-bottom: 2px solid rgba(49, 50, 68, 0.6);
padding: 10px;
margin-bottom: 10px;
}
.current-preview {
max-width: 80%;
max-height: 80%;
object-fit: contain;
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.3));
transition: all 0.3s ease;
}
.current-preview.legendary {
animation: float 3s ease-in-out infinite;
filter: drop-shadow(0 0 8px rgba(249, 226, 175, 0.4));
}
@keyframes float {
0% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0px); }
}
.equipped-cursor-details {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
}
.equipped-cursor-info {
display: flex;
flex-direction: column;
}
.equipped-label {
font-size: 12px;
font-weight: 600;
color: rgba(205, 214, 244, 0.6);
letter-spacing: 1px;
margin-bottom: 4px;
}
.equipped-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 5px;
}
.equipped-rarity {
font-size: 14px;
font-weight: 500;
opacity: 0.8;
}
.next-button {
align-self: flex-start;
background-color: rgba(65, 66, 88, 0.8);
border: none;
border-radius: 4px;
padding: 8px 15px;
color: var(--text-color);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.next-button:hover {
background-color: var(--accent-color);
color: #1e1e2e;
transform: translateY(-2px);
}
/* Library controls section */
.cursors-controls {
display: flex;
flex-direction: column;
gap: 10px;
padding: 5px 10px 15px 10px;
border-bottom: 1px solid rgba(49, 50, 68, 0.4);
}
.search-box {
position: relative;
margin-bottom: 8px;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: rgba(205, 214, 244, 0.5);
pointer-events: none;
}
.search-input {
width: 100%;
background-color: rgba(49, 50, 68, 0.3);
border: 1px solid rgba(49, 50, 68, 0.6);
border-radius: 4px;
color: var(--text-color);
padding: 8px 10px 8px 32px;
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.search-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 5px rgba(137, 180, 250, 0.3);
}
.filter-controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
overflow-x: auto;
padding-bottom: 5px;
}
.filter-button {
background-color: rgba(49, 50, 68, 0.4);
border: none;
border-radius: 4px;
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
border-left: 3px solid transparent;
}
.filter-button:hover {
background-color: rgba(65, 66, 88, 0.6);
}
.filter-button.active {
background-color: rgba(65, 66, 88, 0.8);
color: var(--accent-color);
}
/* Rarity-based styling for filter buttons */
.filter-button.common {
border-left-color: #a6adc8;
}
.filter-button.uncommon {
border-left-color: #a6e3a1;
}
.filter-button.rare {
border-left-color: #89b4fa;
}
.filter-button.epic {
border-left-color: #cba6f7;
}
.filter-button.legendary {
border-left-color: #f9e2af;
}
/* Main cursor library */
.cursor-library {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 0 10px;
scrollbar-width: thin;
margin-top: 5px;
}
.rarity-section {
display: flex;
flex-direction: column;
margin-bottom: 20px;
}
.rarity-header {
display: flex;
align-items: center;
margin-bottom: 10px;
flex-wrap: nowrap;
}
.rarity-title {
font-size: 14px;
font-weight: 600;
margin: 0;
white-space: nowrap;
margin-right: 10px;
}
.rarity-line {
flex: 1;
height: 1px;
opacity: 0.3;
}
/* Rarity colors */
.equipped-name.common, .equipped-rarity.common, .rarity-line.common { background-color: #a6adc8; }
.equipped-name.uncommon, .equipped-rarity.uncommon, .rarity-line.uncommon { background-color: #a6e3a1; }
.equipped-name.rare, .equipped-rarity.rare, .rarity-line.rare { background-color: #89b4fa; }
.equipped-name.epic, .equipped-rarity.epic, .rarity-line.epic { background-color: #cba6f7; }
.equipped-name.legendary, .equipped-rarity.legendary, .rarity-line.legendary { background-color: #f9e2af; }
/* Horizontal scrolling cursor list */
.cursor-list {
display: flex;
gap: 12px;
overflow-x: auto;
padding: 5px 0 15px 0;
scrollbar-width: thin;
margin-bottom: 5px;
}
.cursor-card {
min-width: 150px;
max-width: 180px;
background-color: rgba(49, 50, 68, 0.4);
border-radius: 8px;
overflow: hidden;
transition: all 0.2s ease;
cursor: pointer;
border: 1px solid rgba(49, 50, 68, 0.4);
position: relative;
flex: 0 0 auto;
}
/* Circular rarity indicator in top right */
.cursor-card::before {
content: '';
position: absolute;
top: 8px;
right: 8px;
width: 12px;
height: 12px;
border-radius: 50%;
z-index: 5;
background-color: currentColor;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
}
/* Rarity colors */
.cursor-card.common { color: #a6adc8; }
.cursor-card.uncommon { color: #a6e3a1; }
.cursor-card.rare { color: #89b4fa; }
.cursor-card.epic { color: #cba6f7; }
.cursor-card.legendary { color: #f9e2af; }
/* Hover and active states */
.cursor-card:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
border-color: currentColor;
z-index: 5;
}
.cursor-card:active {
transform: translateY(0) scale(0.98);
}
.cursor-card.active {
border-color: currentColor;
box-shadow: 0 0 12px currentColor;
}
/* Preview container */
.cursor-preview-container {
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(30, 30, 46, 0.5);
padding: 12px;
}
.cursor-preview-img {
max-width: 80%;
max-height: 80%;
object-fit: contain;
transition: transform 0.3s ease;
}
.cursor-card:hover .cursor-preview-img {
transform: scale(1.1);
}
/* Cursor details */
.cursor-details {
padding: 10px;
display: flex;
flex-direction: column;
gap: 3px;
}
.cursor-name {
font-weight: 500;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cursor-rarity-badge {
display: inline-block;
font-size: 11px;
text-transform: capitalize;
opacity: 0.7;
}
/* Animation for legendary items */
.cursor-card.legendary::before {
animation: legendaryGlow 2s infinite;
}
@keyframes legendaryGlow {
0% { opacity: 0.4; }
50% { opacity: 0.9; }
100% { opacity: 0.4; }
}
/* Empty states */
.no-cursors, .no-results {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(49, 50, 68, 0.2);
border-radius: 8px;
padding: 20px;
text-align: center;
color: var(--text-color);
opacity: 0.7;
font-size: 14px;
}
@@ -0,0 +1,144 @@
import React, { useState } from 'react';
import './Cursors.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSort, faFilter, faSortAmountDown, faSearch } from '@fortawesome/free-solid-svg-icons';
function Cursors({ cursors, currentCursorIndex, selectCursor, nextCursor }) {
const [filter, setFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const currentCursor = cursors.length > 0 && currentCursorIndex < cursors.length
? cursors[currentCursorIndex]
: null;
// This will eventually come from backend with proper data
const getRarity = (index) => {
const rarities = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
return rarities[index % rarities.length];
};
// Group cursors by rarity
const cursorsByRarity = cursors.reduce((acc, cursor, index) => {
const rarity = getRarity(index);
if (!acc[rarity]) acc[rarity] = [];
acc[rarity].push({ ...cursor, index, rarity });
return acc;
}, {});
// Filter logic with search
const filteredCursors = Object.entries(cursorsByRarity)
.filter(([rarity, _]) => filter === 'all' || rarity === filter)
.map(([rarity, cursorsOfRarity]) => [
rarity,
cursorsOfRarity.filter(c =>
!searchTerm || c.name.toLowerCase().includes(searchTerm.toLowerCase())
)
])
.filter(([_, cursors]) => cursors.length > 0);
return (
<div className="cursors-container">
{/* Current cursor equipped display */}
{currentCursor && (
<div className="equipped-cursor-banner">
<div className="equipped-cursor-image">
<img
src={currentCursor.path}
alt={currentCursor.name}
className={`current-preview ${getRarity(currentCursorIndex)}`}
/>
</div>
<div className="equipped-cursor-details">
<div className="equipped-cursor-info">
<div className="equipped-label">EQUIPPED</div>
<div className={`equipped-name ${getRarity(currentCursorIndex)}`}>{currentCursor.name}</div>
<div className={`equipped-rarity ${getRarity(currentCursorIndex)}`}>
{getRarity(currentCursorIndex).toUpperCase()}
</div>
</div>
<button className="next-button" onClick={nextCursor}>Next Cursor</button>
</div>
</div>
)}
{/* Library controls */}
<div className="cursors-controls">
<div className="search-box">
<FontAwesomeIcon icon={faSearch} className="search-icon" />
<input
type="text"
placeholder="Search cursors..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="filter-controls">
<button
className={`filter-button ${filter === 'all' ? 'active' : ''}`}
onClick={() => setFilter('all')}
>
All
</button>
{Object.keys(cursorsByRarity).map(rarity => (
<button
key={rarity}
className={`filter-button ${filter === rarity ? 'active' : ''} ${rarity}`}
onClick={() => setFilter(rarity)}
>
{rarity.charAt(0).toUpperCase() + rarity.slice(1)}
</button>
))}
</div>
</div>
{/* Cursor library */}
{cursors.length > 0 ? (
<div className="cursor-library">
{filteredCursors.map(([rarity, cursorsInRarity]) => (
<div className="rarity-section" key={rarity}>
<div className={`rarity-header ${rarity}`}>
<h3 className="rarity-title">
{rarity.toUpperCase()} ({cursorsInRarity.length})
</h3>
<div className={`rarity-line ${rarity}`}></div>
</div>
<div className="cursor-list">
{cursorsInRarity.map((cursor) => (
<div
key={cursor.index}
className={`cursor-card ${cursor.index === currentCursorIndex ? 'active' : ''} ${cursor.rarity}`}
onClick={() => selectCursor(cursor.index)}
>
<div className="cursor-preview-container">
<img
src={cursor.path}
alt={cursor.name}
className="cursor-preview-img"
/>
</div>
<div className="cursor-details">
<span className="cursor-name">{cursor.name}</span>
<span className="cursor-rarity-badge">{cursor.rarity}</span>
</div>
</div>
))}
</div>
</div>
))}
{filteredCursors.length === 0 && (
<div className="no-results">No cursors match your search</div>
)}
</div>
) : (
<div className="no-cursors">
<p>No cursors found in assets directory</p>
</div>
)}
</div>
);
}
export default Cursors;
@@ -0,0 +1,359 @@
/* Inventory Module Styles */
.inventory-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* Search and filter controls */
.inventory-controls {
display: flex;
justify-content: space-between;
padding: 8px 10px;
background-color: rgba(49, 50, 68, 0.4);
border-radius: 8px;
margin-top: 2%;
border: 1px solid rgba(49, 50, 68, 0.8);
}
.search-bar {
flex: 1;
margin-right: 10px;
}
.search-bar input {
width: 100%;
padding: 8px 12px;
border-radius: 4px;
border: 1px solid rgba(49, 50, 68, 0.8);
background-color: rgba(30, 30, 46, 0.7);
color: #cdd6f4;
font-size: 14px;
}
.search-bar input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.3);
}
.search-bar input::placeholder {
color: rgba(205, 214, 244, 0.5);
}
.rarity-filter {
width: 50px;
position: relative;
display: flex;
align-items: center;
}
.rarity-filter select {
width: 100%;
padding: 8px;
border-radius: 4px;
border: 1px solid rgba(49, 50, 68, 0.8);
background-color: rgba(30, 30, 46, 0.7);
color: transparent;
text-indent: -9999px;
font-size: 14px;
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
background-repeat: no-repeat;
background-position: right 4px center;
cursor: pointer;
}
.rarity-filter .rarity-circle {
position: absolute;
aspect-ratio: 1/1;
width: 30%;
margin-left: 8px;
border-radius: 50%;
pointer-events: none;
z-index: 1;
}
/* Rarity colors for circle indicators */
.rarity-circle.all {
background: linear-gradient(45deg, #a6adc8, #a6e3a1, #89b4fa, #cba6f7, #f9e2af);
}
.rarity-circle.common { background-color: #a6adc8; }
.rarity-circle.uncommon { background-color: #a6e3a1; }
.rarity-circle.rare { background-color: #89b4fa; }
.rarity-circle.epic { background-color: #cba6f7; }
.rarity-circle.legendary {
background-color: #f9e2af;
box-shadow: 0 0 5px #f9e2af;
}
/* Style dropdown options to show text and circles */
.rarity-filter select option {
color: white;
position: relative;
}
.rarity-filter .rarity-option {
display: flex;
align-items: center;
gap: 8px;
color: white;
}
.rarity-filter select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.3);
}
/* Equipped cursor display */
.equipped-cursor {
display: flex;
background-color: rgba(49, 50, 68);
border-radius: 8px;
padding: 10px;
min-height: 25%;
max-height: 25%;
border: 1px solid rgba(49, 50, 68, 0.8);
margin-top: 10px;
}
.equipped-cursor-image {
aspect-ratio: 1/1;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(30, 30, 46, 0.5);
border-radius: 6px;
border: 1px solid rgba(49, 50, 68, 0.6);
margin-right: 10px;
}
.equipped-image {
max-width: 80%;
max-height: 80%;
object-fit: contain;
}
.equipped-cursor-info {
display: flex;
flex-direction: column;
}
.equipped-cursor-info h3 {
font-size: 14px;
margin: 0 0 3px 0;
opacity: 0.8;
}
.equipped-cursor-name {
font-size: 16px;
font-weight: 500;
}
.equipped-cursor-rarity {
font-size: 12px;
opacity: 0.8;
}
/* Minecraft-style inventory */
.cute-inventory {
min-height: 58%;
max-height: 58%;
display: flex;
overflow-y: auto;
flex-direction: column;
align-items: center;
background-color: rgba(30, 30, 46, 0.3);
border-radius: 8px;
box-sizing: border-box;
margin-top: 2%;
border: 1px solid rgba(49, 50, 68, 0.6);
}
.inventory-grid {
display: grid;
box-sizing: border-box;
width: 100%;
grid-template-columns: repeat(auto-fill, minmax(9vh, 1fr));
}
.inventory-slot {
aspect-ratio: 1;
background-color: rgba(49, 50, 68, 0.5);
border: 2px solid rgba(30, 30, 46, 0.8);
border-radius: 1vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0.5vh;
position: relative;
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
overflow: hidden;
}
/* Rarity indicator as small circle in top right */
.inventory-slot.has-item {
cursor: pointer;
position: relative;
border-color: rgba(30, 30, 46, 0.8);
}
/* Small circle rarity indicator in top right */
.inventory-slot.has-item::after {
content: '';
position: absolute;
top: 5px;
right: 5px;
width: 10px;
height: 10px;
border-radius: 50%;
z-index: 5;
}
.inventory-slot.has-item.common::after {
background-color: #a6adc8;
}
.inventory-slot.has-item.uncommon::after {
background-color: #a6e3a1;
}
.inventory-slot.has-item.rare::after {
background-color: #89b4fa;
}
.inventory-slot.has-item.epic::after {
background-color: #cba6f7;
}
.inventory-slot.has-item.legendary::after {
background-color: #f9e2af;
box-shadow: 0 0 5px rgba(249, 226, 175, 0.8);
animation: pulseDot 2s infinite;
}
@keyframes pulseDot {
0% { transform: scale(1); box-shadow: 0 0 5px rgba(249, 226, 175, 0.5); }
50% { transform: scale(1.2); box-shadow: 0 0 8px rgba(249, 226, 175, 0.8); }
100% { transform: scale(1); box-shadow: 0 0 5px rgba(249, 226, 175, 0.5); }
}
.inventory-slot.has-item:hover {
transform: scale(1.08);
z-index: 5;
}
.inventory-slot.has-item:active {
transform: scale(0.95);
}
/* Selected item overrides rarity glow */
.inventory-slot.selected {
border-color: var(--accent-color) !important;
box-shadow: 0 0 12px var(--accent-color) !important;
z-index: 10;
animation: selectedPulse 1.5s infinite;
}
@keyframes selectedPulse {
0% { box-shadow: 0 0 8px var(--accent-color) !important; }
50% { box-shadow: 0 0 15px var(--accent-color) !important; }
100% { box-shadow: 0 0 8px var(--accent-color) !important; }
}
/* Shine effect when clicked */
.inventory-slot.has-item:active::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 70%);
opacity: 0.6;
transform: scale(0);
animation: clickRipple 0.5s ease-out;
}
@keyframes clickRipple {
0% { transform: scale(0); opacity: 0.6; }
100% { transform: scale(1); opacity: 0; }
}
.inventory-item {
width: 70%;
height: 70%;
object-fit: contain;
}
/* Tooltip that appears on hover */
.inventory-tooltip {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(30, 30, 46, 0.95);
border-radius: 4px;
padding: 4px 8px;
min-width: 80px;
z-index: 10;
display: none;
flex-direction: column;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
pointer-events: none;
white-space: nowrap;
}
.inventory-slot:hover .inventory-tooltip {
display: flex;
}
.tooltip-name {
font-size: 12px;
font-weight: 500;
}
.tooltip-rarity {
font-size: 10px;
opacity: 0.8;
}
/* Rarity colors */
.common, .common.equipped-cursor-name, .tooltip-name.common { color: #a6adc8; }
.uncommon, .uncommon.equipped-cursor-name, .tooltip-name.uncommon { color: #a6e3a1; }
.rare, .rare.equipped-cursor-name, .tooltip-name.rare { color: #89b4fa; }
.epic, .epic.equipped-cursor-name, .tooltip-name.epic { color: #cba6f7; }
.legendary, .legendary.equipped-cursor-name, .tooltip-name.legendary { color: #f9e2af; }
/* Legendary glow effect */
.inventory-slot.selected .inventory-item.legendary {
filter: drop-shadow(0 0 3px #f9e2af);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { filter: drop-shadow(0 0 2px rgba(249, 226, 175, 0.5)); }
50% { filter: drop-shadow(0 0 4px rgba(249, 226, 175, 0.8)); }
100% { filter: drop-shadow(0 0 2px rgba(249, 226, 175, 0.5)); }
}
.inventory-footer {
width: 100%;
display: flex;
justify-content: center;
padding: 5px 0;
}
.cursor-count {
font-size: 12px;
color: var(--text-color);
opacity: 0.7;
}
@@ -0,0 +1,157 @@
import React, { useState, useEffect } from 'react';
import './Inventory.css';
function Inventory({ cursors, currentCursorIndex, selectCursor }) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedRarity, setSelectedRarity] = useState('all');
const [filteredSlots, setFilteredSlots] = useState([]);
// Minecraft inventory typically has 27 slots (9x3)
const totalSlots = 36; // 9x4 grid
// This will eventually come from backend with proper data
const cursorRarities = {
common: 'Common',
uncommon: 'Uncommon',
rare: 'Rare',
epic: 'Epic',
legendary: 'Legendary'
};
// Assign mock rarities to cursors for demonstration
const getCursorRarity = (index) => {
const rarities = Object.keys(cursorRarities);
return rarities[index % rarities.length];
};
// Get currently selected cursor
const currentCursor = cursors.length > 0 && currentCursorIndex < cursors.length
? cursors[currentCursorIndex]
: null;
// Create array of slots for the inventory grid
const inventorySlots = Array(totalSlots).fill(null).map((_, slotIndex) => {
// Map cursor to slot if available
const cursorIndex = slotIndex < cursors.length ? slotIndex : null;
const cursor = cursorIndex !== null ? cursors[cursorIndex] : null;
const rarity = cursor ? getCursorRarity(cursorIndex) : null;
return {
slotIndex,
cursor,
cursorIndex,
rarity
};
});
// Apply filters to inventory slots
useEffect(() => {
// First, filter the slots that match our criteria
let filtered = inventorySlots.filter(slot => {
// Filter by search query
const matchesSearch = !searchQuery ||
(slot.cursor && slot.cursor.name.toLowerCase().includes(searchQuery.toLowerCase()));
// Filter by rarity
const matchesRarity = selectedRarity === 'all' ||
(slot.rarity && slot.rarity === selectedRarity);
return matchesSearch && matchesRarity;
});
// Create a new array for the filtered slots, prioritizing slots with items
const filledSlots = filtered.filter(slot => slot.cursor);
const emptySlots = Array(totalSlots - filledSlots.length).fill(null)
.map((_, index) => ({
slotIndex: filledSlots.length + index,
cursor: null,
cursorIndex: null,
rarity: null
}));
// Combine filled slots at the beginning and empty ones at the end
const result = [...filledSlots, ...emptySlots];
setFilteredSlots(result);
}, [searchQuery, selectedRarity, inventorySlots]);
return (
<div className="inventory-container">
{/* Search and Filter Controls */}
{/* Equipped cursor display */}
{currentCursor && (
<div className="equipped-cursor">
<div className="equipped-cursor-image">
<img
src={currentCursor.path}
alt={currentCursor.name}
className="equipped-image"
/>
</div>
<div className="equipped-cursor-info">
<h3>Equipped</h3>
<span className={`equipped-cursor-name ${getCursorRarity(currentCursorIndex)}`}>
{currentCursor.name}
</span>
<span className="equipped-cursor-rarity">
{cursorRarities[getCursorRarity(currentCursorIndex)]}
</span>
</div>
</div>
)}
<div className="inventory-controls">
<div className="search-bar">
<input
type="text"
placeholder="Search cursors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="rarity-filter">
<div className={`rarity-circle ${selectedRarity}`}></div>
<select
value={selectedRarity}
onChange={(e) => setSelectedRarity(e.target.value)}
aria-label="Filter by rarity"
>
<option value="all">All Rarities</option>
{Object.keys(cursorRarities).map(rarity => (
<option key={rarity} value={rarity}>{cursorRarities[rarity]}</option>
))}
</select>
</div>
</div>
{/* Minecraft-like inventory grid */}
<div className="cute-inventory">
<div className="inventory-grid">
{filteredSlots.map((slot) => (
<div
key={slot.slotIndex}
className={`inventory-slot ${slot.cursor ? 'has-item' : ''} ${slot.cursorIndex === currentCursorIndex ? 'selected' : ''} ${slot.rarity || ''}`}
onClick={() => slot.cursor && selectCursor(slot.cursorIndex)}
>
{slot.cursor && (
<>
<img
src={slot.cursor.path}
alt={slot.cursor.name}
className={`inventory-item ${slot.rarity}`}
/>
<div className="inventory-tooltip">
<span className={`tooltip-name ${slot.rarity}`}>{slot.cursor.name}</span>
<span className="tooltip-rarity">{cursorRarities[slot.rarity]}</span>
</div>
</>
)}
</div>
))}
</div>
</div>
</div>
);
}
export default Inventory;
@@ -0,0 +1,75 @@
/* Settings Module Styles */
.settings-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 15px;
}
.settings-group {
background-color: var(--button-bg, #313244);
border-radius: 8px;
padding: 15px;
color: var(--text-color, #cdd6f4);
}
.settings-group h3 {
font-size: 16px;
font-weight: 500;
margin: 0 0 12px 0;
color: var(--accent-color, #89b4fa);
}
.setting-item {
padding: 8px 0;
display: flex;
align-items: center;
}
.setting-item label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 14px;
}
.setting-item input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0;
appearance: none;
background-color: var(--modal-bg, #181825);
border: 1px solid var(--text-color, #cdd6f4);
border-radius: 3px;
outline: none;
cursor: pointer;
position: relative;
}
.setting-item input[type="checkbox"]:checked {
background-color: var(--accent-color, #89b4fa);
border-color: var(--accent-color, #89b4fa);
}
.setting-item input[type="checkbox"]:checked::after {
content: '✓';
font-size: 12px;
color: var(--bg-color, #1e1e2e);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.settings-group p {
margin: 5px 0;
font-size: 14px;
opacity: 0.8;
}
@@ -0,0 +1,65 @@
import React from 'react';
import './Settings.css';
function Settings({ settings, setSettings }) {
// Handler for checkbox changes
const handleSettingChange = (settingName) => {
setSettings({
...settings,
[settingName]: !settings[settingName]
});
};
return (
<div className="settings-container">
<h2 className="content-title">Settings</h2>
<div className="settings-content">
<div className="settings-group">
<h3>Application Settings</h3>
<div className="setting-item">
<label>
<input
type="checkbox"
checked={settings.startWithWindows}
onChange={() => handleSettingChange('startWithWindows')}
/>
Start with Windows
</label>
</div>
<div className="setting-item">
<label>
<input
type="checkbox"
checked={settings.minimizeToTrayOnClose}
onChange={() => handleSettingChange('minimizeToTrayOnClose')}
/>
Minimize to tray on close
</label>
</div>
</div>
<div className="settings-group">
<h3>Cursor Settings</h3>
<div className="setting-item">
<label>
<input
type="checkbox"
checked={settings.autoApplyCursorOnSelection}
onChange={() => handleSettingChange('autoApplyCursorOnSelection')}
/>
Auto-apply cursor on selection
</label>
</div>
</div>
<div className="settings-group">
<h3>About</h3>
<p>Cursor Rush v0.1.0</p>
<p>A cross-platform application for customizing your cursor.</p>
</div>
</div>
</div>
);
}
export default Settings;
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

+68
View File
@@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
+9
View File
@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
-828
View File
@@ -1,828 +0,0 @@
use iced::{
widget::{button, text, Column, Row, container, image as iced_image},
Element, Length, Subscription, Task, Theme,
futures::{channel::mpsc},
};
use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
thread,
time::Duration,
};
use ::image::{GenericImageView, RgbaImage, open as image_open};
use walkdir::WalkDir;
use tray_icon::{
TrayIconBuilder,
TrayIcon, // Import the TrayIcon type itself
TrayIconEvent, // Import TrayIconEvent for handling left/right clicks
MouseButton, // Import MouseButton to distinguish between left and right clicks
menu::{Menu, MenuEvent},
};
use global_hotkey::{
GlobalHotKeyManager,
hotkey::HotKey as GlobalHotKey,
HotKeyState,
};
// Import our platform-specific module
mod platform;
pub mod steam;
// Define a struct to hold cursor information
#[derive(Clone)]
struct CursorInfo {
name: String,
path: PathBuf,
}
// We'll use a simpler approach for the cursor overlay
// Define menu item IDs
const NEXT_CURSOR_ID: &str = "next_cursor";
const SHOW_WINDOW_ID: &str = "show_window";
const QUIT_ID: &str = "quit";
// Shared state between the UI and the tray icon
struct SharedState {
current_index: usize,
cursors: Vec<CursorInfo>,
current_cursor_image: Option<RgbaImage>,
running: bool,
window_visible: bool, // Track if the window is visible
}
// Our application state
struct CursorRush {
cursors: Vec<CursorInfo>,
current_index: usize,
shared_state: Option<Arc<Mutex<SharedState>>>,
main_window: Option<iced::window::Id>, // Store the main window ID
tray_icon: Option<TrayIcon>, // Store the TrayIcon object to keep it alive
}
// Make sure the default impl initializes tray_icon to None
impl Default for CursorRush {
fn default() -> Self {
Self {
cursors: Vec::new(),
current_index: 0,
shared_state: None,
main_window: None,
tray_icon: None, // Initialize as None
}
}
}
// Messages that can be sent in our application
#[derive(Debug, Clone)]
enum Message {
NextCursor,
SelectCursor(usize),
Quit,
TrayEvent, // Used as a no-op message
CloseRequested(iced::window::Id), // Message for window close requests with the real window ID
ShowWindow, // Message to show the window from tray
WindowHidden, // Message indicating window was successfully hidden
WindowShown, // Message indicating window was successfully shown
}
// Function to load all PNG files from the assets directory
fn load_cursor_files() -> Vec<CursorInfo> {
let mut cursors = Vec::new();
println!("Looking for cursor PNG files in assets directory...");
// Check if assets directory exists
if !std::path::Path::new("assets").exists() {
println!("Assets directory not found!");
return cursors;
}
for entry in WalkDir::new("assets").into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
println!("Found file: {}", path.display());
if path.is_file() && path.extension().map_or(false, |ext| ext == "png") {
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(CursorInfo {
name,
path: path.to_path_buf(),
});
}
}
println!("Found {} cursor PNG files", cursors.len());
// Sort cursors by name for consistent ordering
cursors.sort_by(|a, b| a.name.cmp(&b.name));
cursors
}
// Function to validate a cursor image
fn validate_cursor_image(path: &Path) -> bool {
// Load the image to validate it
match image_open(path) {
Ok(img) => {
let (width, height) = img.dimensions();
// Ensure reasonable dimensions
if width > 128 || height > 128 || width == 0 || height == 0 {
eprintln!("Image dimensions not suitable for cursor: {}x{}", width, height);
return false;
}
true
},
Err(e) => {
eprintln!("Failed to open image {}: {}", path.display(), e);
false
},
}
}
// Apply the cursor at the given path
fn apply_cursor(path: &Path) -> bool {
// Validate the cursor image
if validate_cursor_image(path) {
// Use platform-specific cursor image update (not hiding system cursor again)
if platform::update_cursor_image(path) {
println!("Successfully updated cursor image");
return true;
} else {
eprintln!("Failed to update cursor image");
}
}
false
}
// Global channel for tray events
use once_cell::sync::Lazy;
static TRAY_SENDER: Lazy<std::sync::Mutex<Option<mpsc::UnboundedSender<Message>>>> =
Lazy::new(|| std::sync::Mutex::new(None));
// Top-level subscription function to listen for events
fn subscription(_state: &CursorRush) -> Subscription<Message> {
// Create a vector to hold all subscriptions
let mut subs = vec![
// Listen for events but filter out mouse movement events to avoid spamming
iced::event::listen().map(|event| {
match event {
// Skip cursor movement events to avoid spamming
iced::Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
// Return TrayEvent which is essentially a no-op
Message::TrayEvent
},
// Handle other events
_ => Message::TrayEvent, // Use TrayEvent as a no-op message
}
}),
// Specifically subscribe to window close requests
// This is the key to handling the X button properly
iced::window::close_requests().map(|window_id| {
println!("Window close requested via X button for window ID: {:?}", window_id);
Message::CloseRequested(window_id)
}),
];
// Add a subscription for tray events
// We'll use a simple channel that the tray icon can send messages to
let tray_subscription = Subscription::run(
// Stream generator function
|| {
// Create a channel that the tray icon can send messages to
let (tx, rx) = mpsc::unbounded::<Message>();
// Store the sender in the global state
*TRAY_SENDER.lock().unwrap() = Some(tx);
// Return the receiver as a stream
rx
}
);
subs.push(tray_subscription);
// Combine all subscriptions
Subscription::batch(subs)
}
// Boot function to initialize the application
fn boot() -> (CursorRush, Task<Message>) {
// Load cursor files from assets directory
let cursors = load_cursor_files();
if cursors.is_empty() {
eprintln!("No cursor PNG files found in assets directory!");
return (CursorRush::default(), Task::none());
}
// Load the first cursor image
let current_cursor_image = if let Some(cursor) = cursors.first() {
match image_open(&cursor.path) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => None,
}
} else {
None
};
// Hide the system cursor once at startup
if !platform::hide_system_cursor() {
eprintln!("Failed to hide system cursor at startup!");
}
// Apply the first cursor by default
if let Some(cursor) = cursors.first() {
apply_cursor(&cursor.path);
// No need to call render_cursor anymore as the mouse hook handles rendering
}
// Create shared state
let shared_state = Arc::new(Mutex::new(SharedState {
current_index: 0,
cursors: cursors.clone(),
current_cursor_image,
running: true,
window_visible: true, // Window is initially visible
}));
// Initialize the application state with loaded cursors
let mut state = CursorRush::default();
state.cursors = cursors.clone(); // Clone here
state.shared_state = Some(shared_state.clone());
// Get the global tray sender (will be initialized by the subscription)
// We'll set it up in the state after the subscription is running
// Set up the tray icon AND store the result in the state
// The tray sender will be available from the global state
state.tray_icon = setup_tray_icon(&state.cursors, shared_state.clone()); // Pass the cloned cursors
if state.tray_icon.is_none() {
eprintln!("Failed to initialize tray icon, application might not function correctly in background.");
// Optionally, decide how to handle this failure (e.g., exit or run without tray)
}
// We'll get the real window ID from the CloseRequested event
// No need to create a window ID here
// Set up global hotkey manager
let hotkey_manager = match GlobalHotKeyManager::new() {
Ok(manager) => manager,
Err(e) => {
eprintln!("Failed to create hotkey manager: {}", e);
// Continue without hotkey support
GlobalHotKeyManager::new().expect("Critical failure: could not create hotkey manager")
}
};
// Register Ctrl+Shift+F12 as the exit hotkey (uncommon but easy to remember)
use global_hotkey::hotkey::{Code, Modifiers};
// We don't need a unique ID for the hotkey in this simple case
let hotkey = GlobalHotKey::new(
Some(Modifiers::CONTROL | Modifiers::SHIFT),
Code::F12,
);
// Register the hotkey with better error handling
match hotkey_manager.register(hotkey) {
Ok(_) => {
println!("Successfully registered Ctrl+Shift+F12 as exit hotkey");
},
Err(e) => {
eprintln!("Failed to register hotkey: {}", e);
// Try registering with a different hotkey as fallback
let fallback_hotkey = GlobalHotKey::new(
Some(Modifiers::CONTROL | Modifiers::ALT),
Code::F12,
);
match hotkey_manager.register(fallback_hotkey) {
Ok(_) => println!("Registered fallback hotkey Ctrl+Alt+F12 instead"),
Err(e) => eprintln!("Failed to register fallback hotkey: {}", e),
}
}
}
println!("Press Ctrl+Shift+F12 to exit the application");
// We're using the event-driven approach with the subscription function for mouse tracking
// We no longer need the thread to forward messages from rx to tx_clone_ui
// since we're using the iced subscription system directly
// Spawn a thread to listen for hotkeys
let state_clone = shared_state.clone();
thread::spawn(move || {
println!("Hotkey listener thread started");
let event_receiver = global_hotkey::GlobalHotKeyEvent::receiver();
loop {
match event_receiver.try_recv() {
Ok(event) => {
println!("Received hotkey event: {:?}", event);
if event.state == HotKeyState::Pressed {
println!("Exit hotkey pressed, sending Quit message...");
// Send Quit message instead of direct exit
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(Message::Quit) {
eprintln!("Failed to send Quit message from hotkey thread: {}", e);
// Fallback to direct exit if channel fails
platform::restore_cursor();
std::process::exit(0);
}
} else {
eprintln!("Hotkey: Global sender not initialized yet");
// Fallback to direct exit if sender not available
platform::restore_cursor();
std::process::exit(0);
}
// Don't exit directly here, let the main thread handle it.
}
},
Err(_) => {
// No events or channel disconnected, sleep briefly
std::thread::sleep(Duration::from_millis(50));
// Check if we should exit (will be checked in the loop condition below)
}
}
// Check shared state running flag (optional, main thread handles exit)
if let Ok(state) = state_clone.lock() {
if !state.running {
println!("Hotkey listener detected running=false, exiting thread.");
break;
}
}
}
});
// Using the event-driven approach with the subscription function for cursor tracking
(state, Task::none())
}
fn main() -> iced::Result {
// Initialize Steam
let _steam = steam::Steam::new();
// Note: We can't use a consistent window ID across boot() and main()
// because iced creates its own window ID internally
// Instead, we'll capture the real window ID in the CloseRequested event
iced::application("Cursor Rush", update, view)
.theme(|_| Theme::Dark)
.subscription(subscription)
.centered()
// This is the key - we need to tell iced not to exit when the window close button is clicked
.exit_on_close_request(false)
// run_with expects a function that returns (State, Task), not the tuple directly
.run_with(boot)
}
// Set up the system tray icon
// Simplified signature without passing the channel
fn setup_tray_icon(
cursors: &[CursorInfo],
shared_state: Arc<Mutex<SharedState>>,
) -> Option<TrayIcon> { // Return Option<TrayIcon>
if cursors.is_empty() {
return None; // Return None if no cursors
}
// Create a menu
let menu = Menu::with_items(&[
&tray_icon::menu::MenuItem::with_id(NEXT_CURSOR_ID, "Next cursor", true, None),
&tray_icon::menu::MenuItem::with_id(SHOW_WINDOW_ID, "Show window", true, None),
&tray_icon::menu::MenuItem::with_id(QUIT_ID, "Quit", true, None),
]).ok()?; // Use ok()? to convert Result to Option
// Use the first PNG as the tray icon
let icon_path = &cursors[0].path;
let img = image_open(icon_path).ok()?; // Use ok()?
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
let icon = tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height).ok()?; // Use ok()?
// Build the tray icon
println!("Building tray icon...");
let tray_result = TrayIconBuilder::new()
.with_tooltip("Cursor Rush")
.with_icon(icon)
.with_menu(Box::new(menu))
.with_menu_on_left_click(false) // Disable menu on left click - only show on right click
.build();
println!("Tray icon configuration: menu_on_left_click = false");
// Store the tray icon itself
let tray = match tray_result {
Ok(tray_instance) => {
println!("Tray icon built successfully");
tray_instance // Assign the TrayIcon object
},
Err(e) => {
eprintln!("Failed to build tray icon: {}", e);
return None; // Return None on error
}
};
// Listen for tray menu events in a separate thread
let state_clone = shared_state.clone();
println!("Spawning tray menu event listener thread...");
thread::spawn(move || {
let menu_channel = MenuEvent::receiver();
println!("Tray menu event listener waiting for events...");
while let Ok(event) = menu_channel.recv() {
println!("Tray menu event received: ID = {:?}", event.id);
let msg_to_send = match event.id.as_ref() {
NEXT_CURSOR_ID => {
println!("Tray: Next Cursor requested");
match state_clone.lock() {
Ok(mut state) => {
if !state.cursors.is_empty() {
state.current_index = (state.current_index + 1) % state.cursors.len();
if let Some(cursor) = state.cursors.get(state.current_index) {
println!("Tray: Updating cursor image: {}", cursor.name);
apply_cursor(&cursor.path);
// No need to call render_cursor anymore as the mouse hook handles rendering
state.current_cursor_image = image_open(&cursor.path).ok().map(|img| img.to_rgba8());
}
}
},
Err(e) => eprintln!("Tray: Failed to lock shared state for next cursor: {}", e),
}
// Send TrayEvent just to trigger UI update if needed
Some(Message::TrayEvent)
},
SHOW_WINDOW_ID => {
println!("Tray: Show Window requested");
// Send ShowWindow message to main thread
Some(Message::ShowWindow)
},
QUIT_ID => {
println!("Tray: Quit requested");
// Send Quit message to main thread
Some(Message::Quit)
},
_ => {
println!("Tray: Unknown menu event ID: {:?}", event.id);
None // Don't send a message for unknown IDs
}
};
// Send the determined message to the main UI thread using the global sender
if let Some(msg) = msg_to_send {
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(msg.clone()) { // Use clone if msg is needed after send
eprintln!("Tray: Failed to send {:?} message: {}", msg, e);
break; // Stop thread if channel broken
}
// If Quit message was sent, break the loop to exit the listener thread
if matches!(msg, Message::Quit) {
println!("Tray: Quit message sent, exiting listener thread");
break;
}
} else {
eprintln!("Tray: Global sender not initialized yet");
}
}
}
println!("Tray menu event listener thread finished");
});
// Set up a direct event handler for tray icon events
// This is the preferred way to handle tray icon clicks
println!("Setting up tray icon click event handler...");
// Use set_event_handler to directly handle tray icon events
TrayIconEvent::set_event_handler(Some(move |event| {
// Only process important events, filter out mouse movements
match event {
// Handle click events
TrayIconEvent::Click { button, .. } => {
println!("Tray: Click detected with button: {:?}", button);
// Left click should show the window
if button == MouseButton::Left {
println!("Tray: Left click detected, sending ShowWindow message");
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
eprintln!("Tray: Failed to send ShowWindow message via handler: {}", e);
}
} else {
eprintln!("Tray: Global sender not initialized yet for click handler");
}
}
},
// Handle double-click events
TrayIconEvent::DoubleClick { button, .. } => {
println!("Tray: Double click detected with button: {:?}", button);
// Double left click should also show the window
if button == MouseButton::Left {
println!("Tray: Double left click detected, sending ShowWindow message");
// Get the global sender
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
eprintln!("Tray: Failed to send ShowWindow message via handler on double click: {}", e);
}
} else {
eprintln!("Tray: Global sender not initialized yet for double click handler");
}
}
},
// Silently ignore mouse movement events
TrayIconEvent::Move { .. } => {
// Don't log anything for mouse movements
},
// Silently ignore mouse enter/leave events
TrayIconEvent::Enter { .. } | TrayIconEvent::Leave { .. } => {
// Don't log anything for mouse enter/leave
},
// Log other events for debugging (but not mouse-related ones)
_ => {
println!("Tray: Event received: {:?}", event);
}
}
}));
println!("Tray setup complete using set_event_handler for clicks."); // Updated log
Some(tray) // Return the TrayIcon object
}
// Helper function to hide a window to the tray
fn hide_window_to_tray(window_id: iced::window::Id) -> Task<Message> {
println!("Attempting to hide window with ID: {:?}", window_id);
// Use ONLY the iced method for hiding. Platform specific might interfere.
let hide_task: Task<()> = iced::window::change_mode(window_id, iced::window::Mode::Hidden);
hide_task.map(|_: ()| {
println!("Window hide task completed (via iced)");
// Return a specific message to confirm the window was hidden
Message::WindowHidden
})
}
// Update function to handle messages
fn update(state: &mut CursorRush, message: Message) -> Task<Message> {
match message {
Message::NextCursor => {
if !state.cursors.is_empty() {
state.current_index = (state.current_index + 1) % state.cursors.len();
// Apply the new cursor
if let Some(cursor) = state.cursors.get(state.current_index) {
apply_cursor(&cursor.path);
// No need to call render_cursor anymore as the mouse hook handles rendering
// Load the new cursor image
let current_cursor_image = match image::open(&cursor.path) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => None,
};
// Update shared state
if let Some(shared) = &state.shared_state {
if let Ok(mut shared_state) = shared.lock() {
shared_state.current_index = state.current_index;
shared_state.current_cursor_image = current_cursor_image;
}
}
}
}
Task::none()
},
Message::SelectCursor(idx) => {
if idx < state.cursors.len() {
state.current_index = idx;
// Apply the selected cursor
if let Some(cursor) = state.cursors.get(state.current_index) {
apply_cursor(&cursor.path);
// No need to call render_cursor anymore as the mouse hook handles rendering
// Load the new cursor image
let current_cursor_image = match image_open(&cursor.path) {
Ok(img) => Some(img.to_rgba8()),
Err(_) => None,
};
// Update shared state
if let Some(shared) = &state.shared_state {
if let Ok(mut shared_state) = shared.lock() {
shared_state.current_index = state.current_index;
shared_state.current_cursor_image = current_cursor_image;
}
}
}
}
Task::none()
},
Message::Quit => {
println!("Update: Handling Quit message");
// Set running flag to false to signal other threads (like hotkey listener)
if let Some(shared) = &state.shared_state {
if let Ok(mut shared_state) = shared.lock() {
shared_state.running = false;
}
}
// Restore the system cursor before exiting
platform::restore_cursor();
// Cleanly drop the tray icon *before* exiting iced
// This removes the icon from the system tray immediately
if let Some(tray) = state.tray_icon.take() {
drop(tray);
println!("Tray icon explicitly dropped.");
}
// Use iced's exit mechanism for a cleaner shutdown
println!("Requesting application exit via iced::exit...");
iced::exit()
},
Message::TrayEvent => {
// Sync with shared state
if let Some(shared) = &state.shared_state {
if let Ok(shared_state) = shared.lock() {
state.current_index = shared_state.current_index;
}
}
Task::none()
},
Message::CloseRequested(window_id) => {
println!("Handling CloseRequested: Hiding window with ID: {:?}", window_id);
// Store the real window ID for future use
// This is critical for showing the window later
state.main_window = Some(window_id);
println!("Stored window ID: {:?} for future use", window_id);
// Update shared state first
if let Some(shared) = &state.shared_state {
match shared.lock() {
Ok(mut shared_state) => {
if shared_state.window_visible { // Only hide if visible
shared_state.window_visible = false;
println!("Set window_visible to false in shared state");
println!("Hiding window with ID: {:?}, app continues running in tray", window_id);
return hide_window_to_tray(window_id);
} else {
println!("Window already hidden, ignoring CloseRequested.");
return Task::none(); // Don't try to hide again
}
}
Err(e) => {
eprintln!("Error locking shared state for hiding: {}", e);
return Task::none();
}
}
} else {
eprintln!("Error: Shared state not available for hiding.");
return Task::none();
}
},
Message::WindowHidden => {
println!("Window successfully hidden to tray");
Task::none()
},
Message::WindowShown => {
println!("Update: Received WindowShown message. Window should now be visible.");
Task::none()
},
Message::ShowWindow => {
println!("Handling ShowWindow: Attempting to show window"); // More specific starting log
if let Some(window_id) = state.main_window {
println!("Using stored window ID: {:?}", window_id);
// It's generally safer to attempt the show operation even if the state *thinks* it's visible.
// Let's update the shared state *after* we know the task is being created.
println!("Creating show/focus tasks for window ID: {:?}", window_id);
let show_task = iced::window::change_mode(window_id, iced::window::Mode::Windowed);
let focus_task = iced::window::gain_focus(window_id);
// Update shared state *before* returning the task, but after creating it.
if let Some(shared) = &state.shared_state {
match shared.lock() {
Ok(mut shared_state) => {
if !shared_state.window_visible { // Only log if changing state
println!("Updating shared state: window_visible = true");
shared_state.window_visible = true;
} else {
println!("Shared state already indicates window is visible.");
}
}
Err(e) => {
eprintln!("Error locking shared state during show: {}", e);
// Proceed with showing the window anyway, state might be out of sync
}
}
} else {
eprintln!("Shared state unavailable during show.");
// Proceed with showing the window anyway
}
println!("Returning Task::batch to show and focus window.");
// Return the batch task
return Task::batch(vec![show_task, focus_task])
.map(|_: ()| {
// This message confirms the Iced runtime *completed* the tasks.
println!("Iced runtime reports show/focus tasks completed successfully.");
Message::WindowShown // Send confirmation message
});
} else {
eprintln!("Error: Main window ID not found when trying to show. Cannot create tasks.");
Task::none() // Cannot proceed without window_id
}
},
}
}
// View function to render the UI
fn view(state: &CursorRush) -> Element<Message> {
let mut content = Column::new()
.spacing(16)
.push(text("Cursor Rush").size(24));
// Add a button for each cursor
if !state.cursors.is_empty() {
let current_cursor = &state.cursors[state.current_index];
content = content.push(text(format!("Current cursor: {}", current_cursor.name)));
// Display the current cursor image as a preview
let img_handle = iced_image::Handle::from_path(&current_cursor.path);
let cursor_image = iced_image::Image::new(img_handle)
.width(Length::Fixed(64.0))
.height(Length::Fixed(64.0));
content = content.push(cursor_image);
// Create a row of cursor buttons
let mut cursor_row = Row::new().spacing(10);
for (i, cursor) in state.cursors.iter().enumerate() {
// Create a small preview image for each cursor
let preview_handle = iced_image::Handle::from_path(&cursor.path);
let preview_image = iced_image::Image::new(preview_handle)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0));
let btn = button(
Row::new()
.spacing(5)
.push(preview_image)
.push(text(&cursor.name).size(16))
)
.on_press(Message::SelectCursor(i));
cursor_row = cursor_row.push(btn);
}
content = content
.push(cursor_row)
.push(button("Next cursor").on_press(Message::NextCursor))
.push(button("Quit").on_press(Message::Quit));
// Add instructions for the hotkey and close button behavior
content = content.push(
container(
Column::new()
.spacing(5)
.push(text("Press Ctrl+Shift+F12 to exit the application").size(16))
.push(text("Close button (X) minimizes to system tray").size(16))
)
.padding(10)
);
} else {
content = content.push(text("No cursors found in assets directory"));
}
// Cursor position display removed to avoid spammy updates
// Create the main container
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.padding(20)
.into()
}
-1006
View File
File diff suppressed because it is too large Load Diff
-24
View File
@@ -1,24 +0,0 @@
use steamworks::{
sys::{SteamAPI_ISteamInventory_TriggerItemDrop, SteamAPI_SteamInventory_v003},
AppId, Client, SingleClient,
};
#[allow(dead_code)]
pub struct Steam {
client: Client,
single: SingleClient,
}
impl Steam {
pub fn new() -> Self {
let (client, single) = Client::init_app(AppId(1430270)).unwrap();
Self { client, single }
}
pub fn drop_item(&self) {
unsafe {
let mut r = 0;
SteamAPI_ISteamInventory_TriggerItemDrop(SteamAPI_SteamInventory_v003(), &mut r, 1);
}
}
}