diff --git a/openless-all/app/src-tauri/Cargo.toml b/openless-all/app/src-tauri/Cargo.toml index 584280b2..70d8533e 100644 --- a/openless-all/app/src-tauri/Cargo.toml +++ b/openless-all/app/src-tauri/Cargo.toml @@ -96,6 +96,7 @@ windows = { version = "0.58", features = [ "Win32_System_Ole", "Win32_System_Registry", "Win32_System_Threading", + "Win32_UI_HiDpi", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_TextServices", diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 12ce3cf0..26e5987b 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -5098,21 +5098,52 @@ struct CapsuleLayoutState { scale_bits: u64, } -fn maybe_position_capsule_bottom_center( - inner: &Arc, +/// 返回胶囊「应该摆放到的显示器」的标识信息。 +/// +/// 它看的显示器必须和 `position_capsule_bottom_center` 实际定位用的一致: +/// Windows 看「正在输入的 App 所在显示器」,其它平台看胶囊自己的显示器。 +/// 这是「是否需要重新定位」去重缓存(`maybe_position_capsule_bottom_center`) +/// 的 key,如果这里看错了显示器,就会出现「输入焦点移到另一块屏、胶囊却没 +/// 跟过去」的 bug。 +fn capsule_layout_snapshot( window: &tauri::WebviewWindow, translation_active: bool, -) { - let Some(monitor) = window.current_monitor().ok().flatten() else { - return; - }; - let next = CapsuleLayoutState { +) -> Option { + // Windows:以「正在输入的 App 所在显示器」为基准。若用胶囊自己的 + // current_monitor,输入焦点切到另一块屏时胶囊仍在原屏 → 误判「没变化」 + // → 跳过重新定位。 + #[cfg(target_os = "windows")] + { + if let Some(mon) = crate::foreground_window_monitor() { + return Some(CapsuleLayoutState { + translation_active, + monitor_x: mon.left, + monitor_y: mon.top, + monitor_width: (mon.right - mon.left).max(0) as u32, + monitor_height: (mon.bottom - mon.top).max(0) as u32, + scale_bits: mon.scale.to_bits(), + }); + } + // 仅当 Win32 取不到前台显示器时,落回下面的 current_monitor。 + } + let monitor = window.current_monitor().ok().flatten()?; + Some(CapsuleLayoutState { translation_active, monitor_x: monitor.position().x, monitor_y: monitor.position().y, monitor_width: monitor.size().width, monitor_height: monitor.size().height, scale_bits: monitor.scale_factor().to_bits(), + }) +} + +fn maybe_position_capsule_bottom_center( + inner: &Arc, + window: &tauri::WebviewWindow, + translation_active: bool, +) { + let Some(next) = capsule_layout_snapshot(window, translation_active) else { + return; }; { let last = inner.capsule_layout.lock(); diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 8b808a61..4b75e11e 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -55,7 +55,10 @@ use tauri::menu::{ CheckMenuItemBuilder, Menu, MenuBuilder, MenuItemBuilder, Submenu, SubmenuBuilder, }; use tauri::tray::{MouseButton, TrayIconBuilder, TrayIconEvent}; -use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, RunEvent, Runtime}; +use tauri::{ + AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, PhysicalPosition, PhysicalSize, + RunEvent, Runtime, +}; use crate::types::PolishMode; @@ -1256,17 +1259,89 @@ fn show_qa_window_no_activate(window: &tauri::WebviewWindow Option { + use windows::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST, + }; + use windows::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}; + use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow; + + unsafe { + let hwnd = GetForegroundWindow(); + let hmon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if hmon.is_invalid() { + return None; + } + let mut mi = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + if !GetMonitorInfoW(hmon, &mut mi).as_bool() { + return None; + } + let mut dpi_x: u32 = 96; + let mut dpi_y: u32 = 96; + // 取不到时退回 96dpi 继续,不让定位整体失败。 + let _ = GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y); + Some(ForegroundMonitor { + left: mi.rcMonitor.left, + top: mi.rcMonitor.top, + right: mi.rcMonitor.right, + bottom: mi.rcMonitor.bottom, + scale: (dpi_x as f64 / 96.0).max(0.1), + }) + } +} + /// 把 capsule 窗口移到屏幕底部居中,与 Swift `CapsuleWindowController.repositionToBottomCenter` 同效。 /// 留 80pt 给 macOS Dock;Windows 任务栏一般在底部 48pt 以内,整体也合适。 pub(crate) fn position_capsule_bottom_center( window: &tauri::WebviewWindow, translation_active: bool, ) -> tauri::Result<()> { + let bounds = capsule_window_bounds(translation_active); + + // Windows:跟随「正在输入的 App」所在显示器摆放,避免多显示器下胶囊 + // 总是固定出现在主屏 / 胶囊自己那块屏。 + #[cfg(target_os = "windows")] + { + if let Some(mon) = foreground_window_monitor() { + let scale = mon.scale; + let phys_w = (bounds.width * scale).round() as i32; + let phys_h = (bounds.height * scale).round() as i32; + window.set_size(PhysicalSize::new(phys_w.max(1) as u32, phys_h.max(1) as u32))?; + + let mon_w = mon.right - mon.left; + let x = mon.left + ((mon_w - phys_w) / 2).max(0); + // 与既有行为一致:「距底部 visual高度 + 80 + inset」,按 physical px 计算。 + let offset_from_bottom = + (capsule_visual_height(translation_active) + 80.0 + bounds.bottom_inset) * scale; + let y = ((mon.bottom as f64) - offset_from_bottom).round() as i32; + window.set_position(PhysicalPosition::new(x, y.max(mon.top)))?; + return Ok(()); + } + // 仅当 Win32 取不到前台显示器时,落回下面的 current_monitor 逻辑。 + } + let monitor = match window.current_monitor()? { Some(m) => m, None => return Ok(()), }; - let bounds = capsule_window_bounds(translation_active); window.set_size(LogicalSize::new(bounds.width, bounds.height))?; let scale = monitor.scale_factor();