Tauri(21)——窗口缩放后的”失焦惊魂”,游戏控制权丢失了

背景

在上一篇文章中,我们分享了如何在 Coco AI 中实现丝滑的 NSPanel 窗口全屏体验。然而,全屏只是第一步,真正的挑战往往隐藏在细节之中。

Coco AI 的核心亮点之一是其日渐强大的插件生态Extensions)。用户可以通过安装插件,在 Coco 的悬浮窗中直接运行各种 Web 应用,甚至玩 Doom 这样的小游戏。

但我们在开发过程中遇到了一个非常影响心情的 Bug:

当用户正沉浸在游戏中,觉得窗口太小而点击"全屏"后,突然发现键盘失灵了------WASD 怎么按都没反应,手动点画面也不能继续操作...

对于一款追求极致体验的生产力工具来说,这种"断触"是不可接受的。今天我们就来深度复盘这个**焦点丢失(Focus Loss)**问题,以及我们在 Coco App 中的"组合拳"解决方案。

场景复现

  1. 用户在 Coco AI 中启动了一个游戏插件(通过 iframe 加载)。
  2. 初始窗口较小,用户通过 WASD 控制角色移动,一切正常。
  3. 用户点击右上角的"全屏"按钮,希望获得沉浸体验。
  4. 窗口瞬间变大铺满屏幕,但此时按下 W 键,角色纹丝不动。
  5. 用户拿起鼠标不断的点击一下游戏画面,控制权也未能恢复。

为什么会失焦?

这个问题的根源在于 DOM 树的重建和窗口系统的焦点管理机制,特别是在 React + Tauri 的混合架构下:

  1. DOM 重绘/重排 :当窗口从悬浮模式切换到全屏模式时,React 组件可能会因为状态变化(如 scale 缩放系数改变、layout 模式切换)而重新渲染。如果 iframe 在这个过程中被卸载并重新挂载,它就是一个全新的 iframe,之前的焦点自然荡然无存。
  2. Native 窗口焦点转移 :调用 Tauri 的 setWindowSizesetFullscreen 等底层 API 时,操作系统可能会暂时把焦点从 WebView 内容区域移开,转移到窗口边框或系统层级。
  3. Iframe 的隔离性iframe 内部是一个独立的 window 上下文。主文档(Parent)获得焦点并不意味着 iframe 获得焦点。你需要显式地把焦点"传递"进去。

Coco AI 的解决方案:全方位焦点守护

为了确保用户体验的连贯性,我们在 ViewExtension.tsx 组件中实施了一套多层级的焦点管理策略。

第一招:组件挂载后的自动聚焦

在 React 中,利用 refonLoad 事件,确保插件加载完毕的那一刻,焦点就自动锁定在它身上。

typescript 复制代码
<div
  // 绑定 Ref
  ref={iframeRef}
  // 任何点击外层容器的操作,都把焦点送给 iframe
  onClickCapture={() => {
    iframeRef.current?.focus();
  }}
>
  <iframe
    ref={iframeRef}
    src={fileUrl}
    // Iframe 加载完毕瞬间聚焦
    onLoad={(event) => {
      event.currentTarget.focus();
      try {
        // 尝试深入聚焦到 iframe 内部的 window
        iframeRef.current?.contentWindow?.focus();
      } catch (e) {
        console.warn("Failed to focus iframe content:", e);
      }
    }}
    // 允许必要的权限:全屏、鼠标锁定(FPS游戏必备)、手柄
    allow="fullscreen; pointer-lock; gamepad"
    tabIndex={-1}
  />
</div>

第二招:状态变化后的延迟聚焦

当你执行全屏或缩放操作后,Native 层的窗口调整是异步的,React 的渲染也是异步的。如果你立即调用 focus(),可能 DOM 还没稳,或者窗口还在动画中,导致聚焦失败。

我们的秘诀是 setTimeout,等待一轮事件循环:

typescript 复制代码
const applyFullscreen = useCallback(async (next: boolean) => {
  // ... 执行窗口大小调整逻辑 ...
  
  // 等待系统窗口调整完成且 React 渲染完毕
  setTimeout(() => {
    // 1. 聚焦 iframe 元素本身
    iframeRef.current?.focus();
    try {
      // 2. 尝试聚焦 iframe 内部内容(处理跨域限制时可能报错,加 try-catch)
      iframeRef.current?.contentWindow?.focus();
    } catch {}
  }, 0);
}, []);

第三招:显式的"焦点救生圈"

为了应对浏览器安全策略限制脚本自动聚焦等极端情况,我们在 UI 上设计了一个显式的 Focus 按钮。这不仅是一个功能补救,也是一个视觉提示。

typescript 复制代码
{/* Focus helper button */}
<button
  aria-label="Focus Game"
  className="absolute top-2 right-12 z-10 p-2 bg-black/50 hover:bg-black/70 rounded text-white transition-colors"
  onClick={() => {
    iframeRef.current?.focus();
    try {
      iframeRef.current?.contentWindow?.focus();
    } catch {}
  }}
>
  <FocusIcon className="size-4"/>
</button>

当用户发现控制失灵时,潜意识会寻找界面上的交互点,点击这个按钮就能瞬间找回焦点。

第四招:事件捕获(Capture Phase)

有时候用户点击了窗口边缘的空白区域(padding 或 margin),焦点会跑回主文档 body。我们可以通过在容器上监听 onMouseDownCapture 来拦截这些点击,强行把焦点按回 iframe

typescript 复制代码
<div
  className="w-full h-full flex flex-col items-center justify-center"
  // 捕获阶段拦截,比冒泡更早
  onMouseDownCapture={() => {
    iframeRef.current?.focus();
  }}
  onPointerDown={() => {
    iframeRef.current?.focus();
  }}
>
  <iframe ... />
</div>

小结

焦点管理看似简单,但在构建像 Coco AI 这样复杂的桌面+Web 混合应用时,它直接关系到用户的沉浸感。通过这套 "主动出击 + 纵深防御 + 异步等待 + 兜底方案" 的组合拳,我们成功解决了跨平台、跨窗口尺寸下的焦点丢失问题。

现在,无论是在写代码时快速查阅文档,还是在休息时玩一把小游戏,Coco AI 都能提供无缝、流畅的交互体验。

如果你对我们的技术细节感兴趣,或者想体验一下这款全能的生产力工具,欢迎访问我们的开源仓库和官网:

相关推荐
Bigger2 小时前
Tauri (20)——为什么 NSPanel 窗口不能用官方 API 全屏?
前端·macos·app
bug总结2 小时前
前端开发中为什么要使用 URL().origin 提取接口根地址
开发语言·前端·javascript·vue.js·html
一招定胜负3 小时前
网络爬虫(第三部)
前端·javascript·爬虫
Shaneyxs3 小时前
从 0 到 1 实现CloudBase云开发 + 低代码全栈开发活动管理小程序(13)
前端
半山烟雨半山青3 小时前
微信内容emoji表情包编辑器 + vue3 + ts + WrchatEmogi Editor
前端·javascript·vue.js
码途潇潇4 小时前
Vue 事件机制全面解析:原生事件、自定义事件与 DOM 冒泡完全讲透
前端·javascript
zmirror4 小时前
Monorepo 在 Docker 中的构建方案方案
前端
用户47949283569154 小时前
node_modules 太胖?用 Node.js 原生功能给依赖做一次大扫除
前端·后端·node.js
_Kayo_4 小时前
TypeScript 学习笔记2
前端·javascript·typescript