Merged system tray functionality with latest Steam integration
This commit is contained in:
@@ -12,6 +12,7 @@ global-hotkey = "0.4.2" # Global hotkey re
|
|||||||
lazy_static = "1.4.0" # Thread-safe static initialization
|
lazy_static = "1.4.0" # Thread-safe static initialization
|
||||||
steamworks = { version = "0.11.0", features = ["raw-bindings"] }
|
steamworks = { version = "0.11.0", features = ["raw-bindings"] }
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
|
once_cell = "1.18.0" # Thread-safe lazy initialized global variables
|
||||||
|
|
||||||
# Platform-specific dependencies
|
# Platform-specific dependencies
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
|||||||
+469
-137
@@ -1,10 +1,11 @@
|
|||||||
use iced::{
|
use iced::{
|
||||||
widget::{button, text, Column, Row, container, image as iced_image},
|
widget::{button, text, Column, Row, container, image as iced_image},
|
||||||
Element, Length, Subscription, Task, Theme,
|
Element, Length, Subscription, Task, Theme,
|
||||||
|
futures::{channel::mpsc, StreamExt},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{mpsc, Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -12,6 +13,9 @@ use ::image::{GenericImageView, RgbaImage, open as image_open};
|
|||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use tray_icon::{
|
use tray_icon::{
|
||||||
TrayIconBuilder,
|
TrayIconBuilder,
|
||||||
|
TrayIcon, // Import the TrayIcon type itself
|
||||||
|
TrayIconEvent, // Import TrayIconEvent for handling left/right clicks
|
||||||
|
MouseButton, // Import MouseButton to distinguish between left and right clicks
|
||||||
menu::{Menu, MenuEvent},
|
menu::{Menu, MenuEvent},
|
||||||
};
|
};
|
||||||
use global_hotkey::{
|
use global_hotkey::{
|
||||||
@@ -37,6 +41,7 @@ struct CursorInfo {
|
|||||||
|
|
||||||
// Define menu item IDs
|
// Define menu item IDs
|
||||||
const NEXT_CURSOR_ID: &str = "next_cursor";
|
const NEXT_CURSOR_ID: &str = "next_cursor";
|
||||||
|
const SHOW_WINDOW_ID: &str = "show_window";
|
||||||
const QUIT_ID: &str = "quit";
|
const QUIT_ID: &str = "quit";
|
||||||
|
|
||||||
// Shared state between the UI and the tray icon
|
// Shared state between the UI and the tray icon
|
||||||
@@ -45,15 +50,31 @@ struct SharedState {
|
|||||||
cursors: Vec<CursorInfo>,
|
cursors: Vec<CursorInfo>,
|
||||||
current_cursor_image: Option<RgbaImage>,
|
current_cursor_image: Option<RgbaImage>,
|
||||||
running: bool,
|
running: bool,
|
||||||
|
window_visible: bool, // Track if the window is visible
|
||||||
}
|
}
|
||||||
|
|
||||||
// Our application state
|
// Our application state
|
||||||
#[derive(Default)]
|
|
||||||
struct CursorRush {
|
struct CursorRush {
|
||||||
cursors: Vec<CursorInfo>,
|
cursors: Vec<CursorInfo>,
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
shared_state: Option<Arc<Mutex<SharedState>>>,
|
shared_state: Option<Arc<Mutex<SharedState>>>,
|
||||||
cursor_position: (f32, f32), // Track the mouse position
|
main_window: Option<iced::window::Id>, // Store the main window ID
|
||||||
|
tray_icon: Option<TrayIcon>, // Store the TrayIcon object to keep it alive
|
||||||
|
tray_tx: Option<mpsc::UnboundedSender<Message>>, // Sender for tray events
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the default impl initializes tray_icon to None
|
||||||
|
impl Default for CursorRush {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
cursors: Vec::new(),
|
||||||
|
current_index: 0,
|
||||||
|
shared_state: None,
|
||||||
|
main_window: None,
|
||||||
|
tray_icon: None, // Initialize as None
|
||||||
|
tray_tx: None, // Initialize as None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages that can be sent in our application
|
// Messages that can be sent in our application
|
||||||
@@ -62,8 +83,11 @@ enum Message {
|
|||||||
NextCursor,
|
NextCursor,
|
||||||
SelectCursor(usize),
|
SelectCursor(usize),
|
||||||
Quit,
|
Quit,
|
||||||
TrayEvent,
|
TrayEvent, // Used as a no-op message
|
||||||
CursorMoved((f32, f32)), // New message for cursor movement
|
CloseRequested(iced::window::Id), // Message for window close requests with the real window ID
|
||||||
|
ShowWindow, // Message to show the window from tray
|
||||||
|
WindowHidden, // Message indicating window was successfully hidden
|
||||||
|
WindowShown, // Message indicating window was successfully shown
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load all PNG files from the assets directory
|
// Function to load all PNG files from the assets directory
|
||||||
@@ -139,17 +163,74 @@ fn apply_cursor(path: &Path) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global channel for tray events
|
||||||
|
use std::sync::Once;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
static INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||||
|
static TRAY_SENDER: Lazy<std::sync::Mutex<Option<mpsc::UnboundedSender<Message>>>> =
|
||||||
|
Lazy::new(|| std::sync::Mutex::new(None));
|
||||||
|
|
||||||
|
// Function to get or create the global tray sender
|
||||||
|
fn get_tray_sender() -> mpsc::UnboundedSender<Message> {
|
||||||
|
if !INITIALIZED.load(Ordering::SeqCst) {
|
||||||
|
INIT.call_once(|| {
|
||||||
|
let (tx, _) = mpsc::unbounded();
|
||||||
|
*TRAY_SENDER.lock().unwrap() = Some(tx);
|
||||||
|
INITIALIZED.store(true, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TRAY_SENDER.lock().unwrap().clone().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
// Top-level subscription function to listen for events
|
// Top-level subscription function to listen for events
|
||||||
fn subscription(_state: &CursorRush) -> Subscription<Message> {
|
fn subscription(_state: &CursorRush) -> Subscription<Message> {
|
||||||
iced::event::listen().map(|event| {
|
// Create a vector to hold all subscriptions
|
||||||
if let iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }) = event {
|
let mut subs = vec![
|
||||||
// Track cursor position for UI display purposes
|
// Listen for events but filter out mouse movement events to avoid spamming
|
||||||
Message::CursorMoved((position.x, position.y))
|
iced::event::listen().map(|event| {
|
||||||
} else {
|
match event {
|
||||||
// We need to return some message, so we'll use TrayEvent as a no-op
|
// Skip cursor movement events to avoid spamming
|
||||||
Message::TrayEvent
|
iced::Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => {
|
||||||
|
// Return TrayEvent which is essentially a no-op
|
||||||
|
Message::TrayEvent
|
||||||
|
},
|
||||||
|
// Handle other events
|
||||||
|
_ => Message::TrayEvent, // Use TrayEvent as a no-op message
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Specifically subscribe to window close requests
|
||||||
|
// This is the key to handling the X button properly
|
||||||
|
iced::window::close_requests().map(|window_id| {
|
||||||
|
println!("Window close requested via X button for window ID: {:?}", window_id);
|
||||||
|
Message::CloseRequested(window_id)
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add a subscription for tray events
|
||||||
|
// We'll use a simple channel that the tray icon can send messages to
|
||||||
|
let tray_subscription = Subscription::run(
|
||||||
|
// Stream generator function
|
||||||
|
|| {
|
||||||
|
// Create a channel that the tray icon can send messages to
|
||||||
|
let (tx, rx) = mpsc::unbounded::<Message>();
|
||||||
|
|
||||||
|
// Store the sender in the global state
|
||||||
|
*TRAY_SENDER.lock().unwrap() = Some(tx);
|
||||||
|
|
||||||
|
// Return the receiver as a stream
|
||||||
|
rx
|
||||||
}
|
}
|
||||||
})
|
);
|
||||||
|
|
||||||
|
subs.push(tray_subscription);
|
||||||
|
|
||||||
|
// Combine all subscriptions
|
||||||
|
Subscription::batch(subs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boot function to initialize the application
|
// Boot function to initialize the application
|
||||||
@@ -185,72 +266,119 @@ fn boot() -> (CursorRush, Task<Message>) {
|
|||||||
cursors: cursors.clone(),
|
cursors: cursors.clone(),
|
||||||
current_cursor_image,
|
current_cursor_image,
|
||||||
running: true,
|
running: true,
|
||||||
|
window_visible: true, // Window is initially visible
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create a channel to communicate with the UI
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
// Set up the tray icon
|
|
||||||
setup_tray_icon(&cursors, shared_state.clone(), tx.clone());
|
|
||||||
|
|
||||||
// Initialize the application state with loaded cursors
|
// Initialize the application state with loaded cursors
|
||||||
let mut state = CursorRush::default();
|
let mut state = CursorRush::default();
|
||||||
state.cursors = cursors;
|
state.cursors = cursors.clone(); // Clone here
|
||||||
state.shared_state = Some(shared_state.clone());
|
state.shared_state = Some(shared_state.clone());
|
||||||
|
|
||||||
|
// Get the global tray sender (will be initialized by the subscription)
|
||||||
|
// We'll set it up in the state after the subscription is running
|
||||||
|
|
||||||
|
// Set up the tray icon AND store the result in the state
|
||||||
|
// The tray sender will be available from the global state
|
||||||
|
state.tray_icon = setup_tray_icon(&state.cursors, shared_state.clone()); // Pass the cloned cursors
|
||||||
|
if state.tray_icon.is_none() {
|
||||||
|
eprintln!("Failed to initialize tray icon, application might not function correctly in background.");
|
||||||
|
// Optionally, decide how to handle this failure (e.g., exit or run without tray)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll get the real window ID from the CloseRequested event
|
||||||
|
// No need to create a window ID here
|
||||||
|
|
||||||
// Set up global hotkey manager
|
// Set up global hotkey manager
|
||||||
let hotkey_manager = GlobalHotKeyManager::new().expect("Failed to create hotkey manager");
|
let hotkey_manager = match GlobalHotKeyManager::new() {
|
||||||
|
Ok(manager) => manager,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create hotkey manager: {}", e);
|
||||||
|
// Continue without hotkey support
|
||||||
|
GlobalHotKeyManager::new().expect("Critical failure: could not create hotkey manager")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Register Ctrl+Shift+F12 as the exit hotkey (uncommon but easy to remember)
|
// Register Ctrl+Shift+F12 as the exit hotkey (uncommon but easy to remember)
|
||||||
use global_hotkey::hotkey::{Code, Modifiers};
|
use global_hotkey::hotkey::{Code, Modifiers};
|
||||||
|
|
||||||
|
// We don't need a unique ID for the hotkey in this simple case
|
||||||
|
|
||||||
let hotkey = GlobalHotKey::new(
|
let hotkey = GlobalHotKey::new(
|
||||||
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
Some(Modifiers::CONTROL | Modifiers::SHIFT),
|
||||||
Code::F12,
|
Code::F12,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Err(e) = hotkey_manager.register(hotkey) {
|
// Register the hotkey with better error handling
|
||||||
eprintln!("Failed to register hotkey: {}", e);
|
match hotkey_manager.register(hotkey) {
|
||||||
} else {
|
Ok(_) => {
|
||||||
println!("Press Ctrl+Shift+F12 to exit the application");
|
println!("Successfully registered Ctrl+Shift+F12 as exit hotkey");
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to register hotkey: {}", e);
|
||||||
|
// Try registering with a different hotkey as fallback
|
||||||
|
let fallback_hotkey = GlobalHotKey::new(
|
||||||
|
Some(Modifiers::CONTROL | Modifiers::ALT),
|
||||||
|
Code::F12,
|
||||||
|
);
|
||||||
|
|
||||||
|
match hotkey_manager.register(fallback_hotkey) {
|
||||||
|
Ok(_) => println!("Registered fallback hotkey Ctrl+Alt+F12 instead"),
|
||||||
|
Err(e) => eprintln!("Failed to register fallback hotkey: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("Press Ctrl+Shift+F12 to exit the application");
|
||||||
|
|
||||||
// We're using the event-driven approach with the subscription function for mouse tracking
|
// We're using the event-driven approach with the subscription function for mouse tracking
|
||||||
|
|
||||||
// Spawn a thread to listen for tray events
|
// We no longer need the thread to forward messages from rx to tx_clone_ui
|
||||||
let tx_clone = tx.clone();
|
// since we're using the iced subscription system directly
|
||||||
thread::spawn(move || {
|
|
||||||
loop {
|
|
||||||
if let Ok(_) = rx.recv() {
|
|
||||||
// Send a message to the UI
|
|
||||||
let _ = tx_clone.send(Message::TrayEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spawn a thread to listen for hotkeys
|
// Spawn a thread to listen for hotkeys
|
||||||
let state_clone = shared_state.clone();
|
let state_clone = shared_state.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
println!("Hotkey listener thread started");
|
||||||
let event_receiver = global_hotkey::GlobalHotKeyEvent::receiver();
|
let event_receiver = global_hotkey::GlobalHotKeyEvent::receiver();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(event) = event_receiver.try_recv() {
|
match event_receiver.try_recv() {
|
||||||
if event.state == HotKeyState::Pressed {
|
Ok(event) => {
|
||||||
println!("Exit hotkey pressed, shutting down...");
|
println!("Received hotkey event: {:?}", event);
|
||||||
|
if event.state == HotKeyState::Pressed {
|
||||||
// Set running to false
|
println!("Exit hotkey pressed, sending Quit message...");
|
||||||
if let Ok(mut state) = state_clone.lock() {
|
// Send Quit message instead of direct exit
|
||||||
state.running = false;
|
// Get the global sender
|
||||||
|
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||||
|
if let Err(e) = tx.unbounded_send(Message::Quit) {
|
||||||
|
eprintln!("Failed to send Quit message from hotkey thread: {}", e);
|
||||||
|
// Fallback to direct exit if channel fails
|
||||||
|
platform::restore_cursor();
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Hotkey: Global sender not initialized yet");
|
||||||
|
// Fallback to direct exit if sender not available
|
||||||
|
platform::restore_cursor();
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
// Don't exit directly here, let the main thread handle it.
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
// No events or channel disconnected, sleep briefly
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
|
||||||
// Restore the system cursor
|
// Check if we should exit (will be checked in the loop condition below)
|
||||||
platform::restore_cursor();
|
}
|
||||||
|
}
|
||||||
// Exit the application
|
// Check shared state running flag (optional, main thread handles exit)
|
||||||
std::process::exit(0);
|
if let Ok(state) = state_clone.lock() {
|
||||||
|
if !state.running {
|
||||||
|
println!("Hotkey listener detected running=false, exiting thread.");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small sleep to prevent high CPU usage
|
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -260,120 +388,223 @@ fn boot() -> (CursorRush, Task<Message>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
|
// Initialize Steam
|
||||||
let _steam = steam::Steam::new();
|
let _steam = steam::Steam::new();
|
||||||
|
|
||||||
|
// Note: We can't use a consistent window ID across boot() and main()
|
||||||
|
// because iced creates its own window ID internally
|
||||||
|
// Instead, we'll capture the real window ID in the CloseRequested event
|
||||||
iced::application("Cursor Rush", update, view)
|
iced::application("Cursor Rush", update, view)
|
||||||
.theme(|_| Theme::Dark)
|
.theme(|_| Theme::Dark)
|
||||||
.subscription(subscription)
|
.subscription(subscription)
|
||||||
.centered()
|
.centered()
|
||||||
|
// This is the key - we need to tell iced not to exit when the window close button is clicked
|
||||||
|
.exit_on_close_request(false)
|
||||||
|
// run_with expects a function that returns (State, Task), not the tuple directly
|
||||||
.run_with(boot)
|
.run_with(boot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the system tray icon
|
// Set up the system tray icon
|
||||||
|
// Simplified signature without passing the channel
|
||||||
fn setup_tray_icon(
|
fn setup_tray_icon(
|
||||||
cursors: &[CursorInfo],
|
cursors: &[CursorInfo],
|
||||||
shared_state: Arc<Mutex<SharedState>>,
|
shared_state: Arc<Mutex<SharedState>>,
|
||||||
tx: mpsc::Sender<Message>,
|
) -> Option<TrayIcon> { // Return Option<TrayIcon>
|
||||||
) {
|
|
||||||
if cursors.is_empty() {
|
if cursors.is_empty() {
|
||||||
return;
|
return None; // Return None if no cursors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a menu
|
// Create a menu
|
||||||
let mut menu = Menu::new();
|
let menu = Menu::with_items(&[
|
||||||
|
&tray_icon::menu::MenuItem::with_id(NEXT_CURSOR_ID, "Next cursor", true, None),
|
||||||
|
&tray_icon::menu::MenuItem::with_id(SHOW_WINDOW_ID, "Show window", true, None),
|
||||||
|
&tray_icon::menu::MenuItem::with_id(QUIT_ID, "Quit", true, None),
|
||||||
|
]).ok()?; // Use ok()? to convert Result to Option
|
||||||
|
|
||||||
// Add menu items with IDs
|
|
||||||
let next_item = tray_icon::menu::MenuItem::with_id(
|
|
||||||
NEXT_CURSOR_ID,
|
|
||||||
"Next cursor",
|
|
||||||
true,
|
|
||||||
None, // No accelerator
|
|
||||||
);
|
|
||||||
|
|
||||||
let quit_item = tray_icon::menu::MenuItem::with_id(
|
|
||||||
QUIT_ID,
|
|
||||||
"Quit",
|
|
||||||
true,
|
|
||||||
None, // No accelerator
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add items to menu
|
|
||||||
if let Ok(menu_with_items) = Menu::with_items(&[&next_item, &quit_item]) {
|
|
||||||
menu = menu_with_items;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the first PNG as the tray icon
|
// Use the first PNG as the tray icon
|
||||||
let icon_path = &cursors[0].path;
|
let icon_path = &cursors[0].path;
|
||||||
|
let img = image_open(icon_path).ok()?; // Use ok()?
|
||||||
// Load the image
|
|
||||||
let img = match image_open(icon_path) {
|
|
||||||
Ok(img) => img,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to open image {}: {}", icon_path.display(), e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert to RGBA
|
|
||||||
let rgba_img = img.to_rgba8();
|
let rgba_img = img.to_rgba8();
|
||||||
let (width, height) = rgba_img.dimensions();
|
let (width, height) = rgba_img.dimensions();
|
||||||
|
let icon = tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height).ok()?; // Use ok()?
|
||||||
// Create icon from RGBA data
|
|
||||||
let icon = match tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height) {
|
|
||||||
Ok(icon) => icon,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to create icon: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build the tray icon
|
// Build the tray icon
|
||||||
let _tray = TrayIconBuilder::new()
|
println!("Building tray icon...");
|
||||||
|
let tray_result = TrayIconBuilder::new()
|
||||||
.with_tooltip("Cursor Rush")
|
.with_tooltip("Cursor Rush")
|
||||||
.with_icon(icon)
|
.with_icon(icon)
|
||||||
.with_menu(Box::new(menu))
|
.with_menu(Box::new(menu))
|
||||||
.build()
|
.with_menu_on_left_click(false) // Disable menu on left click - only show on right click
|
||||||
.unwrap();
|
.build();
|
||||||
|
|
||||||
|
println!("Tray icon configuration: menu_on_left_click = false");
|
||||||
|
|
||||||
|
// Store the tray icon itself
|
||||||
|
let tray = match tray_result {
|
||||||
|
Ok(tray_instance) => {
|
||||||
|
println!("Tray icon built successfully");
|
||||||
|
tray_instance // Assign the TrayIcon object
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to build tray icon: {}", e);
|
||||||
|
return None; // Return None on error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Listen for tray menu events in a separate thread
|
// Listen for tray menu events in a separate thread
|
||||||
let state_clone = shared_state.clone();
|
let state_clone = shared_state.clone();
|
||||||
|
println!("Spawning tray menu event listener thread...");
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let menu_channel = MenuEvent::receiver();
|
let menu_channel = MenuEvent::receiver();
|
||||||
|
println!("Tray menu event listener waiting for events...");
|
||||||
|
|
||||||
while let Ok(event) = menu_channel.recv() {
|
while let Ok(event) = menu_channel.recv() {
|
||||||
match event.id.as_ref() {
|
println!("Tray menu event received: ID = {:?}", event.id);
|
||||||
|
let msg_to_send = match event.id.as_ref() {
|
||||||
NEXT_CURSOR_ID => {
|
NEXT_CURSOR_ID => {
|
||||||
// Handle next cursor event
|
println!("Tray: Next Cursor requested");
|
||||||
let mut state = state_clone.lock().unwrap();
|
match state_clone.lock() {
|
||||||
if !state.cursors.is_empty() {
|
Ok(mut state) => {
|
||||||
state.current_index = (state.current_index + 1) % state.cursors.len();
|
if !state.cursors.is_empty() {
|
||||||
if let Some(cursor) = state.cursors.get(state.current_index) {
|
state.current_index = (state.current_index + 1) % state.cursors.len();
|
||||||
apply_cursor(&cursor.path);
|
if let Some(cursor) = state.cursors.get(state.current_index) {
|
||||||
// Ensure the render thread is running with the new cursor
|
println!("Tray: Applying next cursor: {}", cursor.name);
|
||||||
platform::render_cursor();
|
apply_cursor(&cursor.path);
|
||||||
|
platform::render_cursor(); // Ensure render starts/updates
|
||||||
// Load the new cursor image
|
state.current_cursor_image = image_open(&cursor.path).ok().map(|img| img.to_rgba8());
|
||||||
let current_cursor_image = match image_open(&cursor.path) {
|
}
|
||||||
Ok(img) => Some(img.to_rgba8()),
|
}
|
||||||
Err(_) => None,
|
},
|
||||||
};
|
Err(e) => eprintln!("Tray: Failed to lock shared state for next cursor: {}", e),
|
||||||
|
|
||||||
// Update the cursor image in shared state
|
|
||||||
state.current_cursor_image = current_cursor_image;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Notify the UI
|
// Send TrayEvent just to trigger UI update if needed
|
||||||
let _ = tx.send(Message::TrayEvent);
|
Some(Message::TrayEvent)
|
||||||
|
},
|
||||||
|
SHOW_WINDOW_ID => {
|
||||||
|
println!("Tray: Show Window requested");
|
||||||
|
// Send ShowWindow message to main thread
|
||||||
|
Some(Message::ShowWindow)
|
||||||
},
|
},
|
||||||
QUIT_ID => {
|
QUIT_ID => {
|
||||||
// Restore the system cursor before exiting
|
println!("Tray: Quit requested");
|
||||||
platform::restore_cursor();
|
// Send Quit message to main thread
|
||||||
|
Some(Message::Quit)
|
||||||
// Exit the application
|
|
||||||
std::process::exit(0);
|
|
||||||
},
|
},
|
||||||
_ => {} // any other menu items you add later
|
_ => {
|
||||||
|
println!("Tray: Unknown menu event ID: {:?}", event.id);
|
||||||
|
None // Don't send a message for unknown IDs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the determined message to the main UI thread using the global sender
|
||||||
|
if let Some(msg) = msg_to_send {
|
||||||
|
// Get the global sender
|
||||||
|
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||||
|
if let Err(e) = tx.unbounded_send(msg.clone()) { // Use clone if msg is needed after send
|
||||||
|
eprintln!("Tray: Failed to send {:?} message: {}", msg, e);
|
||||||
|
break; // Stop thread if channel broken
|
||||||
|
}
|
||||||
|
// If Quit message was sent, break the loop to exit the listener thread
|
||||||
|
if matches!(msg, Message::Quit) {
|
||||||
|
println!("Tray: Quit message sent, exiting listener thread");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Tray: Global sender not initialized yet");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("Tray menu event listener thread finished");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up a direct event handler for tray icon events
|
||||||
|
// This is the preferred way to handle tray icon clicks
|
||||||
|
println!("Setting up tray icon click event handler...");
|
||||||
|
|
||||||
|
// Use set_event_handler to directly handle tray icon events
|
||||||
|
TrayIconEvent::set_event_handler(Some(move |event| {
|
||||||
|
// Only process important events, filter out mouse movements
|
||||||
|
match event {
|
||||||
|
// Handle click events
|
||||||
|
TrayIconEvent::Click { button, .. } => {
|
||||||
|
println!("Tray: Click detected with button: {:?}", button);
|
||||||
|
// Left click should show the window
|
||||||
|
if button == MouseButton::Left {
|
||||||
|
println!("Tray: Left click detected, sending ShowWindow message");
|
||||||
|
// Get the global sender
|
||||||
|
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||||
|
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
|
||||||
|
eprintln!("Tray: Failed to send ShowWindow message via handler: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Tray: Global sender not initialized yet for click handler");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Handle double-click events
|
||||||
|
TrayIconEvent::DoubleClick { button, .. } => {
|
||||||
|
println!("Tray: Double click detected with button: {:?}", button);
|
||||||
|
// Double left click should also show the window
|
||||||
|
if button == MouseButton::Left {
|
||||||
|
println!("Tray: Double left click detected, sending ShowWindow message");
|
||||||
|
// Get the global sender
|
||||||
|
if let Some(tx) = TRAY_SENDER.lock().unwrap().as_ref() {
|
||||||
|
if let Err(e) = tx.unbounded_send(Message::ShowWindow) {
|
||||||
|
eprintln!("Tray: Failed to send ShowWindow message via handler on double click: {}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Tray: Global sender not initialized yet for double click handler");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Silently ignore mouse movement events
|
||||||
|
TrayIconEvent::Move { .. } => {
|
||||||
|
// Don't log anything for mouse movements
|
||||||
|
},
|
||||||
|
// Silently ignore mouse enter/leave events
|
||||||
|
TrayIconEvent::Enter { .. } | TrayIconEvent::Leave { .. } => {
|
||||||
|
// Don't log anything for mouse enter/leave
|
||||||
|
},
|
||||||
|
// Log other events for debugging (but not mouse-related ones)
|
||||||
|
_ => {
|
||||||
|
println!("Tray: Event received: {:?}", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
println!("Tray setup complete using set_event_handler for clicks."); // Updated log
|
||||||
|
Some(tray) // Return the TrayIcon object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to hide a window to the tray
|
||||||
|
fn hide_window_to_tray(window_id: iced::window::Id) -> Task<Message> {
|
||||||
|
println!("Attempting to hide window with ID: {:?}", window_id);
|
||||||
|
|
||||||
|
// Use ONLY the iced method for hiding. Platform specific might interfere.
|
||||||
|
let hide_task: Task<()> = iced::window::change_mode(window_id, iced::window::Mode::Hidden);
|
||||||
|
|
||||||
|
hide_task.map(|_: ()| {
|
||||||
|
println!("Window hide task completed (via iced)");
|
||||||
|
// Return a specific message to confirm the window was hidden
|
||||||
|
Message::WindowHidden
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to show a window from the tray
|
||||||
|
fn show_window_from_tray(window_id: iced::window::Id) -> Task<Message> {
|
||||||
|
println!("Attempting to show window with ID: {:?}", window_id);
|
||||||
|
|
||||||
|
// Use ONLY the iced methods for showing and focusing.
|
||||||
|
let show_task: Task<()> = iced::window::change_mode(window_id, iced::window::Mode::Windowed);
|
||||||
|
let focus_task: Task<()> = iced::window::gain_focus(window_id);
|
||||||
|
|
||||||
|
Task::batch(vec![show_task, focus_task]).map(|_: ()| {
|
||||||
|
println!("Window show/focus tasks completed (via iced)");
|
||||||
|
// Return a specific message to confirm the window was shown
|
||||||
|
Message::WindowShown
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update function to handle messages
|
// Update function to handle messages
|
||||||
@@ -434,11 +665,28 @@ fn update(state: &mut CursorRush, message: Message) -> Task<Message> {
|
|||||||
Task::none()
|
Task::none()
|
||||||
},
|
},
|
||||||
Message::Quit => {
|
Message::Quit => {
|
||||||
|
println!("Update: Handling Quit message");
|
||||||
|
|
||||||
|
// Set running flag to false to signal other threads (like hotkey listener)
|
||||||
|
if let Some(shared) = &state.shared_state {
|
||||||
|
if let Ok(mut shared_state) = shared.lock() {
|
||||||
|
shared_state.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restore the system cursor before exiting
|
// Restore the system cursor before exiting
|
||||||
platform::restore_cursor();
|
platform::restore_cursor();
|
||||||
|
|
||||||
// Exit the application
|
// Cleanly drop the tray icon *before* exiting iced
|
||||||
std::process::exit(0);
|
// This removes the icon from the system tray immediately
|
||||||
|
if let Some(tray) = state.tray_icon.take() {
|
||||||
|
drop(tray);
|
||||||
|
println!("Tray icon explicitly dropped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use iced's exit mechanism for a cleaner shutdown
|
||||||
|
println!("Requesting application exit via iced::exit...");
|
||||||
|
iced::exit()
|
||||||
},
|
},
|
||||||
Message::TrayEvent => {
|
Message::TrayEvent => {
|
||||||
// Sync with shared state
|
// Sync with shared state
|
||||||
@@ -449,11 +697,95 @@ fn update(state: &mut CursorRush, message: Message) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
},
|
},
|
||||||
Message::CursorMoved(position) => {
|
Message::CloseRequested(window_id) => {
|
||||||
// Update the cursor position
|
println!("Handling CloseRequested: Hiding window with ID: {:?}", window_id);
|
||||||
state.cursor_position = position;
|
|
||||||
|
// Store the real window ID for future use
|
||||||
|
// This is critical for showing the window later
|
||||||
|
state.main_window = Some(window_id);
|
||||||
|
println!("Stored window ID: {:?} for future use", window_id);
|
||||||
|
|
||||||
|
// Update shared state first
|
||||||
|
if let Some(shared) = &state.shared_state {
|
||||||
|
match shared.lock() {
|
||||||
|
Ok(mut shared_state) => {
|
||||||
|
if shared_state.window_visible { // Only hide if visible
|
||||||
|
shared_state.window_visible = false;
|
||||||
|
println!("Set window_visible to false in shared state");
|
||||||
|
println!("Hiding window with ID: {:?}, app continues running in tray", window_id);
|
||||||
|
return hide_window_to_tray(window_id);
|
||||||
|
} else {
|
||||||
|
println!("Window already hidden, ignoring CloseRequested.");
|
||||||
|
return Task::none(); // Don't try to hide again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error locking shared state for hiding: {}", e);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Error: Shared state not available for hiding.");
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::WindowHidden => {
|
||||||
|
println!("Window successfully hidden to tray");
|
||||||
Task::none()
|
Task::none()
|
||||||
},
|
},
|
||||||
|
Message::WindowShown => {
|
||||||
|
println!("Update: Received WindowShown message. Window should now be visible.");
|
||||||
|
Task::none()
|
||||||
|
},
|
||||||
|
Message::ShowWindow => {
|
||||||
|
println!("Handling ShowWindow: Attempting to show window"); // More specific starting log
|
||||||
|
|
||||||
|
if let Some(window_id) = state.main_window {
|
||||||
|
println!("Using stored window ID: {:?}", window_id);
|
||||||
|
|
||||||
|
// It's generally safer to attempt the show operation even if the state *thinks* it's visible.
|
||||||
|
// Let's update the shared state *after* we know the task is being created.
|
||||||
|
|
||||||
|
println!("Creating show/focus tasks for window ID: {:?}", window_id);
|
||||||
|
let show_task = iced::window::change_mode(window_id, iced::window::Mode::Windowed);
|
||||||
|
let focus_task = iced::window::gain_focus(window_id);
|
||||||
|
|
||||||
|
// Update shared state *before* returning the task, but after creating it.
|
||||||
|
if let Some(shared) = &state.shared_state {
|
||||||
|
match shared.lock() {
|
||||||
|
Ok(mut shared_state) => {
|
||||||
|
if !shared_state.window_visible { // Only log if changing state
|
||||||
|
println!("Updating shared state: window_visible = true");
|
||||||
|
shared_state.window_visible = true;
|
||||||
|
} else {
|
||||||
|
println!("Shared state already indicates window is visible.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error locking shared state during show: {}", e);
|
||||||
|
// Proceed with showing the window anyway, state might be out of sync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Shared state unavailable during show.");
|
||||||
|
// Proceed with showing the window anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Returning Task::batch to show and focus window.");
|
||||||
|
// Return the batch task
|
||||||
|
return Task::batch(vec![show_task, focus_task])
|
||||||
|
.map(|_: ()| {
|
||||||
|
// This message confirms the Iced runtime *completed* the tasks.
|
||||||
|
println!("Iced runtime reports show/focus tasks completed successfully.");
|
||||||
|
Message::WindowShown // Send confirmation message
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
eprintln!("Error: Main window ID not found when trying to show. Cannot create tasks.");
|
||||||
|
Task::none() // Cannot proceed without window_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,21 +834,21 @@ fn view(state: &CursorRush) -> Element<Message> {
|
|||||||
.push(button("Next cursor").on_press(Message::NextCursor))
|
.push(button("Next cursor").on_press(Message::NextCursor))
|
||||||
.push(button("Quit").on_press(Message::Quit));
|
.push(button("Quit").on_press(Message::Quit));
|
||||||
|
|
||||||
// Add instructions for the hotkey
|
// Add instructions for the hotkey and close button behavior
|
||||||
content = content.push(
|
content = content.push(
|
||||||
text("Press Ctrl+Shift+F12 to exit the application").size(14)
|
container(
|
||||||
|
Column::new()
|
||||||
|
.spacing(5)
|
||||||
|
.push(text("Press Ctrl+Shift+F12 to exit the application").size(16))
|
||||||
|
.push(text("Close button (X) minimizes to system tray").size(16))
|
||||||
|
)
|
||||||
|
.padding(10)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
content = content.push(text("No cursors found in assets directory"));
|
content = content.push(text("No cursors found in assets directory"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add cursor position information for debugging
|
// Cursor position display removed to avoid spammy updates
|
||||||
if !state.cursors.is_empty() {
|
|
||||||
let (x, y) = state.cursor_position;
|
|
||||||
content = content.push(
|
|
||||||
text(format!("Cursor position: ({:.1}, {:.1})", x, y)).size(12)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the main container
|
// Create the main container
|
||||||
container(content)
|
container(content)
|
||||||
|
|||||||
@@ -731,6 +731,32 @@ mod linux {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Windows-specific function to hide a window - DEPRECATED
|
||||||
|
// We now use iced's window::change_mode instead for cross-platform compatibility
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn hide_window() {
|
||||||
|
println!("WARNING: platform::hide_window is deprecated. Use iced::window::change_mode instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows-specific function to show a window - DEPRECATED
|
||||||
|
// We now use iced's window::change_mode instead for cross-platform compatibility
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn show_window() {
|
||||||
|
println!("WARNING: platform::show_window is deprecated. Use iced::window::change_mode instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder for non-Windows platforms - DEPRECATED
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn hide_window() {
|
||||||
|
println!("WARNING: platform::hide_window is deprecated. Use iced::window::change_mode instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder for non-Windows platforms - DEPRECATED
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn show_window() {
|
||||||
|
println!("WARNING: platform::show_window is deprecated. Use iced::window::change_mode instead.");
|
||||||
|
}
|
||||||
|
|
||||||
// Export the appropriate implementation based on platform
|
// Export the appropriate implementation based on platform
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use windows::{set_cursor, restore_cursor, render_cursor};
|
pub use windows::{set_cursor, restore_cursor, render_cursor};
|
||||||
|
|||||||
Reference in New Issue
Block a user