☰
MooTool Next Tauri 原生桌面体验设计解析:截图、取色、剪贴板、拖放与防休眠的平台差异决策
2026/10/16 6:23:39 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

导读

本文以 MooTool 仓库中已采纳的 ADR-010:原生桌面体验与平台差异 为骨架,深入解析 MooTool Next Tauri 产品线在 macOS、Windows 与 Linux 上如何统一实现"截图后选区、可视化取色、图片剪贴板、拖放导入和防显示器休眠"这一套桌面任务闭环。全文结合 Rust 命令层、前端 API 适配层、Tauri 能力清单 与各工具页面实现,说明每个决策背后的权限边界、参数约束与批准的平台差异。读完本文,你将掌握该产品线原生桌面能力的调用链、校验规则与跨平台降级策略。

背景:并行产品线中的"同一套桌面任务闭环"

MooTool Next Tauri 与 Java、Next Electron 是并行产品线,并非以迁移 Electron 源码为目标。ADR-010 明确了一个关键前提:截图、取色、剪贴板、防休眠、拖放和窗口外观,应在三个桌面平台上尽量保持相同的用户任务闭环。但由于 Tauri 使用系统 WebView,各平台对桌面捕获、透明窗口、全局指针和 Wayland Portal 的支持能力各不相同,因此需要以 ADR 形式固定首版行为,并把"批准差异"明确记录下来,而不是逐平台即兴实现。

从仓库结构看,这条边界在工程上被明确落实为两层:

  • 原生依赖与平台差异全部封装在 Rust/Platform API 层,工具页面不直接读取任意文件路径,也不调用外部 shell;
  • 前端仅通过 nativeDesktopApi.ts 暴露的四个命令与 Rust 侧通信:captureDisplays、sampleScreenColor、getDisplaySleepStatus、setDisplaySleepPrevention。

决策一:截图 = 隐藏窗口 + xcap 全显示器捕获

ADR-010 的第一项决策是:截图由 Rust 先隐藏当前可见的 MooTool 窗口,再捕获所有显示器;前端显示捕获快照,允许切换显示器、按物理像素框选区域、保留整屏或取消;保存区域后删除临时整屏资产。首版不创建覆盖全部显示器的透明 Overlay,也不支持一次选区跨越两个显示器。

命令调用链

前端入口在 ImageSurface.tsx 的captureScreen(),默认调用nativeDesktopApi.captureDisplays()(默认delayMs = 350):

captureScreen → capture_display_images → hide_visible_windows → sleep(delayMs) → capture_all_monitors(xcap) → restore_windows → persist PNG 资产

Rust 侧 native_desktop.rs 的实现顺序是:

  1. 通过validate_delay校验延迟参数必须在 100–2000 ms 之间,越界直接返回错误;
  2. 借助NativeDesktopManager.capture这把AsyncMutex做单飞保护,避免截图与取色并发竞争;
  3. hide_visible_windows遍历app.webview_windows(),只隐藏当前可见的窗口,并记录每个被隐藏窗口的 label 与 focus 状态;
  4. 等待delay_ms让窗口真正消失后,在spawn_blocking线程中执行capture_all_monitors;
  5. 无论成功失败,都会执行restore_windows恢复窗口并重新聚焦,保证截图不会"丢失"应用窗口。

显示器捕获的物理像素语义

capture_all_monitors(native_desktop.rs)使用xcap::Monitor::all()枚举显示器,并按(y, x)排序,随后对每个显示器执行monitor.capture_image(),把原始 RGBA8 帧通过PngEncoder编码为 PNG。这里有两个值得注意的边界常量:

常量值作用
MAX_MONITORS16显示器数量超出 1–16 范围时直接拒绝
MAX_CAPTURED_IMAGE_BYTES20 MiB任一显示器 PNG 超过 20 MiB 即中止,与图片库单图上限一致

捕获结果按Screenshot-{timestamp}-{display_id}-{index}.png命名,通过persist_image_bytes写入图片库(images.rs),期间任何一个显示器写入失败都会回滚已写入的全部资产。前端随后用imageApi.read逐个读取快照资产,交给CaptureEditor完成选区。

前端选区交互

