Merge pull request #569 from katanumahotori/fix/capsule-follow-input-monitor

fix(windows): 多显示器下胶囊跟随正在输入的屏幕
This commit is contained in:
Chris233 authored and GitHub committed 2026-06-01 12:42:26 +08:00
commit 137c175ccb
3 files changed
+116 -9

No files matched your search

+1
View File
@@ -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",
+38 -7
View File
@@ -5109,21 +5109,52 @@ struct CapsuleLayoutState {
scale_bits: u64,
}
fn maybe_position_capsule_bottom_center<R: tauri::Runtime>(
inner: &Arc<Inner>,
/// 返回胶囊「应该摆放到的显示器」的标识信息。
///
/// 它看的显示器必须和 `position_capsule_bottom_center` 实际定位用的一致:
/// Windows 看「正在输入的 App 所在显示器」,其它平台看胶囊自己的显示器。
/// 这是「是否需要重新定位」去重缓存(`maybe_position_capsule_bottom_center`)
/// 的 key,如果这里看错了显示器,就会出现「输入焦点移到另一块屏、胶囊却没
/// 跟过去」的 bug。
fn capsule_layout_snapshot<R: tauri::Runtime>(
window: &tauri::WebviewWindow<R>,
translation_active: bool,
) {
let Some(monitor) = window.current_monitor().ok().flatten() else {
return;
};
let next = CapsuleLayoutState {
) -> Option<CapsuleLayoutState> {
// 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<R: tauri::Runtime>(
inner: &Arc<Inner>,
window: &tauri::WebviewWindow<R>,
translation_active: bool,
) {
let Some(next) = capsule_layout_snapshot(window, translation_active) else {
return;
};
{
let last = inner.capsule_layout.lock();
+77 -2
View File
@@ -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;
@@ -1257,17 +1260,89 @@ fn show_qa_window_no_activate<R: tauri::Runtime>(window: &tauri::WebviewWindow<R
true
}
/// 输入目标显示器的物理矩形(虚拟桌面坐标)+ DPI 缩放。
#[cfg(target_os = "windows")]
pub(crate) struct ForegroundMonitor {
pub(crate) left: i32,
pub(crate) top: i32,
pub(crate) right: i32,
pub(crate) bottom: i32,
/// 该显示器的有效 DPI 缩放(1.0 = 96dpi)。
pub(crate) scale: f64,
}
/// 用 Win32 定位「当前前台窗口(= 用户正在输入的 App)」所在的显示器。
/// 多显示器下用它把胶囊摆到「正在输入的那块屏」。`window.current_monitor()`
/// 返回的是胶囊窗口自己所在的显示器,因此不能用它来跟随输入位置。
#[cfg(target_os = "windows")]
pub(crate) fn foreground_window_monitor() -> Option<ForegroundMonitor> {
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::<MONITORINFO>() 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<R: tauri::Runtime>(
window: &tauri::WebviewWindow<R>,
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();