From fc9d43a61a6f20580e1ed6e9fff901e8a9e86235 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=9F=8F=E9=9D=92?= Date: Tue, 16 Jun 2026 13:10:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(windows):=20=E6=81=A2=E5=A4=8D=E8=83=B6?= =?UTF-8?q?=E5=9B=8A=E5=9B=9B=E8=BE=B9=20clamp=20+=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8C=BA=E5=AE=9A=E4=BD=8D=EF=BC=8C=E4=BF=AE=E8=A2=AB=201ea467?= =?UTF-8?q?a=20=E5=9B=9E=E9=80=80=E7=9A=84=20#470=20(#689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #470 曾用 fab9651(+4824080 防溢出)加入「四边 clamp + 工作区(rcWork,避开任务栏) 定位」,被 Android 移植 commit 1ea467a 回退:ForegroundMonitor 丢了 work_* 字段、 position_capsule_bottom_center 退回只夹上边 `y.max(mon.top)`(x/下/右不夹、不避任务栏)、 clamp_to_monitor 沦为死码、专用单测被删。多显示器/负坐标/异常 DPI 下胶囊可能被算到 屏幕外或压在任务栏上。 - ForegroundMonitor 重新加回 work_left/top/right/bottom,foreground_window_monitor 从 mi.rcWork 填充; - position_capsule_bottom_center 改回用 clamp_to_monitor 四边夹工作区(取不到 rcWork 退回整屏); - 恢复 clamp_to_monitor 的 3 个纯函数单测(on-screen 不动 / 右下溢出收回 / 右缘内溢收回)。 clamp_to_monitor 本体一直在(含 saturating_sub 防溢出),只是没被调用。Windows 结构 + rcWork 由 CI 编译验证;需 Windows 真机确认多屏下胶囊不再出屏/压任务栏。 --- openless-all/app/src-tauri/src/lib.rs | 61 ++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 4b8e8903..c1dc1f07 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -2362,6 +2362,11 @@ pub(crate) struct ForegroundMonitor { pub(crate) top: i32, pub(crate) right: i32, pub(crate) bottom: i32, + /// 工作区矩形(rcWork,= 显示器矩形减去任务栏),用于把胶囊夹在任务栏之外。 + pub(crate) work_left: i32, + pub(crate) work_top: i32, + pub(crate) work_right: i32, + pub(crate) work_bottom: i32, /// 该显示器的有效 DPI 缩放(1.0 = 96dpi)。 pub(crate) scale: f64, } @@ -2399,6 +2404,10 @@ pub(crate) fn foreground_window_monitor() -> Option { top: mi.rcMonitor.top, right: mi.rcMonitor.right, bottom: mi.rcMonitor.bottom, + work_left: mi.rcWork.left, + work_top: mi.rcWork.top, + work_right: mi.rcWork.right, + work_bottom: mi.rcWork.bottom, scale: (dpi_x as f64 / 96.0).max(0.1), }) } @@ -2431,15 +2440,24 @@ pub(crate) fn position_capsule_bottom_center( 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; - let clamped_y = y.max(mon.top); - // #470 诊断 v2:当前只夹了上边(.max(mon.top)),未夹下/左/右。多显示器、 - // 负坐标或异常 DPI 下胶囊可能被算到屏幕外却无任何观测。记录显示器几何与 - // 最终落点,用于证伪/证实「胶囊定位到屏幕外」(C 子嫌疑)。 + // #470:用工作区(rcWork,已避开任务栏)四边夹住整窗,防止多显示器、负坐标或异常 + // DPI 下胶囊被算到屏幕外、或压在任务栏上。取不到 rcWork 时(理论上不会,rcWork 总 + // 随 rcMonitor 一同填)退回整屏矩形。 + let (work_l, work_t, work_r, work_b) = + if mon.work_right > mon.work_left && mon.work_bottom > mon.work_top { + (mon.work_left, mon.work_top, mon.work_right, mon.work_bottom) + } else { + (mon.left, mon.top, mon.right, mon.bottom) + }; + let (clamped_x, clamped_y) = + clamp_to_monitor(x, y, phys_w, phys_h, work_l, work_t, work_r, work_b); log::debug!( - "[capsule] win position: mon=({},{})..({},{}) scale={:.2} size=({}x{}) -> x={} y={} clamped_y={}", - mon.left, mon.top, mon.right, mon.bottom, scale, phys_w, phys_h, x, y, clamped_y + "[capsule] win position: mon=({},{})..({},{}) work=({},{})..({},{}) scale={:.2} size=({}x{}) -> x={} y={} -> clamped=({},{})", + mon.left, mon.top, mon.right, mon.bottom, + work_l, work_t, work_r, work_b, + scale, phys_w, phys_h, x, y, clamped_x, clamped_y ); - window.set_position(PhysicalPosition::new(x, clamped_y))?; + window.set_position(PhysicalPosition::new(clamped_x, clamped_y))?; return Ok(()); } // 仅当 Win32 取不到前台显示器时,落回下面的 current_monitor 逻辑。 @@ -2622,6 +2640,35 @@ mod tests { ); } + // #470:胶囊四边 clamp(工作区,避开任务栏 + 不溢出屏外)。clamp_to_monitor 是纯函数, + // 三平台都跑。area = (0,0)..(1920,1040),窗口 264x126。 + #[test] + fn clamp_to_monitor_leaves_on_screen_position_untouched() { + // 完全在工作区内 → 不改动。 + assert_eq!( + clamp_to_monitor(800, 900, 264, 126, 0, 0, 1920, 1040), + (800, 900) + ); + } + + #[test] + fn clamp_to_monitor_pulls_back_off_screen_right_and_bottom() { + // 右/下溢出 → 收回到「整窗仍可见」的最大位置(area_right-w, area_bottom-h)。 + assert_eq!( + clamp_to_monitor(2000, 1200, 264, 126, 0, 0, 1920, 1040), + (1920 - 264, 1040 - 126) + ); + } + + #[test] + fn clamp_to_monitor_pulls_back_when_right_edge_overflows_inside_area() { + // 左上角在区域内、但右缘 x+w 超出 area_right → 仍需收回 x。 + assert_eq!( + clamp_to_monitor(1800, 500, 264, 126, 0, 0, 1920, 1040), + (1920 - 264, 500) + ); + } + #[test] fn capsule_window_bounds_expand_for_translation_badge() { let bounds = capsule_window_bounds(true); From 68b31114f692a57c3d477ba3ea3e0ff8eb275b91 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 16 Jun 2026 14:27:22 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(windows):=20=E8=A1=A5=E5=9B=9E=20clamp?= =?UTF-8?q?=5Fto=5Fmonitor=20=E8=B4=9F=E5=8E=9F=E7=82=B9=E5=B7=A6=E5=B1=8F?= =?UTF-8?q?=20+=20=E5=B7=A5=E4=BD=9C=E5=8C=BA=E9=81=BF=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=A0=8F=E4=B8=A4=E4=B8=AA=E5=8D=95=E6=B5=8B=20(#689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审核指出原 fab9651 的 5 个 clamp 单测只恢复了 3 个,漏掉的恰是本 PR 主打的两类 场景:负原点多显示器(左侧副屏)与工作区避让任务栏。补回这两个纯函数单测。 --- openless-all/app/src-tauri/src/lib.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index c1dc1f07..135fc43c 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -2669,6 +2669,24 @@ mod tests { ); } + #[test] + fn clamp_to_monitor_pushes_into_negative_origin_left_monitor() { + // 副屏在主屏左侧(负 X 原点),落点算到了副屏左外侧 → 夹回 area_left。 + // 1.5x DPI 下尺寸偏大,但 area 仍宽于窗口,左上角夹到 (-2560, top)。 + let (x, y) = clamp_to_monitor(-3000, -100, 294, 138, -2560, 0, 0, 1440); + assert_eq!(x, -2560); + assert_eq!(y, 0); + } + + #[test] + fn clamp_to_monitor_respects_work_area_above_taskbar() { + // 工作区底部 = 1040(任务栏占了 1040..1080)。落点本在任务栏区域(y=1030), + // 应被夹到「工作区底 - 窗口高」之上,胶囊整窗不压任务栏。 + let (_x, y) = clamp_to_monitor(800, 1030, 264, 126, 0, 0, 1920, 1040); + assert_eq!(y, 1040 - 126); + assert!(y + 126 <= 1040); + } + #[test] fn capsule_window_bounds_expand_for_translation_badge() { let bounds = capsule_window_bounds(true);