在 ImageSurface.tsx 的CaptureEditor中:

  • 多显示器时顶部出现显示器切换按钮(capture.monitor),并在按钮上直接显示物理分辨率;
  • 用户在快照上拖拽出选区,选区坐标按clientX → asset.width的物理像素比例换算(Math.round((event.clientX - rect.left) * asset.width / rect.width)),因此选区是按物理像素而非 CSS 像素框选;
  • 选区宽度或高度小于 2 px 时"保存选区"按钮被禁用;
  • 提供"保留整屏"(keepFullCapture)与"取消"(cancelCapture)两个出口,取消时会删除本次生成的全部临时整屏资产。

saveCaptureRegion通过cropDataUrl从 dataURL 裁出选区并另存为新的图片资产,随后imageApi.delete删除所有整屏快照——这与 ADR"保存区域后删除临时整屏资产"的决策完全一致。

决策二:屏幕取色复用快照,不依赖 WebView EyeDropper

ADR-010 的第二项决策是:屏幕取色复用隐藏窗口后的显示器快照,提供实时色块、HEX/RGB 与像素坐标,单击确认、Esc 取消。这样既不需要 WebView 的 EyeDropper 权限,也不要求用户在固定倒计时内盲移指针。

双路径设计

ColorSurface.tsx 的sampleScreen()是典型的分支实现:

  • 在 Tauri 运行时(window.__TAURI_INTERNALS__存在)调用nativeDesktopApi.captureDisplays(),读取全部显示器快照后进入ScreenColorEditor;
  • 在纯浏览器预览环境回退到EyeDropperAPI,取色结果直接写入颜色输入框。

ScreenColorEditor用隐藏 Canvas 承载快照,鼠标移动时通过getImageData(x, y, 1, 1)实时读取像素,换算为#RRGGBB大写 HEX、RGB 分量和物理像素坐标,悬浮色块跟随指针展示;单击确认(confirmScreenColor)把该色写入主面板,Esc 键取消(onCancel),两种出口都会imageApi.delete删除临时快照资产。

与 Rust 全局指针采样的关系

值得注意的是,ADR 中描述的"取色复用快照"是首版交付路径;Rust 侧同时保留了sample_screen_color命令(native_desktop.rs),它通过mouse_positioncrate 读取全局指针位置,用xcap::Monitor::from_point定位所在显示器,再capture_region(x, y, 1, 1)采 1×1 像素并格式化为ScreenColorSample。该命令的delayMs校验范围为 500–5000 ms,color_sample的 HEX 格式化与validate_delay均有单测覆盖(native_desktop.rs)。

决策三:平台捕获差异封装与可理解的失败语义

ADR-010 第三项决策要求:macOS、Windows、Linux X11 的捕获通过 Tauri-owned Rust 适配器调用xcap;失败必须显示可理解的权限/桌面会话错误。Linux Wayland 在无法取得显示器或全局指针时明确禁用本次操作,不返回猜测颜色。Portal Overlay 作为后续平台增强,不阻塞产品线。

Rust 侧用统一的错误包装兑现了"可理解"要求。screen_capture_error(native_desktop.rs)把任何xcap底层错误改写为:

native screen capture failed: {error}; verify screen-recording permission and the current desktop session

而sample_color_at_pointer在Mouse::get_mouse_position()返回Mouse::Error时,直接返回 "global pointer position is unavailable; Wayland may require an X11 session",这正是 ADR 所说"Wayland 明确禁用、不猜测"的实现。前端 nativeDesktopApi.ts 还为非 Tauri 环境提供了unavailable抛错与空状态降级,浏览器预览不会因为缺少原生能力而崩溃。

测试层面,native_screen_capture_smoke(native_desktop.rs)被标注为#[ignore],注释明确要求"interactive desktop session and screen-recording permission"——它验证所有捕获结果的 PNG 魔数\x89PNG\r\n\x1a\n与取色 HEX 长度为 7,是需要真机人工跑的门禁。

决策四:剪贴板与拖放的最小权限模型

剪贴板统一走官方插件

ADR 规定图片与文本剪贴板统一使用 Tauri 官方clipboard-manager插件(Cargo.toml中tauri-plugin-clipboard-manager = "2"),但权限按"最小授权"原则拆分到能力清单:

  • tool-clipboard-image.json:读图/写图权限只开放给tool-image与tool-qrcode两个工具 WebView,并附带core:image:allow-new/rgba/size以便把剪贴板位图转换为TauriImage再写回;
  • tool-clipboard-text.json:只授予clipboard-manager:allow-write-text(不授予读文本权限),且仅覆盖提供显式复制动作的 17 个工具 WebView(颜色、编码、JSON、正则、时间戳、翻译、UA、变量等);
  • default.json:主工作台仅保留core:default,不沾染剪贴板权限。

