From 287a0ff4b43519376a6de70ee739590f482755dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=9F=8F=E9=9D=92?= Date: Wed, 10 Jun 2026 09:26:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(capsule):=20=E7=BB=88=E6=80=81/=E7=A9=BA?= =?UTF-8?q?=E9=97=B2=E8=AE=A9=E8=83=B6=E5=9B=8A=E7=82=B9=E5=87=BB=E7=A9=BF?= =?UTF-8?q?=E9=80=8F=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=B4=B4=E8=BF=91=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=A1=86=E8=AF=AF=E8=A7=A6=E5=BC=B9=E4=B8=BB=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=20(#631)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 胶囊窗口 220×110 远大于可见 pill,录音完成后 2s toast + 360ms 离场动画期间 透明区域仍会吃掉点击并激活 OpenLess(把主界面带到前台)。在 emit_capsule 主线程闭包按状态切换 set_ignore_cursor_events:Done/Cancelled/Error/Idle 点击穿透到下层应用;Recording/Transcribing/Polishing 保留 ✕/✓ 按钮可点。 按状态变化去重,录音中 ~30Hz 电平帧不重复调系统 API。Linux 分支不操作 胶囊窗口,不受影响。 --- .../app/src-tauri/src/coordinator/capsule.rs | 22 +++++++++++++++++++ .../app/src-tauri/src/coordinator/tests.rs | 13 +++++++++++ 2 files changed, 35 insertions(+) diff --git a/openless-all/app/src-tauri/src/coordinator/capsule.rs b/openless-all/app/src-tauri/src/coordinator/capsule.rs index 9df2a57c..488ad645 100644 --- a/openless-all/app/src-tauri/src/coordinator/capsule.rs +++ b/openless-all/app/src-tauri/src/coordinator/capsule.rs @@ -29,6 +29,9 @@ pub(crate) fn capsule_show_strategy_for_platform() -> CapsuleShowStrategy { static CAPSULE_NO_ACTIVATE_FALLBACK_WARNED: AtomicBool = AtomicBool::new(false); static CAPSULE_SUPPRESSED_BY_TOGGLE_LOGGED: AtomicBool = AtomicBool::new(false); static CAPSULE_FIRST_SHOW_LOGGED: AtomicBool = AtomicBool::new(false); +// issue #631:上一次应用到胶囊窗口的点击穿透值。初始 false 与窗口创建时一致 +// (tauri.conf.json 未设 ignore),按变化去重,避免录音中 ~30Hz 电平帧重复调系统 API。 +static CAPSULE_IGNORE_CURSOR_APPLIED: AtomicBool = AtomicBool::new(false); // #470 诊断 v2:capsule webview 句柄取不到时的一次性门,区分「窗口压根没创建」(A0)。 static CAPSULE_WINDOW_MISSING_LOGGED: AtomicBool = AtomicBool::new(false); @@ -69,6 +72,18 @@ pub(crate) fn show_capsule_window_for_recording( } } +/// issue #631:该状态下胶囊窗口是否应忽略鼠标事件(点击穿透到下层应用)。 +/// 胶囊窗口 220×110 远大于可见 pill,贴近输入框时透明区域会吃掉用户点击并激活 +/// OpenLess(误触弹出主界面)。终态(Done/Cancelled/Error)与 Idle 没有可交互 +/// 按钮——包括 2s toast 停留和离场动画期间——让点击穿透;录音/转写/润色态有 +/// ✕/✓ 按钮,保持可点。 +pub(crate) fn capsule_ignore_cursor_for_state(state: CapsuleState) -> bool { + !matches!( + state, + CapsuleState::Recording | CapsuleState::Transcribing | CapsuleState::Polishing + ) +} + /// 终止态(Done / Cancelled / Error)后延迟 N ms 把胶囊改回 Idle,让浮窗自动消失。 /// 用户点 ✕ / ✓ / 中途出错 / 按 Esc 都走这里,统一 2 秒。 pub(crate) const CAPSULE_AUTO_HIDE_DELAY_MS: u64 = 2000; @@ -242,6 +257,13 @@ pub(crate) fn emit_capsule( #[cfg(not(target_os = "linux"))] { + // issue #631:终态/空闲让胶囊点击穿透,录音完成后用户点击贴近的输入框 + // 不再误触胶囊激活 OpenLess。状态变化时才真正调系统 API。 + let ignore_cursor = capsule_ignore_cursor_for_state(state); + if CAPSULE_IGNORE_CURSOR_APPLIED.swap(ignore_cursor, Ordering::SeqCst) != ignore_cursor { + let _ = window.set_ignore_cursor_events(ignore_cursor); + } + // 三平台统一:Done / Cancelled / Error 状态保留 ~1.5s toast // (schedule_capsule_idle 之后会回 Idle 隐藏)。 // Windows 上 linger 的真实问题(截图选中 / 死区 / 拖拽卡顿)由 #140 加的 diff --git a/openless-all/app/src-tauri/src/coordinator/tests.rs b/openless-all/app/src-tauri/src/coordinator/tests.rs index 51e01556..1050e984 100644 --- a/openless-all/app/src-tauri/src/coordinator/tests.rs +++ b/openless-all/app/src-tauri/src/coordinator/tests.rs @@ -810,6 +810,19 @@ fn window_hotkey_fallback_is_disabled_when_no_explicit_fallback_is_advertised() ); } +#[test] +fn capsule_ignore_cursor_only_in_non_interactive_states() { + // issue #631:有 ✕/✓ 按钮的三个状态必须可点;终态/空闲(含 toast 停留与 + // 离场动画期间)点击穿透,避免误触激活 OpenLess 弹出主界面。 + assert!(!capsule_ignore_cursor_for_state(CapsuleState::Recording)); + assert!(!capsule_ignore_cursor_for_state(CapsuleState::Transcribing)); + assert!(!capsule_ignore_cursor_for_state(CapsuleState::Polishing)); + assert!(capsule_ignore_cursor_for_state(CapsuleState::Done)); + assert!(capsule_ignore_cursor_for_state(CapsuleState::Cancelled)); + assert!(capsule_ignore_cursor_for_state(CapsuleState::Error)); + assert!(capsule_ignore_cursor_for_state(CapsuleState::Idle)); +} + #[test] fn capsule_show_strategy_matches_platform_activation_contract() { // 平台列表必须与 capsule_show_strategy_for_platform 的 cfg 完全一致: