- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
导读
本文以 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 的实现顺序是:
- 通过
validate_delay校验延迟参数必须在 100–2000 ms 之间,越界直接返回错误; - 借助
NativeDesktopManager.capture这把AsyncMutex做单飞保护,避免截图与取色并发竞争; hide_visible_windows遍历app.webview_windows(),只隐藏当前可见的窗口,并记录每个被隐藏窗口的 label 与 focus 状态;- 等待
delay_ms让窗口真正消失后,在spawn_blocking线程中执行capture_all_monitors; - 无论成功失败,都会执行
restore_windows恢复窗口并重新聚焦,保证截图不会"丢失"应用窗口。
显示器捕获的物理像素语义
capture_all_monitors(native_desktop.rs)使用xcap::Monitor::all()枚举显示器,并按(y, x)排序,随后对每个显示器执行monitor.capture_image(),把原始 RGBA8 帧通过PngEncoder编码为 PNG。这里有两个值得注意的边界常量:
| 常量 | 值 | 作用 |
|---|---|---|
MAX_MONITORS | 16 | 显示器数量超出 1–16 范围时直接拒绝 |
MAX_CAPTURED_IMAGE_BYTES | 20 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 的"结果"章节,用户在三平台获得相同的任务闭环:
- 截图后选区:隐藏窗口 → 捕获全部显示器 → 物理像素框选 → 保存/保留整屏/取消,临时资产自动清理;
- 可视化取色:基于快照实时显示色块、HEX/RGB 与坐标,单击确认、Esc 取消;
- 图片剪贴板:官方
clipboard-manager插件 + 能力清单按工具最小授权; - 拖放导入:原生路径事件 + Rust 侧数量/绝对路径/符号链接/大小/真实解码五重校验与批量回滚;
- 防显示器休眠:按 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
相关推荐
MooTool Next Flutter 桌面能力对齐:截图裁剪、屏幕取色与六种风格 Token 的实现与验收
MooTool Next Flutter 桌面能力对齐:截图裁剪、屏幕取色与六种风格 Token 的实现与验收 本篇指南聚焦 MooTool Next Flut
桌面应用开发工具MooTool Next Flutter P6 截图裁剪与取色桌面通道验收报告:83 项单元测试全通过的实现细节
MooTool Next Flutter P6 截图裁剪与取色桌面通道验收报告:83 项单元测试全通过的实现细节 导读 本文是 MooTool 开源仓库中 Mo
桌面应用开发工具MooTool Next Flutter 桌面通道实战:剪贴板图片、关闭策略、托盘与分离持久化的 macOS 落地
MooTool Next Flutter 桌面通道实战:剪贴板图片、关闭策略、托盘与分离持久化的 macOS 落地 本文面向希望在 Flutter 桌面应用(m
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考