Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa039c7291 | ||
|
|
73634440cf | ||
|
|
eced2a561c | ||
|
|
61157a2402 | ||
|
|
67ee3dc8d1 | ||
|
|
7016e96673 | ||
|
|
09374eecb3 | ||
|
|
4f71789710 | ||
|
|
a9976cbbe5 | ||
|
|
bba65f2f97 | ||
|
|
c1b8160f4a | ||
|
|
16fc8949b3 | ||
|
|
b31aefe896 | ||
|
|
f60f058cf9 | ||
|
|
9f733ed39b | ||
|
|
0133b61944 | ||
|
|
20592954f7 | ||
|
|
06b2f0ffaf | ||
|
|
2e71cd8b9e | ||
|
|
a92ef1e07c | ||
|
|
b3d85106a8 | ||
|
|
34b0d700a6 | ||
|
|
0e6841bd8a | ||
|
|
9f6f3442dc | ||
|
|
a8d8d2ea0b | ||
|
|
49226cf5c5 | ||
|
|
f464052bda | ||
|
|
cf1c90856f |
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
CODE_CLEANUP_TODO.md
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
@@ -1,27 +1,4 @@
|
||||
[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
|
||||
[workspace]
|
||||
members = ["frontend/src-tauri"]
|
||||
resolver = "3"
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# 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.56.0 or later (tested with 1.86.0)
|
||||
- Cargo
|
||||
- Node.js 16.0.0 or later (tested with 20.11.0)
|
||||
- npm 7.0.0 or later (tested with 10.2.4)
|
||||
- Platform-specific dependencies:
|
||||
- Windows: Visual Studio Build Tools (fully supported)
|
||||
- macOS: Development in progress
|
||||
- Linux: Development in progress
|
||||
|
||||
### Dependencies
|
||||
|
||||
The application uses the following major dependencies:
|
||||
- Tauri 1.8.3 - Cross-platform framework
|
||||
- React 19.1.0 - Frontend framework
|
||||
- image 0.24 - Image processing
|
||||
- winapi 0.3.9 - Windows API bindings
|
||||
- once_cell 1.19.0 - Lazy initialization
|
||||
- steamworks 0.11.0 - Steam integration
|
||||
- parking_lot 0.12.3 - High-performance synchronization
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Navigate to the frontend directory
|
||||
cd frontend
|
||||
|
||||
# Install npm dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode (starts both frontend and backend)
|
||||
npm run tauri dev
|
||||
|
||||
# Build for production
|
||||
npm run tauri build
|
||||
|
||||
# Alternative: Run frontend and backend separately for debugging
|
||||
# Terminal 1: npm run dev (starts Vite dev server)
|
||||
# Terminal 2: npm run tauri dev --no-dev-server (starts Tauri without dev server)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. 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 or CUR images with transparent backgrounds
|
||||
2. Place them in the `frontend/src/cursors` directory
|
||||
3. Restart the application to load the new cursors
|
||||
4. The cursor name in the UI will be derived from the filename
|
||||
|
||||
**Note**: The application automatically scans multiple directories for cursor files:
|
||||
- `frontend/src/cursors/` (recommended location)
|
||||
- `frontend/src-tauri/src/cursors/`
|
||||
- `frontend/src-tauri/cursors/`
|
||||
|
||||
## Hotkeys
|
||||
|
||||
- Ctrl+Shift+F12: Exit the application (restores original system cursor)
|
||||
- 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
|
||||
|
||||
## Known Issues & Status
|
||||
|
||||
### Recently Fixed Issues
|
||||
The following issues have been addressed in recent updates:
|
||||
- Task Manager compatibility: The application now properly handles elevated applications
|
||||
- System tray menu stability: Fixed issues with the tray menu disappearing when mousing over it
|
||||
|
||||
### Current Known Issues
|
||||
- Some system tray menu interactions may still be inconsistent on certain Windows versions
|
||||
- Z-ordering issues where custom cursor may hide behind system tray menus due to Windows UI prioritization
|
||||
- Performance may be affected when Task Manager or other elevated applications are open due to Windows security restrictions
|
||||
|
||||
## Steam Integration
|
||||
|
||||
The application includes integration with the Steam API (AppID: 1430270):
|
||||
- Shows as an active game in Steam for time tracking purposes
|
||||
- Steam integration initializes automatically when the application starts
|
||||
- Item drop functionality is implemented but not yet active
|
||||
- Steam must be running for the integration to work properly
|
||||
- The application will appear in your Steam library as "running" while active
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Issues
|
||||
- **Missing Visual C++ Redistributable**: Install Visual Studio Build Tools or Visual C++ Redistributable packages
|
||||
- **Node.js library issues**: Reinstall Node.js (version 16.0.0 or later) and ensure all dependencies are properly installed
|
||||
- **Rust compilation errors**: Ensure Rust 1.56.0 or later is installed and up to date
|
||||
|
||||
### Runtime Issues
|
||||
- **Application won't start**: Check that all dependencies are installed and Steam is running (if using Steam integration)
|
||||
- **Cursor not changing**: Verify cursor files exist in the correct directory and have proper file permissions
|
||||
- **System tray issues**: Try restarting the application or check Windows notification area settings
|
||||
|
||||
### Development Issues
|
||||
- **Tauri dev server not starting**: Ensure port 5173 is available or specify a different port
|
||||
- **Hot reload not working**: Check that the Vite dev server is running properly in the background
|
||||
|
||||
## Contributing
|
||||
|
||||
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.
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -0,0 +1,3 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "cursor-rush"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5.2", features = [] }
|
||||
zip = { version = "4.0.0", features = ["zstd"] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
image = "0.24"
|
||||
# Ensure "consoleapi" is in this features list
|
||||
winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "errhandlingapi", "consoleapi"] }
|
||||
once_cell = "1.19.0"
|
||||
steamworks = "0.11.0"
|
||||
parking_lot = "0.12.3"
|
||||
rand = "0.9.1"
|
||||
tokio = { version = "1.45.1", features = ["macros", "parking_lot", "rt-multi-thread", "time"] }
|
||||
zip = { version = "4.0.0", features = ["zstd"] }
|
||||
anyhow = "1.0.98"
|
||||
futures = "0.3.31"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
@@ -0,0 +1,61 @@
|
||||
use std::{env, fs::{self, read_dir}, io::Write, path::PathBuf};
|
||||
|
||||
use zip::{write::SimpleFileOptions, ZipWriter};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tauri_build::build();
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
|
||||
|
||||
let profile_target_dir = out_dir
|
||||
.ancestors()
|
||||
.nth(3)
|
||||
.ok_or("Failed to determine profile target directory from OUT_DIR")?;
|
||||
|
||||
println!(
|
||||
"cargo:info=Build script running for target OS: {}",
|
||||
env::var("CARGO_CFG_TARGET_OS").unwrap_or_default()
|
||||
);
|
||||
println!(
|
||||
"cargo:info=Final libraries will be copied to: {}",
|
||||
profile_target_dir.display()
|
||||
);
|
||||
|
||||
// --- Define libraries and their source locations based on OS ---
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS")?;
|
||||
|
||||
let source_subdir = if target_os == "windows" {
|
||||
"lib/win64"
|
||||
} else if target_os == "linux" {
|
||||
"lib/linux64"
|
||||
} else if target_os == "macos" {
|
||||
"lib/osx"
|
||||
} else {
|
||||
return Err("Unsupported target".into());
|
||||
};
|
||||
|
||||
for f in read_dir(source_subdir)? {
|
||||
let f = f?;
|
||||
println!("cargo:rerun-if-changed={}", f.path().display());
|
||||
fs::copy(f.path(), profile_target_dir.join(f.file_name()))?;
|
||||
}
|
||||
|
||||
// Build data file
|
||||
println!("cargo:rerun-if-changed=src/cursors");
|
||||
println!("cargo:rerun-if-changed=src/cursor_def.json");
|
||||
|
||||
let mut zip = ZipWriter::new(fs::OpenOptions::new().write(true).create(true).open(profile_target_dir.join("cursor_rush.dat"))?);
|
||||
zip.start_file("cursor_def.json", SimpleFileOptions::default())?;
|
||||
zip.write_all(&fs::read("src/cursor_def.json")?)?;
|
||||
|
||||
for f in read_dir("src/cursors")? {
|
||||
let f = f?;
|
||||
zip.start_file(f.file_name().to_string_lossy(), SimpleFileOptions::default())?;
|
||||
zip.write_all(&fs::read(f.path())?)?;
|
||||
}
|
||||
|
||||
zip.finish()?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,130 @@
|
||||
use anyhow::Result;
|
||||
use futures::FutureExt;
|
||||
use image::{ImageFormat, RgbaImage};
|
||||
use std::{env, fs, sync::Arc, time::Duration};
|
||||
use tokio::time::interval;
|
||||
use tokio::{select, time::Interval};
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
platform::{restore_default_cursor, set_system_cursor},
|
||||
Cursor, CURSOR_CHANGED, CURSOR_CMD,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct AnimationState {
|
||||
cursor_index: Option<usize>,
|
||||
frame_timer: Option<Interval>,
|
||||
frame_index: usize,
|
||||
loaded_image: Vec<RgbaImage>,
|
||||
loaded_particles: Vec<Vec<RgbaImage>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn animate_loop(cursors: Arc<Vec<Cursor>>) -> Result<()> {
|
||||
let mut cmds = CURSOR_CMD.subscribe();
|
||||
let mut selected: Option<usize> = None;
|
||||
|
||||
let mut animation_state = AnimationState::default();
|
||||
|
||||
let mut animation_interval = interval(Duration::from_secs_f32(1.0 / 30.0));
|
||||
|
||||
loop {
|
||||
select! {
|
||||
cmd = cmds.recv() => {
|
||||
let cmd = cmd?;
|
||||
println!("GOT CURSOR CMD {:?}", cmd);
|
||||
match cmd {
|
||||
crate::CursorCmd::Select(i) => {
|
||||
if i < cursors.len() {
|
||||
selected = Some(i);
|
||||
}
|
||||
}
|
||||
crate::CursorCmd::Next => {
|
||||
selected = Some(selected.map(|x| (x + 1) % cursors.len()).unwrap_or(0))
|
||||
}
|
||||
crate::CursorCmd::Reapply => {
|
||||
|
||||
},
|
||||
crate::CursorCmd::Restore => {
|
||||
|
||||
},
|
||||
}
|
||||
CURSOR_CHANGED.send(selected.unwrap_or(0)).ok();
|
||||
}
|
||||
|
||||
_ = animation_interval.tick() => {}
|
||||
}
|
||||
|
||||
update_cursor(&mut animation_state, selected, &cursors).await?;
|
||||
}
|
||||
}
|
||||
|
||||
fn load(path: &str) -> Option<RgbaImage> {
|
||||
let mut zip = ZipArchive::new(
|
||||
fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.open(env::current_exe().ok()?.parent()?.join("cursor_rush.dat"))
|
||||
.ok()?,
|
||||
)
|
||||
.ok()?;
|
||||
let mut def = zip.by_name(path).ok()?;
|
||||
let mut bytes = Vec::new();
|
||||
std::io::copy(&mut def, &mut bytes).ok();
|
||||
Some(
|
||||
image::load_from_memory_with_format(&bytes, ImageFormat::from_path(path).ok()?)
|
||||
.ok()?
|
||||
.to_rgba8(),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_frames(frames: &Vec<String>) -> Vec<RgbaImage> {
|
||||
frames.iter().map(|x| load(x)).flatten().collect()
|
||||
}
|
||||
|
||||
async fn update_cursor(
|
||||
animation_state: &mut AnimationState,
|
||||
selected: Option<usize>,
|
||||
cursors: &Vec<Cursor>,
|
||||
) -> Result<()> {
|
||||
if animation_state.cursor_index != selected {
|
||||
*animation_state = AnimationState::default();
|
||||
animation_state.cursor_index = selected;
|
||||
|
||||
if let Some(selected) = selected {
|
||||
if cursors[selected].animation_speed > 0.0 {
|
||||
animation_state.frame_timer = Some(interval(Duration::from_secs_f32(
|
||||
1.0 / cursors[selected].animation_speed,
|
||||
)))
|
||||
}
|
||||
animation_state.loaded_image = load_frames(&cursors[selected].frames);
|
||||
animation_state.loaded_particles = cursors[selected]
|
||||
.particles
|
||||
.iter()
|
||||
.map(|x| load_frames(&x.frames))
|
||||
.collect();
|
||||
|
||||
if let Some(i) = animation_state.loaded_image.get(0) {
|
||||
set_system_cursor(i)?;
|
||||
}
|
||||
} else {
|
||||
restore_default_cursor()?;
|
||||
}
|
||||
} else {
|
||||
if let Some(frame_timer) = &mut animation_state.frame_timer {
|
||||
if frame_timer.tick().now_or_never().is_some() && animation_state.loaded_image.len() > 0
|
||||
{
|
||||
animation_state.frame_index += 1;
|
||||
animation_state.frame_index %= animation_state.loaded_image.len();
|
||||
if let Some(i) = animation_state
|
||||
.loaded_image
|
||||
.get(animation_state.frame_index)
|
||||
{
|
||||
set_system_cursor(i)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
[
|
||||
{
|
||||
"name": "white",
|
||||
"frames": [
|
||||
"icon.png"
|
||||
],
|
||||
"animation_speed": 0.0,
|
||||
"particles": []
|
||||
},
|
||||
{
|
||||
"name": "red",
|
||||
"frames": [
|
||||
"red.png"
|
||||
],
|
||||
"animation_speed": 0.0,
|
||||
"particles": []
|
||||
},
|
||||
{
|
||||
"name": "flashing",
|
||||
"frames": [
|
||||
"red.png",
|
||||
"icon.png"
|
||||
],
|
||||
"animation_speed": 30.0,
|
||||
"particles": []
|
||||
},
|
||||
{
|
||||
"name": "fire circle",
|
||||
"source": "https://opengameart.org/content/fire-circle-fx",
|
||||
"frames": [
|
||||
"fire_circle_0000.png",
|
||||
"fire_circle_0001.png",
|
||||
"fire_circle_0002.png",
|
||||
"fire_circle_0003.png",
|
||||
"fire_circle_0004.png",
|
||||
"fire_circle_0005.png",
|
||||
"fire_circle_0006.png",
|
||||
"fire_circle_0007.png",
|
||||
"fire_circle_0008.png",
|
||||
"fire_circle_0009.png",
|
||||
"fire_circle_0010.png",
|
||||
"fire_circle_0011.png",
|
||||
"fire_circle_0012.png",
|
||||
"fire_circle_0013.png",
|
||||
"fire_circle_0014.png",
|
||||
"fire_circle_0015.png",
|
||||
"fire_circle_0016.png",
|
||||
"fire_circle_0017.png",
|
||||
"fire_circle_0018.png",
|
||||
"fire_circle_0019.png",
|
||||
"fire_circle_0020.png",
|
||||
"fire_circle_0021.png",
|
||||
"fire_circle_0022.png",
|
||||
"fire_circle_0023.png",
|
||||
"fire_circle_0024.png",
|
||||
"fire_circle_0025.png",
|
||||
"fire_circle_0026.png",
|
||||
"fire_circle_0027.png",
|
||||
"fire_circle_0028.png",
|
||||
"fire_circle_0029.png",
|
||||
"fire_circle_0030.png",
|
||||
"fire_circle_0031.png",
|
||||
"fire_circle_0032.png",
|
||||
"fire_circle_0033.png",
|
||||
"fire_circle_0034.png",
|
||||
"fire_circle_0035.png",
|
||||
"fire_circle_0036.png",
|
||||
"fire_circle_0037.png",
|
||||
"fire_circle_0038.png",
|
||||
"fire_circle_0039.png",
|
||||
"fire_circle_0040.png",
|
||||
"fire_circle_0041.png",
|
||||
"fire_circle_0042.png",
|
||||
"fire_circle_0043.png",
|
||||
"fire_circle_0044.png",
|
||||
"fire_circle_0045.png",
|
||||
"fire_circle_0046.png",
|
||||
"fire_circle_0047.png",
|
||||
"fire_circle_0048.png",
|
||||
"fire_circle_0049.png",
|
||||
"fire_circle_0050.png",
|
||||
"fire_circle_0051.png",
|
||||
"fire_circle_0052.png",
|
||||
"fire_circle_0053.png",
|
||||
"fire_circle_0054.png",
|
||||
"fire_circle_0055.png",
|
||||
"fire_circle_0056.png",
|
||||
"fire_circle_0057.png",
|
||||
"fire_circle_0058.png",
|
||||
"fire_circle_0059.png",
|
||||
"fire_circle_0060.png"
|
||||
],
|
||||
"animation_speed": 30.0,
|
||||
"particles": []
|
||||
}
|
||||
]
|
||||
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.8 KiB |