edit | blame | history | raw

退出编辑后可拖分割线 — 技术方案 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 收敛归档。