bundle cursors into cursor_rush.dat, started animator
This commit is contained in:
@@ -6,12 +6,12 @@ authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5.2", features = [] }
|
||||
zip = { version = "4.0.0", features = ["zstd"] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.6.0", features = [ "window-start-dragging", "shell-open", "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"
|
||||
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"
|
||||
|
||||
[features]
|
||||
# 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>> {
|
||||
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()))?;
|
||||
}
|
||||
|
||||
// Build data file
|
||||
println!("cargo:rerun-if-changed=src/cursors");
|
||||
println!("cargo:rerun-if-changed=src/cursor_def.json");
|
||||
|
||||
let mut zip = ZipWriter::new(fs::OpenOptions::new().write(true).create(true).open(profile_target_dir.join("cursor_rush.dat"))?);
|
||||
zip.start_file("cursor_def.json", SimpleFileOptions::default())?;
|
||||
zip.write_all(&fs::read("src/cursor_def.json")?)?;
|
||||
|
||||
for f in read_dir("src/cursors")? {
|
||||
let f = f?;
|
||||
zip.start_file(f.file_name().to_string_lossy(), SimpleFileOptions::default())?;
|
||||
zip.write_all(&fs::read(f.path())?)?;
|
||||
}
|
||||
|
||||
zip.finish()?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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 parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::thread::spawn;
|
||||
use std::{env, fs};
|
||||
use tauri::{
|
||||
AppHandle, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu,
|
||||
SystemTrayMenuItem, WindowEvent,
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
use zip::ZipArchive;
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
mod integrations;
|
||||
mod platform;
|
||||
mod animator;
|
||||
|
||||
static STEAM: LazyLock<Mutex<Steam>> = LazyLock::new(|| Mutex::new(Steam::new()));
|
||||
static CURSOR_CMD: LazyLock<broadcast::Sender<CursorCmd>> =
|
||||
LazyLock::new(|| broadcast::channel(5).0);
|
||||
static CURSOR_CHANGED: LazyLock<broadcast::Sender<usize>> =
|
||||
LazyLock::new(|| broadcast::channel(5).0);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CursorCmd {
|
||||
Select(usize),
|
||||
Next,
|
||||
Reapply,
|
||||
Restore,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RandomRange {
|
||||
Constant(f32),
|
||||
Linear(f32, f32),
|
||||
}
|
||||
|
||||
impl RandomRange {
|
||||
pub fn next(&self) -> f32 {
|
||||
match self {
|
||||
RandomRange::Constant(_) => todo!(),
|
||||
RandomRange::Linear(_, _) => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ParticleConfig {
|
||||
path: String,
|
||||
animation_speed: f32,
|
||||
gravity_x: RandomRange,
|
||||
gravity_y: RandomRange,
|
||||
rate: f32,
|
||||
fade_in: f32,
|
||||
fade_out: f32,
|
||||
duration: RandomRange,
|
||||
}
|
||||
|
||||
// Define cursor structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Cursor {
|
||||
name: String,
|
||||
path: String,
|
||||
animation_speed: f32,
|
||||
particles: Vec<ParticleConfig>,
|
||||
}
|
||||
|
||||
// App state to track cursors
|
||||
pub struct AppState {
|
||||
cursors: Vec<Cursor>,
|
||||
cursors: Arc<Vec<Cursor>>,
|
||||
current_index: usize,
|
||||
}
|
||||
|
||||
@@ -34,7 +79,7 @@ pub struct AppStateWrapper(Mutex<AppState>);
|
||||
#[tauri::command]
|
||||
fn get_cursors(state: tauri::State<AppStateWrapper>) -> Vec<Cursor> {
|
||||
let app_state = state.0.lock();
|
||||
app_state.cursors.clone()
|
||||
(*app_state.cursors).clone()
|
||||
}
|
||||
|
||||
// Command to get current cursor index
|
||||
@@ -44,33 +89,6 @@ fn get_current_cursor_index(state: tauri::State<AppStateWrapper>) -> usize {
|
||||
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)
|
||||
fn perform_app_cleanup() {
|
||||
// Clean up Steam integration
|
||||
@@ -88,52 +106,25 @@ fn perform_app_cleanup() {
|
||||
|
||||
// Command to select a cursor
|
||||
#[tauri::command]
|
||||
fn select_cursor(
|
||||
index: usize,
|
||||
state: tauri::State<AppStateWrapper>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
let mut app_state = state.0.lock();
|
||||
if index < app_state.cursors.len() {
|
||||
app_state.current_index = index;
|
||||
fn select_cursor(index: usize, app_handle: AppHandle) -> Result<(), String> {
|
||||
println!("=== RESOURCE DEBUG [select_cursor] ===");
|
||||
let mut r = CURSOR_CHANGED.subscribe();
|
||||
CURSOR_CMD.send(CursorCmd::Select(index)).ok();
|
||||
let new_index = r.blocking_recv().unwrap();
|
||||
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
|
||||
|
||||
let cursor = &app_state.cursors[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(index, &app_state, &app_handle)
|
||||
} else {
|
||||
Err("Index out of bounds".to_string())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Command to go to the next cursor
|
||||
#[tauri::command]
|
||||
fn next_cursor(
|
||||
state: tauri::State<AppStateWrapper>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<usize, String> {
|
||||
let mut app_state = state.0.lock();
|
||||
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];
|
||||
|
||||
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())
|
||||
}
|
||||
fn next_cursor(app_handle: AppHandle) -> Result<usize, String> {
|
||||
println!("=== RESOURCE DEBUG [next_cursor] ===");
|
||||
let mut r = CURSOR_CHANGED.subscribe();
|
||||
CURSOR_CMD.send(CursorCmd::Next).ok();
|
||||
let new_index = r.blocking_recv().unwrap();
|
||||
let _ = app_handle.emit_all("cursor_changed", new_index); // Ignore error
|
||||
Ok(new_index)
|
||||
}
|
||||
|
||||
// Command to quit the application
|
||||
@@ -175,120 +166,34 @@ fn temporarily_restore_cursor() -> Result<(), String> {
|
||||
|
||||
// Command to re-apply custom cursor (after drag operations)
|
||||
#[tauri::command]
|
||||
fn reapply_custom_cursor(state: tauri::State<AppStateWrapper>) -> Result<(), String> {
|
||||
let app_state = state.0.lock();
|
||||
if !app_state.cursors.is_empty() && app_state.current_index < app_state.cursors.len() {
|
||||
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())
|
||||
}
|
||||
fn reapply_custom_cursor() -> Result<(), String> {
|
||||
CURSOR_CMD.send(CursorCmd::Reapply).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Command to manually restore default cursors
|
||||
#[tauri::command]
|
||||
fn restore_default_cursors() -> Result<(), String> {
|
||||
match platform::restore_system_cursor() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
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(),
|
||||
},
|
||||
]
|
||||
println!("Manual restore default cursors command received");
|
||||
CURSOR_CMD.send(CursorCmd::Restore).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Function to load cursor files from src/cursors directory
|
||||
fn load_cursor_files() -> Vec<Cursor> {
|
||||
let mut cursors = scan_cursor_directories();
|
||||
|
||||
if cursors.is_empty() {
|
||||
cursors = create_fallback_cursors();
|
||||
}
|
||||
|
||||
// Sort cursors by name for consistent ordering
|
||||
cursors.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
cursors
|
||||
fn load_cursor_files() -> Result<Vec<Cursor>> {
|
||||
println!("=== RESOURCE DEBUG [load_cursor_files] ===");
|
||||
let mut zip = ZipArchive::new(
|
||||
fs::OpenOptions::new().read(true).open(
|
||||
env::current_exe()?
|
||||
.parent()
|
||||
.ok_or(anyhow!("failed to get parent"))?
|
||||
.join("cursor_rush.dat"),
|
||||
)?,
|
||||
)?;
|
||||
let mut def = zip.by_name("cursor_def.json")?;
|
||||
let mut bytes = Vec::new();
|
||||
std::io::copy(&mut def, &mut bytes)?;
|
||||
Ok(serde_json::from_slice(&bytes)?)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
@@ -302,11 +207,17 @@ pub fn run() {
|
||||
}
|
||||
|
||||
// Load cursors from assets directory
|
||||
let cursors = load_cursor_files();
|
||||
let cursors = Arc::new(load_cursor_files().expect("failed to load cursors"));
|
||||
|
||||
{
|
||||
let cursors = cursors.clone();
|
||||
spawn(move ||animator::animate_loop(cursors));
|
||||
}
|
||||
|
||||
|
||||
// Create app state
|
||||
let app_state = AppState {
|
||||
cursors,
|
||||
cursors: cursors,
|
||||
current_index: 0,
|
||||
};
|
||||
|
||||
@@ -358,18 +269,8 @@ pub fn run() {
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => {
|
||||
match id.as_str() {
|
||||
"next_cursor" => {
|
||||
let state = app.state::<AppStateWrapper>();
|
||||
let mut app_state = state.0.lock();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
println!("Next cursor requested from tray");
|
||||
CURSOR_CMD.send(CursorCmd::Next).ok();
|
||||
}
|
||||
"show_window" => {
|
||||
if let Some(window) = app.get_window("main") {
|
||||
|
||||
@@ -15,8 +15,9 @@ mod macos;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
// Error type for platform operations
|
||||
pub type Result<T> = std::result::Result<T, String>;
|
||||
use anyhow::Result;
|
||||
|
||||
use image::RgbaImage;
|
||||
|
||||
// Platform-agnostic interface for cursor handling
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -41,9 +42,9 @@ pub fn initialize() -> Result<()> {
|
||||
}
|
||||
|
||||
// Cross-platform implementation for setting cursor
|
||||
pub fn set_system_cursor(path: &str) -> Result<()> {
|
||||
println!("Setting system cursor: {}", path);
|
||||
set_cursor(path)
|
||||
pub fn set_system_cursor(image: &RgbaImage) -> Result<()> {
|
||||
println!("Setting system cursor");
|
||||
set_cursor(image)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
use ::image::RgbaImage;
|
||||
use std::ptr::null_mut;
|
||||
use std::sync::atomic::Ordering;
|
||||
use winapi::{
|
||||
@@ -17,10 +18,9 @@ use winapi::{
|
||||
shared::minwindef::{TRUE, FALSE, DWORD},
|
||||
shared::windef::{HDC, HBITMAP, HGDIOBJ},
|
||||
};
|
||||
use ::image::{RgbaImage, open as image_open};
|
||||
|
||||
use super::types::*;
|
||||
use super::state::*;
|
||||
use super::types::*;
|
||||
|
||||
// Control handler function for console events (Ctrl+C, close, etc.)
|
||||
extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 {
|
||||
@@ -333,7 +333,8 @@ pub fn cleanup() {
|
||||
|
||||
// Try to restore each system cursor to its default
|
||||
for &cursor_id in SYSTEM_CURSORS.iter() {
|
||||
let default_cursor = LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16));
|
||||
let default_cursor =
|
||||
LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16));
|
||||
if !default_cursor.is_null() {
|
||||
let cursor_copy = CopyIcon(default_cursor);
|
||||
if !cursor_copy.is_null() {
|
||||
@@ -413,7 +414,10 @@ pub fn hide_system_cursor() -> bool {
|
||||
if SetConsoleCtrlHandler(Some(ctrl_handler), TRUE) != 0 {
|
||||
CTRL_HANDLER_REGISTERED.store(true, Ordering::SeqCst);
|
||||
} 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 {
|
||||
for &cursor_id in SYSTEM_CURSORS.iter() {
|
||||
// Get the original cursor
|
||||
let original_cursor = CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
|
||||
let original_cursor =
|
||||
CopyIcon(LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor_id as u16)));
|
||||
if !original_cursor.is_null() {
|
||||
// Save the original cursor for restoration
|
||||
original_cursors.push((cursor_id, SyncHCURSOR(original_cursor)));
|
||||
@@ -456,27 +461,8 @@ pub fn hide_system_cursor() -> bool {
|
||||
}
|
||||
|
||||
// Update the cursor image using CursorInfo (with caching)
|
||||
pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo) -> bool {
|
||||
// Use cached image if available, otherwise load from disk
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
pub fn update_cursor_info(image: &RgbaImage) -> bool {
|
||||
println!("Updating cursor");
|
||||
|
||||
// Store the image in the global state for reference
|
||||
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 {
|
||||
// Create a new cursor first before destroying the old one
|
||||
// This minimizes the time when no custom cursor is active
|
||||
let new_cursor = if let Some(ref cached_image) = cursor_info.image {
|
||||
// Always create a fresh cursor from the image to avoid invalid handle issues
|
||||
create_cursor_from_image(cached_image)
|
||||
} else {
|
||||
create_cursor_from_image(&image)
|
||||
};
|
||||
let new_cursor = create_cursor_from_image(&image);
|
||||
|
||||
if let Some(new_cursor_wrapper) = new_cursor {
|
||||
// Apply the new cursor to all system cursors first
|
||||
@@ -515,15 +496,6 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
|
||||
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 {
|
||||
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
|
||||
@@ -533,30 +505,11 @@ pub fn update_cursor_info(cursor_info: &mut crate::platform::types::CursorInfo)
|
||||
return true;
|
||||
} else {
|
||||
eprintln!("Failed to create cursor from image");
|
||||
|
||||
// If we failed to create a new cursor, try to use the cached one as a fallback
|
||||
#[cfg(windows)]
|
||||
if let Some(cached_cursor) = &cursor_info.win_cursor {
|
||||
if !cached_cursor.handle.is_null() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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;
|
||||
@@ -584,7 +537,10 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
|
||||
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);
|
||||
eprintln!(
|
||||
"Failed to copy cursor for priority ID: {} - Error: {}",
|
||||
cursor_id, error
|
||||
);
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
@@ -592,7 +548,10 @@ unsafe fn apply_cursor_to_all_system_cursors(cursor: winapi::shared::windef::HCU
|
||||
// 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);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -4,26 +4,17 @@ pub mod types;
|
||||
mod cursor;
|
||||
mod state;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use crate::platform::Result;
|
||||
use image::RgbaImage;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
// Functions expected by the platform-agnostic interface
|
||||
pub fn set_cursor(path: &str) -> Result<()> {
|
||||
|
||||
// Create a cursor info struct to pass to update_cursor_info
|
||||
let mut cursor_info = crate::platform::types::CursorInfo {
|
||||
name: path.split('\\').last().unwrap_or("unknown").to_string(),
|
||||
path: PathBuf::from(path),
|
||||
image: None,
|
||||
hotspot_x: 0, // Default hotspot at top-left
|
||||
hotspot_y: 0,
|
||||
#[cfg(target_os = "windows")]
|
||||
win_cursor: None,
|
||||
};
|
||||
pub fn set_cursor(image: &RgbaImage) -> Result<()> {
|
||||
println!("Windows platform: setting cursor");
|
||||
|
||||
// Use the existing update_cursor_info function to handle loading and applying the cursor
|
||||
if !cursor::update_cursor_info(&mut cursor_info) {
|
||||
return Err(format!("Failed to update cursor from {}", path));
|
||||
if !cursor::update_cursor_info(image) {
|
||||
return Err(anyhow!("Failed to update cursor"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -44,7 +35,7 @@ pub fn restore_default_cursor_temporarily() -> Result<()> {
|
||||
pub fn init() -> Result<()> {
|
||||
println!("Windows platform: initializing cursor system");
|
||||
if !cursor::hide_system_cursor() {
|
||||
return Err("Failed to initialize cursor system".to_string());
|
||||
return Err(anyhow!("Failed to initialize cursor system"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user