diff --git a/src-tauri/src/panel.rs b/src-tauri/src/panel.rs index 2c5165da..ce440aee 100644 --- a/src-tauri/src/panel.rs +++ b/src-tauri/src/panel.rs @@ -3,6 +3,64 @@ use tauri_nspanel::{ CollectionBehavior, ManagerExt, PanelLevel, StyleMask, WebviewWindowExt, tauri_panel, }; +fn monitor_contains_physical_point( + origin_x: f64, + origin_y: f64, + width: f64, + height: f64, + point_x: f64, + point_y: f64, +) -> bool { + point_x >= origin_x + && point_x < origin_x + width + && point_y >= origin_y + && point_y < origin_y + height +} + +unsafe fn set_panel_frame_top_left(panel: &tauri_nspanel::NSPanel, x: f64, y: f64) { + let point = tauri_nspanel::NSPoint::new(x, y); + let _: () = objc2::msg_send![panel, setFrameTopLeftPoint: point]; +} + +fn set_panel_top_left_immediately( + window: &tauri::WebviewWindow, + app_handle: &AppHandle, + panel_x: f64, + panel_y: f64, + primary_logical_h: f64, +) { + let Ok(panel_handle) = app_handle.get_webview_panel("main") else { + return; + }; + + let target_x = panel_x; + let target_y = primary_logical_h - panel_y; + + if objc2_foundation::MainThreadMarker::new().is_some() { + unsafe { + set_panel_frame_top_left(panel_handle.as_panel(), target_x, target_y); + } + return; + } + + let (tx, rx) = std::sync::mpsc::channel(); + let panel_handle = panel_handle.clone(); + + if let Err(error) = window.run_on_main_thread(move || { + unsafe { + set_panel_frame_top_left(panel_handle.as_panel(), target_x, target_y); + } + let _ = tx.send(()); + }) { + log::warn!("Failed to position panel on main thread: {}", error); + return; + } + + if rx.recv().is_err() { + log::warn!("Failed waiting for panel position on main thread"); + } +} + /// Macro to get existing panel or initialize it if needed. /// Returns Option - Some if panel is available, None on error. macro_rules! get_or_init_panel { @@ -30,10 +88,31 @@ macro_rules! get_or_init_panel { // Export macro for use in other modules pub(crate) use get_or_init_panel; -/// Show the panel (initializing if needed). +/// Retrieve the tray icon rect and position the panel beneath it. +/// No-ops gracefully if the tray icon or its rect is unavailable. +fn position_panel_from_tray(app_handle: &AppHandle) { + let Some(tray) = app_handle.tray_by_id("tray") else { + log::debug!("position_panel_from_tray: tray icon not found"); + return; + }; + match tray.rect() { + Ok(Some(rect)) => { + position_panel_at_tray_icon(app_handle, rect.position, rect.size); + } + Ok(None) => { + log::debug!("position_panel_from_tray: tray rect not available yet"); + } + Err(e) => { + log::warn!("position_panel_from_tray: failed to get tray rect: {}", e); + } + } +} + +/// Show the panel (initializing if needed), positioned under the tray icon. pub fn show_panel(app_handle: &AppHandle) { if let Some(panel) = get_or_init_panel!(app_handle) { panel.show_and_make_key(); + position_panel_from_tray(app_handle); } } @@ -50,6 +129,7 @@ pub fn toggle_panel(app_handle: &AppHandle) { } else { log::debug!("toggle_panel: showing panel"); panel.show_and_make_key(); + position_panel_from_tray(app_handle); } } @@ -115,17 +195,6 @@ pub fn position_panel_at_tray_icon( ) { let window = app_handle.get_webview_window("main").unwrap(); - // Tray icon events on macOS report coordinates in a hybrid physical space where - // each monitor region uses its own scale (logical_pos × scale = physical origin). - // On mixed-DPI setups this creates overlapping regions, making it impossible to - // reliably determine the correct monitor from tray coordinates alone. - // - // Instead, we use NSEvent::mouseLocation() which returns the cursor position in - // macOS's unified logical (point) coordinate space — always unambiguous regardless - // of how many monitors or scale factors are involved. We find which monitor - // contains the cursor, then convert the tray icon's physical coordinates to - // logical coordinates within that monitor. - let (icon_phys_x, icon_phys_y) = match &icon_position { Position::Physical(pos) => (pos.x as f64, pos.y as f64), Position::Logical(pos) => (pos.x, pos.y), @@ -135,12 +204,6 @@ pub fn position_panel_at_tray_icon( Size::Logical(s) => (s.width, s.height), }; - // Get the cursor's logical position via NSEvent — this is in macOS's flipped - // coordinate system (origin at bottom-left of primary screen). - let mouse_logical = objc2_app_kit::NSEvent::mouseLocation(); - - // Convert from macOS bottom-left origin to top-left origin used by Tauri. - // Primary screen height (in points) defines the flip axis. let monitors = window.available_monitors().expect("failed to get monitors"); let primary_logical_h = window .primary_monitor() @@ -149,35 +212,29 @@ pub fn position_panel_at_tray_icon( .map(|m| m.size().height as f64 / m.scale_factor()) .unwrap_or(0.0); - let mouse_x = mouse_logical.x; - let mouse_y = primary_logical_h - mouse_logical.y; - - // Find the monitor containing the cursor in logical space (no ambiguity). - let mut found_monitor = None; - for m in &monitors { - let pos = m.position(); - let scale = m.scale_factor(); - let logical_w = m.size().width as f64 / scale; - let logical_h = m.size().height as f64 / scale; - - let logical_x = pos.x as f64 / scale; - let logical_y = pos.y as f64 / scale; - let x_in = mouse_x >= logical_x && mouse_x < logical_x + logical_w; - let y_in = mouse_y >= logical_y && mouse_y < logical_y + logical_h; - - if x_in && y_in { - found_monitor = Some(m.clone()); - break; - } - } + let icon_center_x = icon_phys_x + (icon_phys_w / 2.0); + let icon_center_y = icon_phys_y + (icon_phys_h / 2.0); + + let found_monitor = monitors.iter().find(|monitor| { + let origin = monitor.position(); + let size = monitor.size(); + monitor_contains_physical_point( + origin.x as f64, + origin.y as f64, + size.width as f64, + size.height as f64, + icon_center_x, + icon_center_y, + ) + }); let monitor = match found_monitor { - Some(m) => m, + Some(m) => m.clone(), None => { log::warn!( - "No monitor found for cursor at ({:.0}, {:.0}), using primary", - mouse_x, - mouse_y + "No monitor found for tray rect center at ({:.0}, {:.0}), using primary", + icon_center_x, + icon_center_y ); match window.primary_monitor() { Ok(Some(m)) => m, @@ -187,16 +244,13 @@ pub fn position_panel_at_tray_icon( }; let target_scale = monitor.scale_factor(); - let mon_logical_x = monitor.position().x as f64; - let mon_logical_y = monitor.position().y as f64; - - // Convert tray icon physical coords to logical within the identified monitor. - // Physical origin of this monitor in the hybrid tray coordinate space: - let phys_origin_x = mon_logical_x * target_scale; - let phys_origin_y = mon_logical_y * target_scale; + let mon_phys_x = monitor.position().x as f64; + let mon_phys_y = monitor.position().y as f64; + let mon_logical_x = mon_phys_x / target_scale; + let mon_logical_y = mon_phys_y / target_scale; - let icon_logical_x = mon_logical_x + (icon_phys_x - phys_origin_x) / target_scale; - let icon_logical_y = mon_logical_y + (icon_phys_y - phys_origin_y) / target_scale; + let icon_logical_x = mon_logical_x + (icon_phys_x - mon_phys_x) / target_scale; + let icon_logical_y = mon_logical_y + (icon_phys_y - mon_phys_y) / target_scale; let icon_logical_w = icon_phys_w / target_scale; let icon_logical_h = icon_phys_h / target_scale; @@ -220,5 +274,5 @@ pub fn position_panel_at_tray_icon( let nudge_up: f64 = 6.0; let panel_y = icon_logical_y + icon_logical_h - nudge_up; - let _ = window.set_position(tauri::LogicalPosition::new(panel_x, panel_y)); + set_panel_top_left_immediately(&window, app_handle, panel_x, panel_y, primary_logical_h); }