前端封装在 clipboardApi.ts,Tauri 运行时走readNativeImage/writeNativeImage/writeNativeText,浏览器预览回退到navigator.clipboard。在 ImageSurface.tsx 的copyImage()中可以看到实际写入路径:dataURL → Canvas →ImageData→TauriImage.new(rgba, width, height)→writeClipboardImage,用完即nativeImage.close()。

拖放:原生路径事件而非 DOM File

图片拖放使用 Tauri WebView 原生路径事件。getCurrentWebview().onDragDropEvent(ImageSurface.tsx)只在 Tauri 运行时注册,drop事件的paths直接交给imageApi.importPaths;非 Tauri 环境则退化为 DOMonDrop+dataTransfer.files。

Rust 侧import_image_files(images.rs)把 ADR 的校验逐条落地:

校验项规则违规处理
数量1–50 个路径越界直接拒绝
路径必须是绝对路径回滚已导入后报错
文件类型必须是非符号链接的常规文件(symlink_metadata+!is_file判否)回滚
大小1 字节至 20 MiB(MAX_IMAGE_BYTES)回滚
真实性inspect_image_bytes必须真实解码出 MIME、宽高回滚
回滚rollback_imported_images清理本批已导入资产—

persist_image_bytes还会二次校验签名与宽高元数据一致性,防止"文件名看着是 PNG 实际不是"的伪装文件入库。

导出:单张固定扩展名,多张避让重名

export_image_assets(images.rs)体现了两条 ADR 细节:

  • 单张导出走原生保存对话框(app.dialog().file()),add_filter只放行该图片真实的扩展名,并通过enforce_image_extension把用户输入的目标路径强制修正为真实图片扩展名(如 JPEG →.jpg),再校验目标不能是符号链接;
  • 多张导出走目录选择,unique_export_path自动避让目标目录里已有的同名文件;某一张失败时只回滚本批已创建的文件(for path in &exported { fs::remove_file }),不动历史文件。

决策五:防显示器休眠 = 按 owner 隔离的 token

ADR 第五项决策要求 Rust 维护按 owner 隔离的防休眠 token:系统工具和留言板演示模式各自持有 owner,一个页面退出不会关闭另一个页面仍需要的防休眠状态。

状态模型

DisplaySleepState(native_desktop.rs)的核心是一个Option<KeepAwake>token 加一个HashSet<String>owners:

struct DisplaySleepState { token: Option<KeepAwake>, owners: HashSet<String>, }

set_display_sleep_prevention(native_desktop.rs)的逻辑是:owner 首次启用时加入集合;任一 owner 禁用时从集合移除;集合从空变非空时创建KeepAwaketoken,从非空变空时释放 token。因此:

  • 并发持有:system-tool与message-board-presentation同时启用时只存在一个底层 token;
  • 退出互不影响:留言板退出演示模式只移除自己的 owner,只要系统工具仍持有,token 不会被释放。

keepawake::Builder的参数在代码中完全可见:display(true)+idle(true),reason("MooTool display mode"),app_name("MooTool Next Tauri"),app_reverse_domain("com.rememberber.mootool.next.tauri")(与 tauri.conf.json 的 identifier 一致)。owner 字符串本身受validate_sleep_owner约束:长度 1–64,仅允许小写字母、数字与连字符,单测里"message-board-presentation"通过而"Other Product"被拒(native_desktop.rs)。

两个真实 owner 的调用方

  • SystemSurface.tsx:挂载时getDisplaySleepStatus('system-tool')恢复开关状态,切换开关时setDisplaySleepPrevention('system-tool', !sleepPrevention);
  • MessageBoardSurface.tsx:进入演示模式时启用'message-board-presentation',退出演示模式的 effect 清理函数里对称地关闭它。

两个页面通过不同的 owner 字符串实现了 ADR 要求的隔离语义。

决策六:macOS 窗口外观——隐藏标题 + 原生 Overlay,不做透明 WebView

ADR 规定 macOS 首版使用隐藏标题和原生 Overlay 标题栏,保留系统红黄绿窗口按钮;不启用macOSPrivateApi,也不做完全透明 WebView。与 Electron 毛玻璃强度的差异属于批准差异,优先保证直接分发、输入法和工具 WebView 稳定性。

窗口配置直接落在 tauri.conf.json 的main窗口定义上:

