bundle cursors into cursor_rush.dat, started animator
This commit is contained in:
@@ -6,12 +6,12 @@ authors = ["you"]
|
|||||||
license = ""
|
license = ""
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.56"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.5.2", features = [] }
|
tauri-build = { version = "1.5.2", features = [] }
|
||||||
|
zip = { version = "4.0.0", features = ["zstd"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
|
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "system-tray", "window-minimize", "window-hide", "window-show", "window-close"] }
|
||||||
@@ -23,6 +23,10 @@ winapi = { version = "0.3.9", features = ["winuser", "libloaderapi", "wingdi", "
|
|||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
steamworks = "0.11.0"
|
steamworks = "0.11.0"
|
||||||
parking_lot = "0.12.3"
|
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"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use std::{env, fs::{self, read_dir}, path::PathBuf};
|
use std::{env, fs::{self, read_dir}, io::Write, path::PathBuf};
|
||||||
|
|
||||||
|
use zip::{write::SimpleFileOptions, ZipWriter};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tauri_build::build();
|
tauri_build::build();
|
||||||
@@ -38,5 +40,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
fs::copy(f.path(), profile_target_dir.join(f.file_name()))?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use image::{ImageFormat, RgbaImage};
|
||||||
|
use std::{env, fs, future::pending, sync::Arc, time::Duration};
|
||||||
|
use tokio::time::interval;
|
||||||
|
use tokio::{
|
||||||
|
select,
|
||||||
|
time::{Instant, 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>,
|
||||||
|
loaded_image: Option<RgbaImage>,
|
||||||
|
loaded_particles: Option<Vec<RgbaImage>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pend_optional_interval(t: &mut Option<Interval>) -> Instant {
|
||||||
|
if let Some(t) = t {
|
||||||
|
t.tick().await
|
||||||
|
} else {
|
||||||
|
pending::<Instant>().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 start = Instant::now();
|
||||||
|
|
||||||
|
let mut animation_state = AnimationState::default();
|
||||||
|
|
||||||
|
let mut animation_interval: Option<Interval> = None;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if cursors[i].animation_speed > 0.0 {
|
||||||
|
animation_interval = Some(interval(Duration::from_secs_f32(1.0/cursors[i].animation_speed)));
|
||||||
|
}else{
|
||||||
|
animation_interval = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = pend_optional_interval(&mut animation_interval) => {
|
||||||
|
println!("interval {:?}", start.elapsed());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
animation_state.loaded_image = load(&cursors[selected].path);
|
||||||
|
animation_state.loaded_particles = cursors[selected]
|
||||||
|
.particles
|
||||||
|
.iter()
|
||||||
|
.map(|x| load(&x.path))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(i) = &animation_state.loaded_image {
|
||||||
|
set_system_cursor(i)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
restore_default_cursor()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "white",
|
||||||
|
"path": "icon.png",
|
||||||
|
"animation_speed": 0.0,
|
||||||
|
"particles": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "red",
|
||||||
|
"path": "red.png",
|
||||||
|
"animation_speed": 0.0,
|
||||||
|
"particles": []
|
||||||
|
}
|
||||||
|
]
|
||||||
+94
-193
@@ -1,29 +1,74 @@
|
|||||||
use integrations::steam::Steam;
|
use integrations::steam::Steam;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::env;
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::path::{Path, PathBuf};
|
use std::thread::spawn;
|
||||||
use std::sync::LazyLock;
|
use std::{env, fs};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
AppHandle, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
|
AppHandle, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
|
||||||
SystemTrayMenuItem, WindowEvent,
|
SystemTrayMenuItem, WindowEvent,
|
||||||
};
|
};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use zip::ZipArchive;
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
mod integrations;
|
mod integrations;
|
||||||
mod platform;
|
mod platform;
|
||||||
|
mod animator;
|
||||||
|
|
||||||
static STEAM: LazyLock<Mutex<Steam>> = LazyLock::new(|| Mutex::new(Steam::new()));
|
static STEAM: LazyLock<Mutex<Steam>> = LazyLock::new(|| Mutex::new(Steam::new()));
|
||||||
|
static CURSOR_CMD: LazyLock<broadcast::Sender<CursorCmd>> =
|
||||||
|
LazyLock::new(|| broadcast::channel(5).0);
|
||||||
|
static CURSOR_CHANGED: LazyLock<broadcast::Sender<usize>> =
|
||||||
|
LazyLock::new(|| broadcast::channel(5).0);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CursorCmd {
|
||||||
|
Select(usize),
|
||||||
|
Next,
|
||||||
|
Reapply,
|
||||||
|
Restore,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum RandomRange {
|
||||||
|
Constant(f32),
|
||||||
|
Linear(f32, f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RandomRange {
|
||||||
|
pub fn next(&self) -> f32 {
|
||||||
|
match self {
|
||||||
|
RandomRange::Constant(_) => todo!(),
|
||||||
|
RandomRange::Linear(_, _) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ParticleConfig {
|
||||||
|
path: String,
|
||||||
|
animation_speed: f32,
|
||||||
|
gravity_x: RandomRange,
|
||||||
|
gravity_y: RandomRange,
|
||||||
|
rate: f32,
|
||||||
|
fade_in: f32,
|
||||||
|
fade_out: f32,
|
||||||
|
duration: RandomRange,
|
||||||
|
}
|
||||||
|
|
||||||
// Define cursor structure
|
// Define cursor structure
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
name: String,
|
name: String,
|
||||||
path: String,
|
path: String,
|
||||||
|
animation_speed: f32,
|
||||||
|
particles: Vec<ParticleConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// App state to track cursors
|
// App state to track cursors
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
cursors: Vec<Cursor>,
|
cursors: Arc<Vec<Cursor>>,
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +79,7 @@ pub struct AppStateWrapper(Mutex<AppState>);
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
|
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
|
||||||
let app_state = state.0.lock();
|
let app_state = state.0.lock();
|
||||||
app_state.cursors.clone()
|
(*app_state.cursors).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to get current cursor index
|
// Command to get current cursor index
|
||||||
@@ -44,33 +89,6 @@ fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
|
|||||||
app_state.current_index
|
app_state.current_index
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to apply a cursor by index with proper error handling and event emission
|
|
||||||
fn apply_cursor_by_index(
|
|
||||||
index: usize,
|
|
||||||
app_state: &AppState,
|
|
||||||
app_handle: &AppHandle,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if index >= app_state.cursors.len() {
|
|
||||||
return Err("Index out of bounds".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let cursor = &app_state.cursors[index];
|
|
||||||
|
|
||||||
// Verify the file exists
|
|
||||||
if !Path::new(&cursor.path).exists() {
|
|
||||||
return Err(format!("Cursor file does not exist: {}", cursor.path));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the cursor
|
|
||||||
if let Err(e) = platform::set_system_cursor(&cursor.path) {
|
|
||||||
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
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to perform application cleanup (Steam shutdown and cursor restoration)
|
// Helper function to perform application cleanup (Steam shutdown and cursor restoration)
|
||||||
fn perform_app_cleanup() {
|
fn perform_app_cleanup() {
|
||||||
// Clean up Steam integration
|
// Clean up Steam integration
|
||||||
@@ -88,52 +106,25 @@ fn perform_app_cleanup() {
|
|||||||
|
|
||||||
// Command to select a cursor
|
// Command to select a cursor
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn select_cursor(
|
fn select_cursor(index: usize, app_handle: AppHandle) -> Result<(), String> {
|
||||||
index: usize,
|
println!("=== RESOURCE DEBUG [select_cursor] ===");
|
||||||
state: tauri::State<AppStateWrapper>,
|
let mut r = CURSOR_CHANGED.subscribe();
|
||||||
app_handle: AppHandle,
|
CURSOR_CMD.send(CursorCmd::Select(index)).ok();
|
||||||
) -> Result<(), String> {
|
let new_index = r.blocking_recv().unwrap();
|
||||||
let mut app_state = state.0.lock();
|
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
|
||||||
if index < app_state.cursors.len() {
|
|
||||||
app_state.current_index = index;
|
|
||||||
|
|
||||||
let cursor = &app_state.cursors[index];
|
Ok(())
|
||||||
|
|
||||||
let _filename = Path::new(&cursor.path)
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
|
|
||||||
|
|
||||||
// Use the helper function to apply the cursor
|
|
||||||
apply_cursor_by_index(index, &app_state, &app_handle)
|
|
||||||
} else {
|
|
||||||
Err("Index out of bounds".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to go to the next cursor
|
// Command to go to the next cursor
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn next_cursor(
|
fn next_cursor(app_handle: AppHandle) -> Result<usize, String> {
|
||||||
state: tauri::State<AppStateWrapper>,
|
println!("=== RESOURCE DEBUG [next_cursor] ===");
|
||||||
app_handle: AppHandle,
|
let mut r = CURSOR_CHANGED.subscribe();
|
||||||
) -> Result<usize, String> {
|
CURSOR_CMD.send(CursorCmd::Next).ok();
|
||||||
let mut app_state = state.0.lock();
|
let new_index = r.blocking_recv().unwrap();
|
||||||
if !app_state.cursors.is_empty() {
|
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
|
||||||
app_state.current_index = (app_state.current_index + 1) % app_state.cursors.len();
|
Ok(new_index)
|
||||||
|
|
||||||
let cursor = &app_state.cursors[app_state.current_index];
|
|
||||||
|
|
||||||
let _filename = Path::new(&cursor.path)
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.ok_or_else(|| format!("Invalid cursor path: {}", cursor.path))?;
|
|
||||||
|
|
||||||
// Use the helper function to apply the cursor
|
|
||||||
apply_cursor_by_index(app_state.current_index, &app_state, &app_handle)?;
|
|
||||||
Ok(app_state.current_index)
|
|
||||||
} else {
|
|
||||||
Err("No cursors available".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to quit the application
|
// Command to quit the application
|
||||||
@@ -175,120 +166,34 @@ fn temporarily_restore_cursor() -> Result<(), String> {
|
|||||||
|
|
||||||
// Command to re-apply custom cursor (after drag operations)
|
// Command to re-apply custom cursor (after drag operations)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn reapply_custom_cursor(state: tauri::State<AppStateWrapper>) -> Result<(), String> {
|
fn reapply_custom_cursor() -> Result<(), String> {
|
||||||
let app_state = state.0.lock();
|
CURSOR_CMD.send(CursorCmd::Reapply).ok();
|
||||||
if !app_state.cursors.is_empty() && app_state.current_index < app_state.cursors.len() {
|
Ok(())
|
||||||
let current_cursor = &app_state.cursors[app_state.current_index];
|
|
||||||
match platform::set_system_cursor(¤t_cursor.path) {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(format!("Failed to re-apply custom cursor: {}", e)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err("No cursor to re-apply".to_string())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to manually restore default cursors
|
// Command to manually restore default cursors
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn restore_default_cursors() -> Result<(), String> {
|
fn restore_default_cursors() -> Result<(), String> {
|
||||||
match platform::restore_system_cursor() {
|
println!("Manual restore default cursors command received");
|
||||||
Ok(_) => Ok(()),
|
CURSOR_CMD.send(CursorCmd::Restore).ok();
|
||||||
Err(e) => {
|
Ok(())
|
||||||
eprintln!("Failed to restore default cursors: {}", e);
|
|
||||||
Err(format!("Failed to restore default cursors: {}", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get possible cursor directory paths
|
|
||||||
fn get_cursor_directory_paths() -> Vec<PathBuf> {
|
|
||||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
|
|
||||||
vec![
|
|
||||||
manifest_dir.join("../src/cursors"),
|
|
||||||
manifest_dir.join("src/cursors"),
|
|
||||||
manifest_dir.join("cursors"),
|
|
||||||
manifest_dir.clone(), // Check manifest dir itself
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a single cursor file entry
|
|
||||||
fn process_cursor_file(entry: std::fs::DirEntry) -> Option<Cursor> {
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
Some(Cursor {
|
|
||||||
name,
|
|
||||||
path: path.to_string_lossy().to_string(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan cursor directories for cursor files
|
|
||||||
fn scan_cursor_directories() -> Vec<Cursor> {
|
|
||||||
let possible_cursor_dirs = get_cursor_directory_paths();
|
|
||||||
|
|
||||||
for cursors_dir in possible_cursor_dirs.iter() {
|
|
||||||
if cursors_dir.exists() {
|
|
||||||
if let Ok(entries) = std::fs::read_dir(cursors_dir) {
|
|
||||||
let cursors: Vec<Cursor> = entries
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter_map(process_cursor_file)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !cursors.is_empty() {
|
|
||||||
return cursors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create fallback cursors when no files are found
|
|
||||||
fn create_fallback_cursors() -> Vec<Cursor> {
|
|
||||||
eprintln!("No cursor files found, using fallback cursors");
|
|
||||||
|
|
||||||
vec![
|
|
||||||
Cursor {
|
|
||||||
name: "Default".to_string(),
|
|
||||||
path: "src/cursors/default.png".to_string(),
|
|
||||||
},
|
|
||||||
Cursor {
|
|
||||||
name: "Icon".to_string(),
|
|
||||||
path: "src/cursors/icon.png".to_string(),
|
|
||||||
},
|
|
||||||
Cursor {
|
|
||||||
name: "Red".to_string(),
|
|
||||||
path: "src/cursors/red.png".to_string(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load cursor files from src/cursors directory
|
// Function to load cursor files from src/cursors directory
|
||||||
fn load_cursor_files() -> Vec<Cursor> {
|
fn load_cursor_files() -> Result<Vec<Cursor>> {
|
||||||
let mut cursors = scan_cursor_directories();
|
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
|
||||||
|
let mut zip = ZipArchive::new(
|
||||||
if cursors.is_empty() {
|
fs::OpenOptions::new().read(true).open(
|
||||||
cursors = create_fallback_cursors();
|
env::current_exe()?
|
||||||
}
|
.parent()
|
||||||
|
.ok_or(anyhow!("failed to get parent"))?
|
||||||
// Sort cursors by name for consistent ordering
|
.join("cursor_rush.dat"),
|
||||||
cursors.sort_by(|a, b| a.name.cmp(&b.name));
|
)?,
|
||||||
cursors
|
)?;
|
||||||
|
let mut def = zip.by_name("cursor_def.json")?;
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
std::io::copy(&mut def, &mut bytes)?;
|
||||||
|
Ok(serde_json::from_slice(&bytes)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -302,11 +207,17 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load cursors from assets directory
|
// Load cursors from assets directory
|
||||||
let cursors = load_cursor_files();
|
let cursors = Arc::new(load_cursor_files().expect("failed to load cursors"));
|
||||||
|
|
||||||
|
{
|
||||||
|
let cursors = cursors.clone();
|
||||||
|
spawn(move ||animator::animate_loop(cursors));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create app state
|
// Create app state
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
cursors,
|
cursors: cursors,
|
||||||
current_index: 0,
|
current_index: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -358,18 +269,8 @@ pub fn run() {
|
|||||||
SystemTrayEvent::MenuItemClick { id, .. } => {
|
SystemTrayEvent::MenuItemClick { id, .. } => {
|
||||||
match id.as_str() {
|
match id.as_str() {
|
||||||
"next_cursor" => {
|
"next_cursor" => {
|
||||||
let state = app.state::<AppStateWrapper>();
|
println!("Next cursor requested from tray");
|
||||||
let mut app_state = state.0.lock();
|
CURSOR_CMD.send(CursorCmd::Next).ok();
|
||||||
|
|
||||||
if !app_state.cursors.is_empty() {
|
|
||||||
app_state.current_index =
|
|
||||||
(app_state.current_index + 1) % app_state.cursors.len();
|
|
||||||
|
|
||||||
// Use the helper function to apply the cursor
|
|
||||||
if let Err(e) = apply_cursor_by_index(app_state.current_index, &app_state, app) {
|
|
||||||
eprintln!("Error applying cursor from tray: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"show_window" => {
|
"show_window" => {
|
||||||
if let Some(window) = app.get_window("main") {
|
if let Some(window) = app.get_window("main") {
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ mod macos;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod linux;
|
mod linux;
|
||||||
|
|
||||||
// Error type for platform operations
|
use anyhow::Result;
|
||||||
pub type Result<T> = std::result::Result<T, String>;
|
|
||||||
|
use image::RgbaImage;
|
||||||
|
|
||||||
// Platform-agnostic interface for cursor handling
|
// Platform-agnostic interface for cursor handling
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -41,9 +42,9 @@ pub fn initialize() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cross-platform implementation for setting cursor
|
// Cross-platform implementation for setting cursor
|
||||||
pub fn set_system_cursor(path: &str) -> Result<()> {
|
pub fn set_system_cursor(image: &RgbaImage) -> Result<()> {
|
||||||
println!("Setting system cursor: {}", path);
|
println!("Setting system cursor");
|
||||||
set_cursor(path)
|
set_cursor(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cross-platform implementation for restoring default cursor
|
// Cross-platform implementation for restoring default cursor
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
use image::RgbaImage;
|
|
||||||
|
|
||||||
// Windows-specific cursor handle wrapper
|
|
||||||
//
|
|
||||||
// SAFETY: This wrapper around HCURSOR implements Send for the same reasons as SyncHCURSOR:
|
|
||||||
// Windows cursor handles are reference-counted by the kernel and safe to transfer between threads.
|
|
||||||
// The handle value is just a pointer-sized integer that represents a kernel resource.
|
|
||||||
//
|
|
||||||
// NOTE: This type serves a similar purpose to SyncHCURSOR in windows/types.rs but is used
|
|
||||||
// for cross-platform compatibility. Consider consolidating these types in the future.
|
|
||||||
#[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 {}
|
|
||||||
|
|
||||||
// NOTE: WinCursorWrapper doesn't implement Sync because it's only used for transferring
|
|
||||||
// cursor handles between threads, not for sharing them. If Sync is needed in the future,
|
|
||||||
// it would be safe to implement for the same reasons as Send.
|
|
||||||
|
|
||||||
// Common type for cursor information
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct CursorInfo {
|
|
||||||
pub name: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub image: Option<RgbaImage>,
|
|
||||||
pub hotspot_x: i32,
|
|
||||||
pub hotspot_y: i32,
|
|
||||||
|
|
||||||
// Platform-specific fields
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub win_cursor: Option<WinCursorWrapper>,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Windows platform cursor management
|
// Windows platform cursor management
|
||||||
|
|
||||||
|
use ::image::RgbaImage;
|
||||||
use std::ptr::null_mut;
|
use std::ptr::null_mut;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use winapi::{
|
use winapi::{
|
||||||
@@ -17,10 +18,9 @@ use winapi::{
|
|||||||
shared::minwindef::{TRUE, FALSE, DWORD},
|
shared::minwindef::{TRUE, FALSE, DWORD},
|
||||||
shared::windef::{HDC, HBITMAP, HGDIOBJ},
|
shared::windef::{HDC, HBITMAP, HGDIOBJ},
|
||||||
};
|
};
|
||||||
use ::image::{RgbaImage, open as image_open};
|
|
||||||
|
|
||||||
use super::types::*;
|
|
||||||
use super::state::*;
|
use super::state::*;
|
||||||
|
use super::types::*;
|
||||||
|
|
||||||
// Control handler function for console events (Ctrl+C, close, etc.)
|
// Control handler function for console events (Ctrl+C, close, etc.)
|
||||||
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
|
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
|
||||||
@@ -333,7 +333,8 @@ pub fn cleanup() {
|
|||||||
|
|
||||||
// Try to restore each system cursor to its default
|
// Try to restore each system cursor to its default
|
||||||
for &cursor_id in SYSTEM_CURSORS.iter() {
|
for &cursor_id in SYSTEM_CURSORS.iter() {
|
||||||
let default_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16));
|
let default_cursor =
|
||||||
|
LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16));
|
||||||
if !default_cursor.is_null() {
|
if !default_cursor.is_null() {
|
||||||
let cursor_copy = CopyIcon(default_cursor);
|
let cursor_copy = CopyIcon(default_cursor);
|
||||||
if !cursor_copy.is_null() {
|
if !cursor_copy.is_null() {
|
||||||
@@ -413,7 +414,10 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
|
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
|
||||||
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
|
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to register control handler: {}", std::io::Error::last_os_error());
|
eprintln!(
|
||||||
|
"Failed to register control handler: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -428,7 +432,8 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
unsafe {
|
unsafe {
|
||||||
for &cursor_id in SYSTEM_CURSORS.iter() {
|
for &cursor_id in SYSTEM_CURSORS.iter() {
|
||||||
// Get the original cursor
|
// Get the original cursor
|
||||||
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
|
let original_cursor =
|
||||||
|
CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
|
||||||
if !original_cursor.is_null() {
|
if !original_cursor.is_null() {
|
||||||
// Save the original cursor for restoration
|
// Save the original cursor for restoration
|
||||||
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
|
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
|
||||||
@@ -456,27 +461,8 @@ pub fn hide_system_cursor() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the cursor image using CursorInfo (with caching)
|
// Update the cursor image using CursorInfo (with caching)
|
||||||
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool {
|
pub fn update_cursor_info(image: &RgbaImage) -> bool {
|
||||||
// Use cached image if available, otherwise load from disk
|
println!("Updating cursor");
|
||||||
let image: image::RgbaImage = if let Some(ref cached_image) = cursor_info.image {
|
|
||||||
let img_clone: image::RgbaImage = cached_image.clone();
|
|
||||||
img_clone
|
|
||||||
} else {
|
|
||||||
match image_open(&cursor_info.path) {
|
|
||||||
Ok(img) => {
|
|
||||||
let rgba = img.to_rgba8();
|
|
||||||
// Cache the image for future use
|
|
||||||
// Make sure to specify the type to avoid type inference issues
|
|
||||||
let image_copy: image::RgbaImage = rgba.clone();
|
|
||||||
cursor_info.image = Some(image_copy);
|
|
||||||
rgba
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to load cursor image: {}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the image in the global state for reference
|
// Store the image in the global state for reference
|
||||||
if let Some(mut guard) = safe_lock(&CURRENT_CURSOR_IMAGE, "storing cursor image") {
|
if let Some(mut guard) = safe_lock(&CURRENT_CURSOR_IMAGE, "storing cursor image") {
|
||||||
@@ -488,12 +474,7 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
|
|||||||
unsafe {
|
unsafe {
|
||||||
// Create a new cursor first before destroying the old one
|
// Create a new cursor first before destroying the old one
|
||||||
// This minimizes the time when no custom cursor is active
|
// This minimizes the time when no custom cursor is active
|
||||||
let new_cursor = if let Some(ref cached_image) = cursor_info.image {
|
let new_cursor = create_cursor_from_image(&image);
|
||||||
// Always create a fresh cursor from the image to avoid invalid handle issues
|
|
||||||
create_cursor_from_image(cached_image)
|
|
||||||
} else {
|
|
||||||
create_cursor_from_image(&image)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(new_cursor_wrapper) = new_cursor {
|
if let Some(new_cursor_wrapper) = new_cursor {
|
||||||
// Apply the new cursor to all system cursors first
|
// Apply the new cursor to all system cursors first
|
||||||
@@ -515,15 +496,6 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
if !success {
|
||||||
eprintln!("Failed to apply cursor to all system cursors");
|
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
|
// Even if we failed to set all system cursors, try to at least set the current cursor
|
||||||
@@ -533,30 +505,11 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
|
|||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to create cursor from image");
|
eprintln!("Failed to create cursor from image");
|
||||||
|
|
||||||
// If we failed to create a new cursor, try to use the cached one as a fallback
|
|
||||||
#[cfg(windows)]
|
|
||||||
if let Some(cached_cursor) = &cursor_info.win_cursor {
|
|
||||||
if !cached_cursor.handle.is_null() {
|
|
||||||
if let Some(mut cursor_guard) = safe_lock(&CUSTOM_CURSOR, "using cached cursor fallback") {
|
|
||||||
// Convert WinCursorWrapper to SyncHCURSOR
|
|
||||||
let sync_cursor = super::types::SyncHCURSOR(cached_cursor.handle);
|
|
||||||
*cursor_guard = Some(sync_cursor);
|
|
||||||
SetCursor(cached_cursor.handle);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
eprintln!("Failed to use cached cursor fallback - mutex lock failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Apply a cursor to all system cursors
|
// Apply a cursor to all system cursors
|
||||||
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
|
unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCURSOR) -> bool {
|
||||||
let mut success = true;
|
let mut success = true;
|
||||||
@@ -584,7 +537,10 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
|
|||||||
let cursor_copy = CopyIcon(cursor);
|
let cursor_copy = CopyIcon(cursor);
|
||||||
if cursor_copy.is_null() {
|
if cursor_copy.is_null() {
|
||||||
let error = std::io::Error::last_os_error();
|
let error = std::io::Error::last_os_error();
|
||||||
eprintln!("Failed to copy cursor for priority ID: {} - Error: {}", cursor_id, error);
|
eprintln!(
|
||||||
|
"Failed to copy cursor for priority ID: {} - Error: {}",
|
||||||
|
cursor_id, error
|
||||||
|
);
|
||||||
success = false;
|
success = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -592,7 +548,10 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
|
|||||||
// Set the system cursor
|
// Set the system cursor
|
||||||
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
|
if SetSystemCursor(cursor_copy, cursor_id) == 0 {
|
||||||
let error = std::io::Error::last_os_error();
|
let error = std::io::Error::last_os_error();
|
||||||
eprintln!("Failed to set cursor for priority ID: {} - Error: {}", cursor_id, 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
|
DestroyIcon(cursor_copy); // Clean up the copy if we failed to set it
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,17 @@ pub mod types;
|
|||||||
mod cursor;
|
mod cursor;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use image::RgbaImage;
|
||||||
use crate::platform::Result;
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
// Functions expected by the platform-agnostic interface
|
// Functions expected by the platform-agnostic interface
|
||||||
pub fn set_cursor(path: &str) -> Result<()> {
|
pub fn set_cursor(image: &RgbaImage) -> Result<()> {
|
||||||
|
println!("Windows platform: setting cursor");
|
||||||
// Create a cursor info struct to pass to update_cursor_info
|
|
||||||
let mut cursor_info = crate::platform::types::CursorInfo {
|
|
||||||
name: path.split('\\').last().unwrap_or("unknown").to_string(),
|
|
||||||
path: PathBuf::from(path),
|
|
||||||
image: None,
|
|
||||||
hotspot_x: 0, // Default hotspot at top-left
|
|
||||||
hotspot_y: 0,
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
win_cursor: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the existing update_cursor_info function to handle loading and applying the cursor
|
// Use the existing update_cursor_info function to handle loading and applying the cursor
|
||||||
if !cursor::update_cursor_info(&mut cursor_info) {
|
if !cursor::update_cursor_info(image) {
|
||||||
return Err(format!("Failed to update cursor from {}", path));
|
return Err(anyhow!("Failed to update cursor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -44,7 +35,7 @@ pub fn restore_default_cursor_temporarily() -> Result<()> {
|
|||||||
pub fn init() -> Result<()> {
|
pub fn init() -> Result<()> {
|
||||||
println!("Windows platform: initializing cursor system");
|
println!("Windows platform: initializing cursor system");
|
||||||
if !cursor::hide_system_cursor() {
|
if !cursor::hide_system_cursor() {
|
||||||
return Err("Failed to initialize cursor system".to_string());
|
return Err(anyhow!("Failed to initialize cursor system"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user