Tauri (20)——为什么 NSPanel 窗口不能用官方 API 全屏?

在基于 Electron 或 Tauri 开发 macOS 桌面应用时,我们经常会遇到一种特殊的窗口类型:NSPanel。它通常用于 spotlight 搜索栏、悬浮工具条等场景。然而,当我们想给这种"小窗口"加上全屏能力(比如玩游戏、看大图)时,往往会撞上一堵墙:官方的全屏 API 对 NSPanel 并不友好,甚至直接失效。

项目背景:Coco AI

我们在构建 Coco AI ------ 这款集成了统一搜索、协作与 AI 助手的跨平台桌面生产力工具时,遇到了一个有趣的技术挑战。

Coco AI 强大的插件系统Extensions)允许用户在应用内直接运行各种工具(如小游戏、可视化图表、Web 应用等)。为了保持轻量和随手即用的体验,Coco 默认使用类似于 Spotlight 的悬浮窗(NSPanel)展示。但当用户想要沉浸式地使用插件(比如玩个小游戏)时,默认的小窗口就显得局促了。

我们希望实现的效果是:平时召之即来挥之即去,需要时一键变身全屏工作台。

然而,官方的窗口 API 在 NSPanel 上却频频"翻车"。今天就来复盘一下我们是如何在 Coco App 中解决这个问题的。

应用场景:一个嵌入式小游戏窗口

假设我们在开发一个类似于 Spotlight 的启动器,平时它是一个悬浮在屏幕中央的小框。但我们允许用户通过插件系统加载一个网页(比如 HTML5 游戏)。

需求很直接:

  1. 默认窗口大小固定(如 1200x900)。
  2. 用户点击"全屏"按钮,窗口瞬间铺满当前屏幕。
  3. 再次点击,恢复原状。

技术栈:

  • Tauri (Rust + WebView)
  • Frontend: React + TypeScript
  • Window Type : NSPanel (macOS)

遇到的坑:NSPanel 与 setFullscreen 的爱恨情仇

在普通的 NSWindow 中,调用 Tauri 的 window.setFullscreen(true) 或 Electron 的 setFullScreen(true),系统会自动创建一个新的 Space,把窗口平铺进去,非常优雅。

但 Coco AI 为了追求"极致的快速启动与无感交互",使用了 NSPanel 并设置了较高的窗口层级。当我们试图对它调用标准全屏 API 时:

  1. 系统动画冲突:由于没有标准标题栏,系统全屏动画可能会导致窗口消失、闪烁甚至错位。
  2. 多屏支持噩梦:用户在副屏唤起 Coco AI,点击全屏,结果窗口直接飞回了主屏。
  3. 状态不可逆:退出全屏后,窗口焦点和层级可能回不到原来的状态,打断了用户的心流。

简单来说,官方 API 是给 "标准应用窗口" 设计的,并不适配我们这种 "灵动挂件"

解决方案:手动接管窗口布局

既然系统 API 不懂我们的心,我们就自己动手,"伪造"一个全屏效果。通过 "手动计算 + 逻辑坐标转换" 来优雅解决这个问题。

核心原理

  1. 精准定位:获取当前鼠标所在的显示器(Monitor),确保"在哪里唤起,就在哪里全屏"。
  2. 坐标系转换 :macOS 使用逻辑像素(Logical Pixel),而底层屏幕信息往往是物理像素(Physical Pixel),必须通过 scaleFactor 进行转换,否则窗口会巨大无比或只有四分之一大。
  3. 暴力美学 :直接修改窗口的 x, y, width, height,使其完美覆盖目标屏幕的 Bounds。
  4. 状态快照:在变身前,记住原来的位置和大小,以便随时缩回那个熟悉的"小框框"。

Coco AI 的实现代码

以下是我们在 ViewExtension.tsx 中的核心实现逻辑。

typescript 复制代码
const applyFullscreen = useCallback(
  async (next: boolean) => {
    if (next) {
      // 1. 状态快照:保存当前位置、大小、是否可调整
      const size = await platformAdapter.getWindowSize();
      const resizable = await platformAdapter.isWindowResizable();
      const pos = await platformAdapter.getWindowPosition();
      
      fullscreenPrevRef.current = {
        width: size.width,
        height: size.height,
        resizable,
        x: pos.x,
        y: pos.y,
      };

      // 2. 针对 macOS + Tauri (NSPanel) 的特殊处理
      if (isMac && isTauri) {
        // 关键步:获取鼠标所在的屏幕,实现"原位全屏"
        const monitor = await platformAdapter.getMonitorFromCursor();

        if (!monitor) return;
        const window = await platformAdapter.getCurrentWebviewWindow();
        const factor = await window.scaleFactor();

        // 3. 坐标转换:物理像素 -> 逻辑像素
        const { size, position } = monitor;
        const { width, height } = size.toLogical(factor);
        const { x, y } = position.toLogical(factor);

        // 4. 手动铺满屏幕
        await platformAdapter.setWindowSize(width, height);
        await platformAdapter.setWindowPosition(x, y);
        await platformAdapter.setWindowResizable(true); // 全屏模式下通常允许调整
        await recomputeScale(); // 调整内部 Web 内容的缩放比例
      } else {
        // 其他平台使用标准 API 即可
        await platformAdapter.setWindowFullscreen(true);
        await recomputeScale();
      }
    } else {
      // 5. 退出全屏:恢复如初
      if (!isMac) {
        await platformAdapter.setWindowFullscreen(false);
      }
      // 从配置或默认值恢复大小
      const nextWidth = ui?.width ?? DEFAULT_VIEW_WIDTH;
      const nextHeight = ui?.height ?? DEFAULT_VIEW_HEIGHT;
      
      await platformAdapter.setWindowSize(nextWidth, nextHeight);
      await platformAdapter.setWindowResizable(ui?.resizable ?? true);
      
      // 关键步:居中回原来的屏幕
      await platformAdapter.centerOnCurrentMonitor();
      await recomputeScale();
      
      // 6. 焦点修复(防止操作中断)
      setTimeout(() => {
        iframeRef.current?.focus();
      }, 0);
    }
  },
  [ui, recomputeScale]
);

为什么这样做体验更好?

  • 瞬时响应:没有了系统全屏动画的拖泥带水,点击即全屏。
  • 多屏友好:完美支持多显示器环境,不再发生"窗口瞬移"的灵异事件。
  • UI 自由度:保留了我们自定义的 UI 控件,不受系统标题栏的干扰。

小结

在开发 Coco AI 的过程中,我们始终坚持 "不因为技术限制而妥协用户体验" 。虽然手动管理窗口状态比调用一个 API 麻烦得多,但为了让用户在使用插件时能有丝滑的体验,这一切都是值得的。

如果你对我们的技术栈(Rust + Tauri + React)感兴趣,或者想体验一下这个"既能小巧悬浮,又能全屏沉浸"的生产力工具,欢迎访问我们的 GitHub 仓库和官网:

相关推荐
崔庆才丨静觅16 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606117 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了17 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅17 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅18 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
未来侦察班18 小时前
一晃13年过去了,苹果的Airdrop依然很坚挺。
macos·ios·苹果vision pro
崔庆才丨静觅18 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment18 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅18 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊18 小时前
jwt介绍
前端