{ "label": "main", "title": "MooTool Next Tauri", "width": 1440, "height": 900, "minWidth": 760, "minHeight": 620, "center": true, "hiddenTitle": true, "titleBarStyle": "Overlay" }

hiddenTitle与titleBarStyle: "Overlay"即"隐藏标题 + 原生 Overlay 标题栏"的声明式表达,窗口按钮交给系统绘制,同时 CSP 限制了object-src与frame-src均为none,为工具 WebView 提供严格的内容边界。窗口恢复、关闭到托盘、托盘菜单与全局快捷键的生命周期由 desktop.rs 的setup/handle_window_event/handle_menu_event维护,与原生标题栏决策协同构成 macOS 首发体验。

决策七:Windows 逻辑坐标与 per-monitor DPI

在 Windows 上,ADR 要求使用 WebView2/Tauri 的逻辑坐标和 per-monitor DPI 处理;图片文件拖放一律走原生物理路径,不从 DOMFile推断本地路径——这正是为什么 ImageSurface.tsx 在 Tauri 运行时关闭 DOM 的onDragOver/onDrop,只信任onDragDropEvent的paths。真实多缩放显示器(混合 DPI)场景仍属于发布矩阵的人工验收项,即自动化门禁之外需要真机确认的项目。

决策八:更新、签名与发布通道保持 Tauri 产品线独立

ADR 强调自动更新、签名和发布通道继续使用 Tauri 产品线独立实现,不与 Electron updater、证书或 Release 标签共用。这在仓库的 ADR 系列中是闭环的:

  • ADR-008:自动更新清单 采用两层清单:根 update-manifest.json 只做产品发现,Tauri 客户端固定读取products.next-tauri并校验专属更新 URL;next-tauri-updaterRelease 中的latest.json才是 Tauri Updater 的稳定静态清单(含三平台下载 URL 与 updater 签名);
  • ADR-009:代码签名策略 规定所有可更新包必须使用 Tauri 产品线独占的 updater 私钥签名,Java/Electron 不共用密钥;macOS 0.x 开发预览允许 ad-hoc 签名、Windows 0.x 允许缺少 Authenticode,但必须在 Release Notes 中明确系统信任警告。

结果与验收:三平台任务闭环、批准差异与工程边界

三平台统一的任务闭环

根据 ADR 的"结果"章节,用户在三平台获得相同的任务闭环:

  1. 截图后选区:隐藏窗口 → 捕获全部显示器 → 物理像素框选 → 保存/保留整屏/取消,临时资产自动清理;
  2. 可视化取色:基于快照实时显示色块、HEX/RGB 与坐标,单击确认、Esc 取消;
  3. 图片剪贴板:官方clipboard-manager插件 + 能力清单按工具最小授权;
  4. 拖放导入:原生路径事件 + Rust 侧数量/绝对路径/符号链接/大小/真实解码五重校验与批量回滚;
  5. 防显示器休眠:按 owner 隔离的 token,系统工具与演示模式互不干扰。

明确记录的批准差异

以下差异不阻塞 1.0 发布,但必须在产品文档与错误提示中公开:

  • macOS 毛玻璃强度与 Electron 存在差异(优先保证直接分发、输入法与工具 WebView 稳定性);
  • 首版不支持一次选区跨越两个显示器,也不创建全屏透明 Overlay;
  • Linux Wayland 下显示器/全局指针不可用时直接禁用操作,Portal Overlay 留待后续平台增强。

工程边界的兑现

"原生依赖与平台差异封装在 Rust/Platform API,工具页面不直接读取任意文件路径或调用外部 shell"这条承诺可以从仓库结构直接验证:四个原生桌面命令全部集中在 native_desktop.rs,前端只经过 nativeDesktopApi.ts 与 nativeDesktop.test.ts 定义的类型化接口访问,工具 WebView 的能力由 tool-views.json 统一收口。

P0 记录的角色调整

ADR 最后特别说明:P0 记录继续保留历史实测事实,但不再承担 Go/No-Go 立项职责——即历史测量数据仍可作为参考证据,但项目的原生桌面能力是否立项、如何立项,以本文所述决策(以及 ADR-008/009 等相邻决策)为准。这一点与 ADR 作为"固定首版行为与批准差异"的定位一致:它不是一份推测性方案,而是一份已被仓库代码逐条落地的实施契约。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

相关推荐

上一篇:実行プランをハーネスの第一級市民にする:repo-template の PLANS.md 運用ガイド
下一篇:Proton Native 热重载(Hot Reloading)完整指南:原理、配置与实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询