mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
64c068415e
Linux users reported the window UI (including native title bar buttons) couldn't receive clicks until manually maximizing and restoring the window. Root causes: (1) Tauri webview did not acquire focus on startup so first clicks were consumed by X11/Wayland click-to-activate (Tauri #10746, wry #637); (2) GTK surface input region failed to renegotiate on the visible:false + show() path under some WebKitGTK/compositor combinations. - Add linux_fix::nudge_main_window helper that performs set_focus plus a ±1px no-op resize after window show, with a 500ms reconciliation readback to compensate for dropped resize requests on slow compositors. - Wire the helper into every window re-show path: normal startup, deeplink, single_instance, tray show_main, and lightweight exit. - Set WEBKIT_DISABLE_COMPOSITING_MODE=1 at startup to avoid resize crashes and Wayland surface negotiation issues. - Remove data-tauri-drag-region on Linux from App.tsx header and the shared FullScreenPanel (used by all provider/MCP/workspace forms) to avoid Tauri #13440 in Wayland sessions. Extract drag-region constants to src/lib/platform.ts for reuse. All Rust changes are gated by #[cfg(target_os = "linux")]; frontend changes preserve macOS/Windows behavior via runtime isLinux() checks. Known limitation: tiling Wayland compositors ignore set_size, so GDK_BACKEND=x11 remains the user-side workaround.
122 lines
6.6 KiB
Rust
122 lines
6.6 KiB
Rust
//! Linux 专用的主窗口恢复补丁。
|
||
//!
|
||
//! 解决 Tauri 2.x 在部分 Linux 发行版(尤其是 Wayland / 某些 WebKitGTK
|
||
//! 版本)上启动后 UI 无法响应点击的问题:
|
||
//!
|
||
//! - **失效模式 A**(Tauri #10746 / wry #637):webview 在 `show()` 后
|
||
//! 没有获得 keyboard focus,导致首次点击被 X11/Wayland 用作
|
||
//! click-to-activate 而非传给 webview。
|
||
//! - **失效模式 B**:GTK surface 与 WebKitWebView 的 input region 尺寸
|
||
//! 协商在 `visible:false` → `show()` 的路径上失败,整窗永远不响应
|
||
//! 点击,只有重新 `size_allocate`(例如最大化-还原)才能恢复。
|
||
//!
|
||
//! 本模块导出 [`nudge_main_window`],它通过「显式 set_focus + 无视觉
|
||
//! 版本的 ±1px 伪 resize」精确模拟用户手动最大化再还原的 workaround,
|
||
//! 但肉眼无法察觉。所有"让主窗口出现在用户面前"的路径(正常启动、
|
||
//! deeplink 唤起、single_instance 回调、托盘 show_main、lightweight
|
||
//! 退出)都应在现有 `set_focus()` 之后追加一次调用。
|
||
|
||
use std::time::Duration;
|
||
|
||
use tauri::{PhysicalSize, WebviewWindow};
|
||
|
||
/// 在 webview realize 之后的延迟,等 GTK 主循环把 realize 事件处理完。
|
||
/// 200ms 是社区经验值;太短 set_focus 仍会无效,太长会让首屏可交互
|
||
/// 时间被肉眼感知到。
|
||
const REALIZE_WAIT: Duration = Duration::from_millis(200);
|
||
|
||
/// ±1px 伪 resize 两步之间的间隔,确保 GTK 先处理了第一次
|
||
/// `size_allocate` 再收到第二次 resize。放宽到 100ms 是因为 Tao 在 Linux
|
||
/// 上的尺寸 API 是异步的(底层走 `gtk_window_resize` → 合成器 configure),
|
||
/// 太短会让合成器把两次连续 resize coalesce 成一次。
|
||
const RESIZE_GAP: Duration = Duration::from_millis(100);
|
||
|
||
/// 尺寸对账回读前的额外等待。200ms + 100ms + 500ms = 总共 ~800ms 后
|
||
/// 校验窗口尺寸是否回到 original。这个时间足够所有合成器处理完
|
||
/// resize 消息队列。
|
||
const RECONCILE_WAIT: Duration = Duration::from_millis(500);
|
||
|
||
/// 对主窗口执行 Linux 专用的「focus + surface 重激活」序列。
|
||
///
|
||
/// 调用是 fire-and-forget:内部 spawn 一个异步任务在 ~250ms 后完成。
|
||
/// 调用线程立即返回,不阻塞 UI。
|
||
pub(crate) fn nudge_main_window(window: WebviewWindow) {
|
||
// 第一次 set_focus:webview 可能还没 realize,这一次通常是无效的,
|
||
// 但成本极低(线程安全,内部 run_on_main_thread),顺手做掉。
|
||
let _ = window.set_focus();
|
||
|
||
tauri::async_runtime::spawn(async move {
|
||
tokio::time::sleep(REALIZE_WAIT).await;
|
||
|
||
// 第二次 set_focus:此时 webview realize 已完成,在绝大多数
|
||
// 发行版上这一次会真的生效,消除失效模式 A。
|
||
let _ = window.set_focus();
|
||
|
||
// 伪 resize:读取当前 inner_size,先加 1px 再还原。这会触发
|
||
// GTK 的 size-allocate → WebKitWebViewBase::size_allocate →
|
||
// 重新 attach input surface,消除失效模式 B。
|
||
//
|
||
// 使用 PhysicalSize 避免跨 DPI 的逻辑坐标漂移;saturating_add
|
||
// 防止极端尺寸溢出。
|
||
match window.inner_size() {
|
||
Ok(original) => {
|
||
let bumped = PhysicalSize::new(original.width.saturating_add(1), original.height);
|
||
let _ = window.set_size(bumped);
|
||
tokio::time::sleep(RESIZE_GAP).await;
|
||
let _ = window.set_size(original);
|
||
log::info!("Linux: 已对主窗口执行 focus + surface 重激活");
|
||
|
||
// 尺寸对账回读:Tao Linux 的尺寸 API 是异步的,`set_size` 只是把
|
||
// resize 请求送进 GTK 主循环队列,合成器可能会 coalesce 两次连续
|
||
// 请求(尤其是第二次 `set_size(original)`),导致窗口永久停留在
|
||
// width+1。这里等合成器处理完队列后读一次实际尺寸,发现 drift 就
|
||
// 再补一次 `set_size(original)` 兜底。
|
||
//
|
||
// 已知限制:tiling Wayland 合成器(sway/river/hyprland)会完全忽略
|
||
// `set_size`,此时对账永远 drift=0(因为两次 set_size 都是 no-op),
|
||
// 看起来"没问题"但失效模式 B 其实没被修复;这是已知限制,需要用户
|
||
// 侧用 GDK_BACKEND=x11 绕过,README 应该有说明。
|
||
tokio::time::sleep(RECONCILE_WAIT).await;
|
||
match window.inner_size() {
|
||
Ok(after) => {
|
||
if after.width != original.width || after.height != original.height {
|
||
log::info!(
|
||
"Linux nudge 尺寸 drift: expected={}x{}, got={}x{},已补偿",
|
||
original.width,
|
||
original.height,
|
||
after.width,
|
||
after.height
|
||
);
|
||
let _ = window.set_size(original);
|
||
// 最终校验:如果补偿后仍然不一致,记 warn 让用户/开发者
|
||
// 知道对账失败。这时窗口会停在非预期尺寸(通常是 +1px),
|
||
// 属于极端兜底场景。
|
||
if let Ok(final_size) = window.inner_size() {
|
||
if final_size.width != original.width
|
||
|| final_size.height != original.height
|
||
{
|
||
log::warn!(
|
||
"Linux nudge 尺寸 drift 补偿后仍不一致: expected={}x{}, got={}x{}",
|
||
original.width,
|
||
original.height,
|
||
final_size.width,
|
||
final_size.height
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Err(e) => {
|
||
log::warn!("Linux nudge: 对账回读 inner_size 失败: {e}");
|
||
}
|
||
}
|
||
}
|
||
Err(e) => {
|
||
// 极罕见的失败路径;只做了 set_focus 也比什么都不做强,
|
||
// 不要让 resize 失败把整个补丁吞掉。
|
||
log::warn!("Linux nudge: 读取 inner_size 失败,跳过伪 resize: {e}");
|
||
}
|
||
}
|
||
});
|
||
}
|