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