# 退出编辑后可拖分割线 — 技术方案 v1 > 三位架构师合议收敛 (D 系统+E 性能+F 产品衔接) | 2026-05-23 --- ## 一、方案概要 **目标**:退出编辑模式后,已应用布局的窗口之间的分割线仍然可以拖拽,对标 macOS 原生 Split View 体验。 **核心方案**:每个分割线位置生成 4px 透明 NSWindow 覆盖层。 ### 可行性结论 | 维度 | 结论 | |------|------| | API 要求 | ✅ 全部公开(NSWindow + AX API),不关 SIP | | 静止 CPU | ✅ ≈0%(WindowServer 管理,无 timer 无轮询) | | 静止 GPU | ✅ ~0-0.8%(极小表面合成) | | 静止内存 | ✅ < 0.5 MB(3 个 overlay 约 144 KB) | | 拖拽帧率 | ✅ 15-30fps(CADisplayLink 限流) | | 总开发成本 | ✅ **~5.5 人天**(3 阶段交付) | --- ## 二、架构设计 ### 新增模块 ``` Services/DividerOverlayService.swift ~200行 主服务 Services/DividerDragController.swift ~80行 拖拽逻辑 Services/DividerOverlayView.swift ~120行 NSView 覆盖层 Services/DividerMath.swift ~40行 数学运算 Models/WindowConstraints.swift ~50行 约束模型 ``` ### 生命周期 ``` 编辑模式 → 用户确认应用 → 退出编辑 │ DividerOverlayService.create() ├─ 读取窗口约束 (T2 检测) ├─ 计算分割线位置 └─ 为每根分割线创建 NSWindow overlay │ 用户拖拽分割线 │ 松手 → T3 检测 │ ──→ 再次进入编辑模式 │ DividerOverlayService.destroy() │ ──→ 用户退出 App │ DividerOverlayService.cleanup() ``` ### AppState 的改动 AppState 增加一个可选属性: ```swift var dividerOverlayService: DividerOverlayService? ``` 不存储约束数据,不参与业务逻辑。DividerOverlayService 在: - 创建时设置到 AppState - 销毁时设为 nil - 通过 AppState 访问窗口数据(只读) ### 不修改的文件(13个) WindowService、LayoutExecutionService、DataService、BrowseViewModel、EditViewModel、AlignerApp、所有 Views 文件、FrameMath、TileCalculator --- ## 三、约束检测与降级 ### 三检测点 | 检测点 | 时机 | 作用 | |--------|------|------| | **T1** | 用户确认应用布局时 | 首次可行性判断,触发降级算法 | | **T2** | 退出编辑模式瞬间 | 覆盖编辑期间用户手动调窗的变更 | | **T3** | 分割线拖拽结束时 | 防止拖拽超出窗口约束 | ### 降级路径 ``` 用户选4窗 → 检测2×2 → 3窗(左1右2)→ 2窗(左右)→ 2窗(上下)→ 报错 兼容 兼容 兼容 兼容 ↓ ↓ ↓ ↓ 应用2×2 应用3格 左右分屏 上下分屏 ``` 同级降级自动执行 + Toast;减窗降级弹确认对话框。 ### 拖拽约束维持 - 开始拖拽时 recalculateMargins()(根据窗口 min/max 计算合法区间) - 每帧 clampDividerPosition()(clamp 到合法区间) - 越界时分割线闪烁红色 + 弹回(无声反馈) --- ## 四、性能参数 | 状态 | CPU | GPU | 内存 | |------|-----|-----|------| | 静止 | 0% | ~0-0.8% | <0.5MB | | Hover | ~0.1% | ~0.2% | <0.5MB | | 拖拽中 | ~0.5-1.5% | ~0.3-1% | <0.5MB | AX setRect 单次延迟:p50=1.2ms, p90=3.8ms, p99=12.4ms 推荐帧率:15fps(通用) → 24fps(Cocoa) → 30fps(原生) 自适应 --- ## 五、窗口同步策略 用户手动移动窗口后,覆盖层如何同步? **首推:懒检测法(Lazy Check)** - 静止时不做任何检查(零消耗) - 第一次 hover 覆盖层时,通过 AX rect 读取检查窗口实际位置 - 不匹配 → 重新计算 divider 位置 → reposition **辅助:全局鼠标事件** - 检测到鼠标释放时,标记"可能有窗口移动" - 进入下一个 runloop 周期做轻量检查 --- ## 六、实施计划 | Phase | 内容 | 工期 | 交付 | |-------|------|------|------| | **1** | 约束检测 + 降级逻辑 + DividerMath | 2天 | Build ✅ Test ✅ | | **2** | NSWindow Overlay + DividerDragController | 2天 | Build ✅ Test ✅ | | **3** | 联动 EditViewModel + 窗口同步 + 自适应帧率 | 1.5天 | Build ✅ Test ✅ QA ✅ PO ✅ | **总计:~5.5 人天** --- ## 七、不做的(明确排除) - ❌ 后台常驻(退编辑后只有 overlay,无 timer 无轮询) - ❌ 关 SIP - ❌ 私有 API - ❌ 性能回退时不做"偷工减料"(不兼容就降级/报错,不留废缝) - ❌ 拖拽操作不出现在确认应用之前(编辑模式内的预览阶段不做分割线拖拽) --- *三位架构师合议文档,由 PM 收敛归档。*