# Rust + Tauri 重构开发文档 ## 1. 目标 用 Rust + Tauri 重构当前 Python + PySide6 版本,功能行为保持一致: - 启动后显示 80×80 置顶悬浮窗。 - `Ctrl+1` 进入窗口识别模式。 - 全屏半透明遮罩覆盖虚拟屏幕。 - 鼠标移动时识别底层应用窗口,蓝色边框高亮;窗口内部无遮罩。 - 鼠标在桌面或无合格窗口时,默认选择整个虚拟屏幕。 - 鼠标点击锁定当前窗口;锁定后鼠标移动不再切换目标。 - `Esc` 或回车退出当前选择/标注流程,不删除已保存标注。 - 锁定窗口后 `Ctrl+R` 进入矩形标注模式。 - 可创建多个标注框;标注框可选中、移动、缩放。 - 左侧显示 300px 标注信息面板。 - 标注框附近显示编辑表单,可编辑控件名、描述、类型、动作、特征图占位。 - 自动保存到用户目录 JSON 文件。 首版仍只支持 Windows 10/11。 ## 2. 当前 Python 版本功能清单 ### 2.1 应用启动 当前入口为 `python -m desktop_mark`。 启动时创建: - `WindowProvider`:Win32 窗口识别。 - `FloatingButton`:80×80 悬浮窗。 - `OverlayWindow`:窗口选择与标注 overlay。 - `AnnotationPanel`:左侧标注信息面板。 - `AnnotationFormPopup`:标注编辑表单。 - `HotkeyWindow`:全局快捷键桥接。 启动后只显示悬浮窗;overlay、信息面板、表单默认隐藏。 ### 2.2 DPI 策略 当前 Python 版启动时调用: - `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)` - Qt 高 DPI rounding policy:`PassThrough` Tauri 版应在 Rust 入口尽早设置 Win32 DPI awareness,避免 Win32 坐标和 WebView 坐标偏移。 ### 2.3 悬浮窗 当前行为: - 固定尺寸:80×80。 - 置顶、无边框、工具窗口。 - 深色半透明圆角背景。 - 主文本:`RPA`。 - 副文本:`Ctrl+1`。 - 点击等价于触发窗口选择。 - 支持鼠标拖动移动。 - 自身窗口句柄必须加入窗口识别排除列表,避免选中工具自身。 ### 2.4 全局快捷键 当前快捷键: - `Ctrl+1`:进入窗口选择模式。 - `Ctrl+R`:进入标注模式;如果没有锁定窗口,则先进入窗口选择模式。 当前实现使用 Win32 `RegisterHotKey`,不是键盘 hook。Tauri 版也应使用 Rust 原生层注册系统热键,避免管理员权限和杀软误报。 ### 2.5 窗口识别 当前算法: 1. 不直接使用 `WindowFromPoint`,因为全屏 overlay 会遮住底层窗口。 2. 从 `GetTopWindow(None)` 开始,按 Z 序循环 `GetWindow(hwnd, GW_HWNDNEXT)`。 3. 跳过: - 已排除窗口句柄。 - 不可见窗口。 - 最小化窗口。 - `WS_EX_TOOLWINDOW` 工具窗口。 - 宽高小于等于 0 的窗口。 4. 用 `GetWindowRect(hwnd)` 得到窗口矩形。 5. 返回第一个包含鼠标点的合格顶层窗口。 6. 标题来自 `GetWindowText(hwnd)`。 7. 进程名来自 `GetWindowThreadProcessId` + `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + 模块路径文件名。 8. 没有命中任何窗口时,返回虚拟屏幕桌面窗口: - `hwnd = null` - `title = "Desktop"` - `process_name = "explorer.exe"` - `rect = virtual_screen_rect` - `is_desktop = true` 虚拟屏幕使用: - `SM_XVIRTUALSCREEN` - `SM_YVIRTUALSCREEN` - `SM_CXVIRTUALSCREEN` - `SM_CYVIRTUALSCREEN` ### 2.6 Overlay 状态机 状态枚举: ```rust enum OverlayMode { Idle, PickingWindow, WindowLocked, DrawingAnnotation, EditingAnnotation, MovingAnnotation, ResizingAnnotation, } ``` 行为: - `Idle`:overlay 隐藏。 - `PickingWindow`:overlay 显示,每 50ms 读取鼠标位置并识别窗口。 - `WindowLocked`:点击窗口后进入;蓝框保持锁定,不再跟随鼠标。 - `DrawingAnnotation`:锁定窗口内拖拽新建矩形。 - `EditingAnnotation`:显示表单,选中标注。 - `MovingAnnotation`:拖动已有标注框。 - `ResizingAnnotation`:拖动 8 个 resize handles。 ### 2.7 Overlay 绘制规则 当前视觉规则: - 全屏半透明黑色遮罩,透明度约 120/255。 - 当前目标窗口区域清除遮罩。 - 目标窗口边缘绘制 2px 蓝色边框。 - 右上角显示窗口标题和 bbox。 - 已有标注框为橙色 2px 边框。 - 标注序号显示在框左上角。 - 选中标注显示 8 个白底蓝边 resize handles。 - 新建中的临时框为蓝色 2px 边框。 Tauri 版建议: - 用独立透明 always-on-top overlay window。 - 前端用 SVG 或 Canvas 绘制遮罩、挖空、边框、标注框。 - 遮罩挖空可用 SVG `mask` / `clipPath`,或 Canvas `globalCompositeOperation = "destination-out"`。 ### 2.8 标注交互 创建标注: 1. `Ctrl+R` 进入 `DrawingAnnotation`。 2. 鼠标左键按下记录起点。 3. 拖动时显示临时矩形。 4. 鼠标释放: - 对矩形做 `normalized()`。 - 裁剪到锁定窗口 `clamped_to(locked_window.rect)`。 - 小于 `6×6` 像素视为误触,不创建。 - 合格则添加到 `project.annotations`。 - 进入 `EditingAnnotation`。 - 在标注框右下附近显示编辑表单。 命中测试顺序: 1. resize handle。 2. 标注框内部移动。 3. 空白区域新建。 多个标注重叠时,最后创建的标注优先命中。 移动规则: - 移动时保持标注框宽高不变。 - 移动结果必须完整留在锁定窗口内。 - 不允许因简单裁剪导致宽高变小。 缩放规则: - 8 个 handles:`nw`、`n`、`ne`、`e`、`se`、`s`、`sw`、`w`。 - 最小尺寸:`6×6`。 - 缩放结果裁剪到锁定窗口内。 取消规则: - `Esc` 或回车:退出当前 overlay 流程。 - 清空当前 hover/locked/temp 状态。 - 隐藏 overlay、左侧面板、表单。 - 不删除内存中的项目数据和已保存数据。 ## 3. 数据模型契约 Tauri 版 Rust 与前端必须使用同一份 JSON schema。建议 Rust `serde` 类型如下: ```rust #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub struct Rect { pub x: i32, pub y: i32, pub width: i32, pub height: i32, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WindowInfo { pub hwnd: Option, pub title: String, pub process_name: String, pub rect: Rect, pub is_desktop: bool, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum AnnotationType { Button, Input, Text, Image, Other, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Annotation { pub id: String, pub rect: Rect, pub control_name: String, pub description: String, #[serde(rename = "type")] pub annotation_type: AnnotationType, pub actions: Vec, pub feature_preview_path: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AnnotationProject { pub window: WindowInfo, pub custom_window_name: String, pub window_description: String, pub annotations: Vec, } ``` ### 3.1 `Rect` 方法 必须实现并测试: - `right() -> i32`:`x + width` - `bottom() -> i32`:`y + height` - `contains(x, y) -> bool`:使用 normalized 后半开区间 `[x, right)`、`[y, bottom)`。 - `normalized() -> Rect`:支持负向拖拽。 - `clamped_to(bounds) -> Rect`:矩形裁剪到边界内。 - `move_clamped(dx, dy, bounds) -> Rect`:保持原宽高移动,并完整限制在边界内。 ### 3.2 坐标约定 所有坐标都使用屏幕绝对物理像素: - 窗口 bbox:屏幕绝对物理像素。 - 标注框:屏幕绝对物理像素。 - JSON 保存:屏幕绝对物理像素。 首版不实现窗口移动后的标注自动重定位。 ## 4. JSON 保存格式 保存路径必须兼容当前版本: ```text %USERPROFILE%\.desktop_mark\last_project.json ``` JSON 字段固定: ```json { "window": { "hwnd": 123456, "title": "Untitled - Notepad", "process_name": "notepad.exe", "rect": { "x": 100, "y": 100, "width": 800, "height": 600 }, "is_desktop": false }, "custom_window_name": "登录页", "window_description": "用户登录窗口", "annotations": [ { "id": "uuid-string", "rect": { "x": 150, "y": 160, "width": 120, "height": 32 }, "control_name": "用户名输入框", "description": "输入登录用户名", "type": "input", "actions": ["input_text"], "feature_preview_path": null } ] } ``` 保存时机: - 标注表单保存后。 - 窗口名称或窗口简介变化后。 加载规则: - 启动时如果文件存在则加载到内存。 - 不自动显示 overlay。 - 加载失败时弹出错误,继续空项目。 - 不覆盖坏文件。 ## 5. Tauri 推荐架构 ### 5.1 窗口划分 建议使用 4 个 Tauri windows: 1. `floating` - 80×80。 - always-on-top。 - decorations false。 - transparent true。 - resizable false。 - skip taskbar true。 - 显示 `RPA` / `Ctrl+1`。 2. `overlay` - 覆盖虚拟屏幕。 - always-on-top。 - decorations false。 - transparent true。 - skip taskbar true。 - 初始隐藏。 - 用于窗口选择、遮罩、标注绘制。 3. `panel` - 宽 300。 - 高度为虚拟屏幕高度。 - 位于虚拟屏幕左侧。 - always-on-top。 - 初始隐藏。 - 显示窗口信息和标注列表。 4. `annotation_form` - 约 320×420。 - always-on-top。 - 初始隐藏。 - 位于标注框右下附近,越界则左/上偏移。 也可以把 `panel` 和 `annotation_form` 做成 overlay 内部 DOM,但独立窗口更接近当前 PySide6 行为。 ### 5.2 Rust 模块建议 ```text src-tauri/src/ main.rs models.rs rect.rs win32_window.rs hotkeys.rs storage.rs window_manager.rs commands.rs ``` 职责: - `models.rs`:`WindowInfo`、`Annotation`、`AnnotationProject`、`AnnotationType`。 - `rect.rs`:`Rect` 几何方法和单元测试。 - `win32_window.rs`:窗口枚举、进程名、虚拟屏幕。 - `hotkeys.rs`:`RegisterHotKey` 或 Tauri global shortcut 适配。 - `storage.rs`:JSON 保存/加载。 - `window_manager.rs`:创建/定位/显示隐藏 Tauri windows。 - `commands.rs`:暴露给前端的 Tauri commands。 ### 5.3 前端模块建议 ```text src/ main.tsx state/projectStore.ts types.ts windows/Floating.tsx windows/Overlay.tsx windows/Panel.tsx windows/AnnotationForm.tsx geometry/rect.ts ``` 职责: - `types.ts`:与 Rust serde 对齐的类型。 - `projectStore.ts`:当前项目、锁定窗口、hover 窗口、选中标注、overlay mode。 - `Overlay.tsx`:遮罩、窗口高亮、标注框、鼠标交互。 - `Panel.tsx`:窗口信息、窗口名称/简介、标注列表。 - `AnnotationForm.tsx`:控件名、描述、类型、动作、预览占位。 - `rect.ts`:前端几何算法,必须与 Rust 测试一致。 ## 6. Rust Native API / Tauri Commands 建议命令: ```rust #[tauri::command] fn get_virtual_screen_rect() -> Rect; #[tauri::command] fn window_at(x: i32, y: i32, excluded_hwnds: Vec) -> WindowInfo; #[tauri::command] fn foreground_window(excluded_hwnds: Vec) -> Option; #[tauri::command] fn save_project(project: AnnotationProject) -> Result<(), String>; #[tauri::command] fn load_project() -> Result, String>; #[tauri::command] fn set_tool_window_exclusions(labels: Vec) -> Result, String>; ``` 前端事件: ```text hotkey:start-window-picking hotkey:start-annotation project:changed annotation:edit-requested annotation:selected flow:cancelled ``` ## 7. Win32 实现要点 Rust 可使用 `windows` crate。 必须调用或等价实现: - `SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)` - `GetTopWindow(HWND(0))` - `GetWindow(hwnd, GW_HWNDNEXT)` - `IsWindowVisible(hwnd)` - `IsIconic(hwnd)` - `GetWindowLongPtrW(hwnd, GWL_EXSTYLE)`,跳过 `WS_EX_TOOLWINDOW` - `GetWindowRect(hwnd)` - `GetWindowTextW(hwnd)` - `GetWindowThreadProcessId(hwnd, &mut pid)` - `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)` - `QueryFullProcessImageNameW` 或等价 API 获取进程路径 - `GetSystemMetrics(SM_XVIRTUALSCREEN)` 等虚拟屏幕指标 不要把 overlay window 作为候选窗口。所有 Tauri 工具窗口 hwnd 必须加入排除集合。 ## 8. UI 字段与枚举 ### 8.1 标注类型 固定值: ```text button input text image other ``` ### 8.2 动作枚举 固定值: ```text click double_click right_click input_text hover scroll ``` ### 8.3 左侧面板字段 - 窗口标题。 - 进程名。 - bbox:`x, y, width, height`。 - 窗口名称:可编辑。 - 窗口简介:可编辑。 - 标注列表:`序号. label (type)`。 `label` 优先级: 1. `control_name` 2. `description` 3. `标注 {index}` ### 8.4 标注表单字段 - 控件名:文本输入。 - 描述:多行文本。 - 类型:下拉选择。 - 动作:多选列表。 - 特征图:占位,显示 `预览待生成`。 - 保存按钮。 - 取消按钮。 保存行为: - 更新当前 annotation。 - `feature_preview_path` 保持 `null`。 - 刷新左侧列表。 - 自动保存 JSON。 - 隐藏表单。 取消行为: - 只关闭表单。 - 不删除 annotation。 ## 9. 关键行为验收 ### 9.1 启动 命令启动后: - 桌面出现 80×80 `RPA` 悬浮窗。 - overlay、panel、form 不显示。 - 无控制台异常。 ### 9.2 窗口选择 准备:打开记事本和浏览器。 操作:按 `Ctrl+1`。 预期: - 全屏半透明遮罩出现。 - 鼠标移动到记事本/浏览器时,对应窗口显示蓝色边框。 - 被高亮窗口内部无遮罩。 - 鼠标移动到桌面空白时,蓝框覆盖虚拟屏幕。 ### 9.3 锁定与取消 操作:在浏览器窗口上点击。 预期: - 蓝框锁定浏览器。 - 鼠标移动到其他窗口不切换蓝框。 - 按 `Esc` 或回车后 overlay 隐藏。 - 悬浮窗仍存在。 ### 9.4 标注 操作:重新 `Ctrl+1` 锁定记事本,按 `Ctrl+R`,拖拽两个矩形。 预期: - 两个橙色标注框出现。 - 标注框不能拖出记事本窗口。 - 选中框显示 8 个调整手柄。 - 左侧出现 300px 信息面板并列出两个标注。 ### 9.5 表单与保存 操作:完成标注后,在表单输入: - 控件名:`用户名输入框` - 描述:`输入登录用户名` - 类型:`input` - 动作:`input_text` 点击保存。 预期: - 左侧列表同步显示该标注。 - `%USERPROFILE%\.desktop_mark\last_project.json` 存在。 - JSON 包含对应 `control_name`、`description`、`type`、`actions`。 ### 9.6 边界 操作:在锁定窗口边缘向窗口外拖拽创建标注。 预期: - 最终矩形被裁剪到锁定窗口内。 - 小于 `6×6` 的点击/拖拽不会新增标注。 ## 10. 与当前 Python 版本保持一致的边界决定 - 只支持 Windows 10/11。 - 坐标保存为屏幕绝对物理像素。 - 不实现窗口移动后的标注自动重定位。 - 不实现特征图真实截图,只显示占位。 - 快捷键冲突时显示错误,不自动改用其他快捷键。 - 工具自身窗口必须被窗口识别排除。 - 没有命中应用窗口时选择整个虚拟屏幕。 ## 11. 推荐开发顺序 1. 建 Tauri 项目和四个窗口。 2. 实现 Rust `Rect`、模型、JSON 保存/加载和单元测试。 3. 实现 Win32 虚拟屏幕与窗口识别。 4. 实现全局快捷键。 5. 实现 `floating` 窗口。 6. 实现 `overlay` 状态机和窗口高亮。 7. 实现标注框创建、移动、缩放。 8. 实现 `panel` 和 `annotation_form`。 9. 串联自动保存和启动加载。 10. 按第 9 节逐项手工验收。 ## 12. 当前 Python 版本验证基线 当前版本已验证: ```text uv venv --python 3.11 --seed --clear .\.venv\Scripts\python -m pip install -e .[dev] .\.venv\Scripts\python -m pytest ``` 结果: ```text 6 passed ``` 启动烟测: ```text .\.venv\Scripts\python -m desktop_mark ``` 进程保持运行 3 秒,无 stdout/stderr 异常;测试后主动终止。