Reorganize codebase into modular structure

This commit is contained in:
2025-05-09 21:37:15 -05:00
parent f4c83e31c0
commit cf1c90856f
24 changed files with 2008 additions and 817 deletions
+7
View File
@@ -0,0 +1,7 @@
// UI module
// Contains UI components and rendering logic
pub mod view;
pub mod tray;
pub use view::view_app;
+194
View File
@@ -0,0 +1,194 @@
// System tray functionality
use std::{
sync::{Arc, Mutex},
thread,
};
use ::image::open as image_open;
use tray_icon::{
TrayIconBuilder,
TrayIcon,
TrayIconEvent,
MouseButton,
menu::{Menu, MenuEvent},
};
use crate::app::{Message, CursorInfo, TRAY_SENDER, SharedState};
// Define menu item IDs
const NEXT_CURSOR_ID: &str = "next_cursor";
const SHOW_WINDOW_ID: &str = "show_window";
const QUIT_ID: &str = "quit";
// Set up the system tray icon
pub fn setup_tray_icon(
cursors: &[CursorInfo],
shared_state: Arc<Mutex<SharedState>>,
) -> Option<TrayIcon> { // Return Option<TrayIcon>
if cursors.is_empty() {
return None; // Return None if no cursors
}
// Create a menu
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
// Use the first PNG as the tray icon
let icon_path = &cursors[0].path;
let img = image_open(icon_path).ok()?; // Use ok()?
let rgba_img = img.to_rgba8();
let (width, height) = rgba_img.dimensions();
let icon = tray_icon::Icon::from_rgba(rgba_img.into_raw(), width, height).ok()?; // Use ok()?
// Build the tray icon
println!("Building tray icon...");
let tray_result = TrayIconBuilder::new()
.with_tooltip("Cursor Rush")
.with_icon(icon)
.with_menu(Box::new(menu))
.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() {
println!("Tray menu event received: ID = {:?}", event.id);
let msg_to_send = match event.id.as_ref() {
NEXT_CURSOR_ID => {
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: Updating cursor image: {}", cursor.name);
crate::app::apply_cursor(&cursor.path);
// No need to call render_cursor anymore as the mouse hook handles rendering
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),
}
// 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 => {
println!("Tray: Quit requested");
// Send Quit message to main thread
Some(Message::Quit)
},
_ => {
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
}
+77
View File
@@ -0,0 +1,77 @@
// UI view components
use iced::{
widget::{button, text, Column, Row, container, image as iced_image},
Element, Length,
};
use crate::app::{CursorRush, Message};
// View function to render the UI
pub fn view_app(state: &CursorRush) -> Element<Message> {
let mut content = Column::new()
.spacing(16)
.push(text("Cursor Rush").size(24));
// Add a button for each cursor
if !state.cursors.is_empty() {
let current_cursor = &state.cursors[state.current_index];
content = content.push(text(format!("Current cursor: {}", current_cursor.name)));
// Display the current cursor image as a preview
let img_handle = iced_image::Handle::from_path(&current_cursor.path);
let cursor_image = iced_image::Image::new(img_handle)
.width(Length::Fixed(64.0))
.height(Length::Fixed(64.0));
content = content.push(cursor_image);
// Create a row of cursor buttons
let mut cursor_row = Row::new().spacing(10);
for (i, cursor) in state.cursors.iter().enumerate() {
// Create a small preview image for each cursor
let preview_handle = iced_image::Handle::from_path(&cursor.path);
let preview_image = iced_image::Image::new(preview_handle)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0));
let btn = button(
Row::new()
.spacing(5)
.push(preview_image)
.push(text(&cursor.name).size(16))
)
.on_press(Message::SelectCursor(i));
cursor_row = cursor_row.push(btn);
}
content = content
.push(cursor_row)
.push(button("Next cursor").on_press(Message::NextCursor))
.push(button("Quit").on_press(Message::Quit));
// Add instructions for the hotkey and close button behavior
content = content.push(
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"));
}
// Create the main container
container(content)
.width(Length::Fill)
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.padding(20)
.into()
}