新增 Components/Shared/{Inc,Src}/UiButtonWidget.*:通用「文字按钮」控件,视觉规则与旧 Toolbar
按钮逐像素一致(主按钮强调色实底 / 普通按钮 1px 描边 / 可切换选中态 / 禁用态配色);
交互由控件自己处理:按下即 CaptureMouse 进入按下态,在自身范围内松开才触发 onClick,
指针离开(不会再收到抬起)时 CancelPress。
Toolbar 内部重写,公开 API 一字未改:
- Button 只保留静态配置 + 控件指针;矩形与外观状态归控件。
- 删除 m_hoverIndex / m_pressedIndex 与死字段 Button::hover,改由 UiWidget 的悬停链与 capture 维护。
- Paint 改为「根控件(底色 + 底部分隔线)+ 按钮子节点」,一次 root->Paint(ctx)。
- OnMouseMove/Down/Up/Leave 统一翻译成 UiEvent 后 root->Dispatch;动作经 onClick 回调登记、抬起后执行。
框架约定调整:UiWidget::Paint 的裁剪从「严格等于自身矩形」改为「自身矩形外扩 1px(kUiClipBleed)」。
边框/描边以矩形边界为中心绘制,严格裁剪会切掉外半侧使控件变细、与旧绘制不再像素一致。
uicheck 对应断言改为「裁剪 ⊇ 自身矩形且不超出外扩范围」。
验证:
- make release / make debug 零警告;Target.exe 881,664 B(< 900KB)。
- 逐像素 A/B(8 场景:全览 / 深放大+自由标记+滤波+弱峰+光标 / 平移 / 悬停按钮 / 按下 / 松开 /
悬停主按钮 / 移出工具栏):迁移前后哈希逐一相同
A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289
E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113。
- 功能断言:locate-cursor-button PASS (1291,73);click-cursor-changes-mode PASS (1 -> 2);
release-outside-does-not-fire PASS (2 -> 2)。
- Tools/uicheck 62 passed / 0 failed。
188 lines
12 KiB
Markdown
188 lines
12 KiB
Markdown
# UI 重构进度(UI_Refactor 分支)
|
||
|
||
> 本文件是**跨会话/跨 Agent 的交接台账**。每次提交前更新;每一行都要能对应到一个可复跑的验证结果。
|
||
> 计划见 `Documents/UI_Refactor_Plan.md`;硬约束见 `Documents/agent_mem.md` §11.2。
|
||
|
||
## 分支状态
|
||
|
||
| 分支 | 基线 | 用途 |
|
||
| --- | --- | --- |
|
||
| `main` | `df715a0` | 可发布,**不受本次重构影响** |
|
||
| `dev` | `4e51b41`(删除过时报告 `refactor_report_2026-10-16.md`) | 日常开发基线 |
|
||
| `UI_Refactor` | 从 `dev` 切出 | 本次 UI 重构(全部提交只留在本地) |
|
||
|
||
## 阶段看板
|
||
|
||
| 阶段 | 内容 | 状态 |
|
||
| --- | --- | --- |
|
||
| **P0** | 离屏回归网 `Tools/uicheck`(入库) | ✅ 完成(62 项断言全绿) |
|
||
| **P1** | `UiLayout` + `UiWidget` 基础层 | ✅ 完成(编译零警告,`uicheck` 覆盖) |
|
||
| **P2** | `PlotViewPaint.cpp` 零行为分片 | ✅ 完成(**逐像素 0 差异**,见 §2/§3) |
|
||
| **P3** | 迁移 chrome(Toolbar / TabBar / 菜单 / 拾色器 / 列表行) | 🔄 **Toolbar 已完成**,其余待做 |
|
||
| **P4** | 迁移 5 个浮动面板 | ⏳ 待做 |
|
||
| **P5** | 波形管线包成叶子 surface + 共享 DIB | ⏳ 待做 |
|
||
|
||
---
|
||
|
||
## 提交记录
|
||
|
||
### 1. `P0 + P1`:UI 基础层与离屏回归网
|
||
**新增文件**
|
||
|
||
| 文件 | 行数 | 职责 |
|
||
| --- | --- | --- |
|
||
| `Components/Shared/Inc/UiLayout.hpp` | ~250 | 布局原语 + `UiLayoutCache` |
|
||
| `Components/Shared/Src/UiLayout.cpp` | ~300 | 矩形原语 / 对齐 / Stack / Flow / 缓存 |
|
||
| `Components/Shared/Inc/UiWidget.hpp` | ~330 | 控件基类接口与设计说明 |
|
||
| `Components/Shared/Src/UiWidget.cpp` | ~560 | 树 / Measure·Arrange / Paint(脏区+裁剪)/ 派发 / 仲裁 / 动画 |
|
||
| `Tools/uicheck/main.cpp` | ~570 | 离屏回归网(62 项断言) |
|
||
| `Tools/uicheck/build.ps1` | ~70 | 链接 `Build/<Config>/*.o`(排除 `Main.o`) |
|
||
|
||
**已实现并验证的行为**
|
||
|
||
- 布局:`UiAlignInRect`(含 `STRETCH`、超尺寸夹取、偏移后置)、`UiRectSnapInward`(设备像素内侧对齐)、
|
||
`UiStackRects`(固定 / `kUiSizeAuto` 均分 / **绝不产生负尺寸**)、`UiFlowRects`(换行 + `overflow` 下标)。
|
||
- 缓存:`UiLayoutCache` 用 `std::deque` 保证**引用稳定**;容量淘汰按"最久未命中";`Invalidate` 为**定向**失效。
|
||
- 布局 pass:`Measure` 命中缓存不重复调用 `OnMeasure`;`InvalidateMeasure` **只**沿祖先链失效(兄弟不重算)。
|
||
- 绘制:`dirty` 为空 = 全画;与子树矩形无交集 ⇒ **整棵子树跳过**;有效裁剪 = 祖先裁剪切 ∩ 自身矩形 ∩ 脏区;
|
||
不可见节点跳过。
|
||
- 派发:**只沿命中链**(最深的命中节点 → 祖先冒泡),**绝不分发给同层兄弟**;`capture` 期间事件直投捕获者;
|
||
`modal` 只向模态子树派发;`hit-transparent` 让事件穿透;`QUERY_CURSOR` 由最上层认领者返回;
|
||
键盘/字符沿**焦点链**派发(无焦点交给根,保证宿主级快捷键可用)。
|
||
- 动画:`AdvanceAnimation` 逐节点下发同一时间戳并汇总"是否仍有动画"。
|
||
- 悬停:进入 / 离开各只触发一次,跨子节点迁移时正确配对。
|
||
|
||
**过程中修掉的两个自身缺陷**
|
||
|
||
1. `UiWidget` 缺 `AdvanceAnimationSelf` / `AnimationActiveSelf` 两个 protected 虚函数(`.cpp` 已引用、头文件漏声明)。
|
||
2. 最初的派发实现是"遍历同层兄弟、未消费就继续" ⇒ **一个点击可能同时被视觉上位于下方的控件处理**;
|
||
已改为"只沿命中链派发 + 祖先兜底"。回归网里对应的断言从"绝对次数"改为**增量断言**
|
||
(绝对次数会被前面用例计数污染,本轮踩过一次)。
|
||
|
||
**验证**
|
||
|
||
```
|
||
make release TARGET_FILE_NAME=TargetCheck # 零警告
|
||
powershell -File Tools/uicheck/build.ps1 # 链接 Build/Release/*.o(排除 Main.o)
|
||
Tools/uicheck/uicheck.exe # 62 passed, 0 failed
|
||
```
|
||
|
||
**未做(有意)**
|
||
|
||
- 未改动任何现有组件(Toolbar / TabBar / SignalPanel / PlotView 仍走旧路径),因此**本提交对产品像素零影响**。
|
||
- 未把 `UiWidget` 接入 `Application`,避免在 P2 之前引入行为差异。
|
||
|
||
---
|
||
|
||
### 2. `P2`:`PlotViewPaint.cpp` 按关注点分片(零行为变更)
|
||
|
||
**为什么**:该文件 9 080 行里混住四类内容 —— 真绘制 ≈3.5k / 光栅化缓存 ≈2.5k / 缓存键 ≈1.1k /
|
||
布局与命中几何 ≈1.3k。按"函数名前缀"分片是上一轮膨胀的直接原因,本轮按**关注点**再分一次。
|
||
|
||
**做法**:**机械搬移成员函数定义**(不改一行逻辑)。`PlotViewInternal.hpp` 里的 `pvdetail` 全部 `inline`
|
||
⇒ 跨分片安全,无需改任何 include、无需改 `Makefile`(按 basename 自动收集 `.cpp`)。
|
||
|
||
| 文件 | 搬入内容 | 行数变化 |
|
||
| --- | --- | --- |
|
||
| `PlotViewRaster.cpp`(新) | 光晕离屏层(`PaintSignalHalo*` / `UploadHaloLayer` / `ReleaseHaloLayer`)<br>场景缓存(`Ensure/Capture/Blit/Invalidate/ReleaseSceneCache`)<br>装饰层(`Ensure/Release/Render/BlitDecorCache` / `ResolveAlphaBlend`)<br>宽基底图层与预算(`EnsureLaneLayer` … `BlitWideBuffer`、并行重建、滚动补条、预热) | 1 876 |
|
||
| `PlotViewCacheKey.cpp`(新) | `MakeSceneCacheKey` / `SceneCacheMatches` / `SceneHaloRef`<br>`SceneContentHash` / `SceneGeometryHash` / `SceneDecorHash`<br>`CollectSceneStyles` / `FindStyleOnlyLanes` / `CollectSceneDerived` / `RepaintSceneCachePatch` | 474 |
|
||
| `PlotViewPaint.cpp` | 只剩绘制(`Paint*` / `Render*` / 输入框绘制) | 9 080 → **6 252**(−31%) |
|
||
|
||
**验证(关键)**
|
||
|
||
1. `make release TARGET_FILE_NAME=TargetCheck` **零警告**;`Target.exe` 873 984 → **875 008 B**(+1 KB,
|
||
属分片后各 TU 的对齐差异;仍远低于 900 KB 阈值)。
|
||
2. **逐像素 A/B**:临时写了一个渲染哈希探针(仓库外),对三个场景渲染后备缓冲区并取 FNV 哈希:
|
||
* A 默认全览;B 深放大 + 自由标记(含固定点)+ 均值滤波 + 弱峰调整 + 时间光标;C 再平移一帧(触发宽基底滚动/补条)。
|
||
* **分片前**:`A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418`
|
||
* **分片后**:**完全相同**(三个场景逐一一致)。
|
||
* 该探针未入库(一次性验证用),结论记录在此以便复现。
|
||
3. `Tools/uicheck` 62/62 全绿。
|
||
4. 顶层注释从"四个分片"更新为"六个分片(按关注点)",并写明"`Paint/PaintLane` 仍只允许定义在 `PlotViewPaint.cpp`"。
|
||
|
||
**⚠️ 复现 A/B 的坑**:机械搬移后用旧的 `.bak` 回退时,`Build/<Config>/` 里会残留
|
||
`PlotViewRaster.o` / `PlotViewCacheKey.o`,链接探针(`Build/Release/*.o`)会因重复符号失败 ——
|
||
**必须先删掉这两个陈旧 `.o`** 再链接。
|
||
|
||
---
|
||
|
||
### 3. `P2 续`:把"布局与命中几何"从绘制分片里拆出(`PlotViewPanels.cpp`)
|
||
|
||
**为什么**:`EdgePanelLayoutFor`(651 行,19 个调用点)、`EdgePanelWidth`、`LaneBadgeRect`、
|
||
`FreeMarkerValue/Delta/Contents/LabelRects`、`ProbeFreeMarker` 都是**几何**,却被塞在绘制分片里 ——
|
||
改一次布局要动 9 千行的绘制文件,回归面被放大。
|
||
|
||
**做法**:同样机械搬移(5 个不连续块),`PlotViewPaint.cpp` 从 6 645 → **4 913** 行。
|
||
|
||
**最终七个分片**(按关注点)
|
||
|
||
| 文件 | 行数 | 职责 |
|
||
| --- | --- | --- |
|
||
| `PlotViewPaint.cpp` | 4 913 | 绘制(`Paint*` / `Render*` / 输入框绘制) |
|
||
| `PlotViewInteraction.cpp` | 3 430 | 命中、鼠标/键盘、光标形状、面板层序 |
|
||
| `PlotViewFeatures.cpp` | 3 266 | 光标读数、提示、缩放、自适应、派生曲线、边沿统计 |
|
||
| `PlotViewRaster.cpp` | 1 876 | 光晕层 / 场景缓存 / 装饰层 / 宽基底图层与预算 |
|
||
| `PlotViewPanels.cpp` | 1 359 | 面板布局、图例徽标几何、自由标记取值与探测 |
|
||
| `PlotView.cpp` | 889 | 视图核心(构造、布局、坐标映射、网格结构) |
|
||
| `PlotViewCacheKey.cpp` | 474 | 内容指纹与缓存键 |
|
||
|
||
**累计效果**:`PlotViewPaint.cpp` **9 080 → 4 913(−46%)**,最大文件从 9.1k 降到 4.9k。
|
||
|
||
**验证**
|
||
|
||
1. `make release TARGET_FILE_NAME=TargetCheck` 零警告;`Target.exe` **877 568 B**(< 900 KB)。
|
||
2. **逐像素 A/B**:同三场景哈希与迁移前**完全一致**
|
||
(`A 2166933039626F66 / B C444ECFEB18FD73D / C 7984446D8E7A4418`)。
|
||
3. `Tools/uicheck` 62/62。
|
||
4. 顶层注释更新为"七个分片(按关注点)",并标明 `Paint/PaintLane` 只允许定义在 `PlotViewPaint.cpp`。
|
||
|
||
---
|
||
|
||
### 4. `P3`:Toolbar 迁移到控件树(**逐像素 0 差异**)
|
||
|
||
**新增**:`Components/Shared/{Inc,Src}/UiButtonWidget.*` —— 通用"文字按钮"控件,视觉规则与旧 Toolbar
|
||
按钮逐像素一致(主按钮强调色实底 / 普通按钮 1px 描边 / 可切换选中态 / 禁用态配色);
|
||
交互由自己处理:按下即 `CaptureMouse()` 进入按下态,**在自身范围内**松开才触发 `onClick`,
|
||
指针离开时 `CancelPress()`。
|
||
|
||
**`Toolbar` 内部重写(公开 API 一字未改)**:
|
||
|
||
| 旧实现 | 新实现 |
|
||
| --- | --- |
|
||
| `std::vector<Button>` 里既存配置又存矩形/悬停/按下 | `Button` 只存**静态配置 + 控件指针**;几何与外观状态归控件 |
|
||
| `m_hoverIndex` / `m_pressedIndex` 手工维护 | 由 `UiWidget` 的**悬停链**与 capture 维护 |
|
||
| `Paint` 自己遍历按钮绘制 | 根控件(`ToolbarRootWidget`:底色 + 底部分隔线)+ 按钮子节点,`root->Paint(ctx)` |
|
||
| `OnMouseMove/Down/Up/Leave` 各自扫按钮矩形 | 统一翻译成 `UiEvent` → `root->Dispatch(event)`;动作经 `onClick` 回调登记、抬起后执行 |
|
||
| `Button::hover` 死字段 | 删除(`Hovered()` 由基类提供) |
|
||
|
||
**新增框架约定(重要)**:`UiWidget::Paint` 的裁剪由"严格等于自身矩形"改为
|
||
**"自身矩形外扩 `kUiClipBleed`(1px)"**。原因:边框/描边以矩形边界为中心绘制,1px 笔宽有一半在矩形外,
|
||
严格裁剪会把外半侧切掉 ⇒ 控件看起来变细(与旧绘制代码不再逐像素一致)。
|
||
`uicheck` 对应断言改为"裁剪 ⊇ 自身矩形且不超出外扩范围"。
|
||
|
||
**验证**
|
||
|
||
1. `make release` / `make debug` **零警告**;`Target.exe` **881 664 B**(< 900 KB)。
|
||
2. **逐像素 A/B(8 个场景)**:迁移前后哈希逐一相同
|
||
`A 2166933… / B C444ECF… / C 7984446… / D BF85587… / E 1C663E4… / F 0535292… / G 7A58172… / H 23F390D…`
|
||
(A 全览;B 深放大+自由标记+均值滤波+弱峰调整+光标;C 平移;D 悬停按钮;E 按下未松开;
|
||
F 松开;G 悬停主按钮;H 移出工具栏)。
|
||
3. **功能验证**(证明点击真的翻译成动作,而不是"看起来没坏"):
|
||
* `FUNC locate-cursor-button PASS (1291,73)` —— 用控件自身矩形定位按钮;
|
||
* `FUNC click-cursor-changes-mode PASS (1 -> 2)` —— 点击「光标」按钮后光标模式 单→双;
|
||
* `FUNC release-outside-does-not-fire PASS (2 -> 2)` —— 按下后移出再松开,不触发动作。
|
||
4. `Tools/uicheck` **62/62**。
|
||
|
||
---
|
||
|
||
## 下一步(接手者请从这里开始)
|
||
|
||
1. **P3 续**:按同一套路迁移 `TabBar`(标签 + 「+」按钮 + 重命名内联输入框)→ 再迁移
|
||
`PopupMenu` / `ColorPickerPopup` 的条目行。判据同为"8+ 场景像素哈希一致 + 功能断言"。
|
||
2. **P4**:5 个浮动面板(`EdgePanelLayoutFor` 651 行 → 真控件;Stack + Abs + Measure-driven),
|
||
消掉 `rowRefs/rowChecks/rowPoints/rowOrder/rowLevelTracks/...` 等平行数组。
|
||
⚠️ 每个面板单独一轮;吸顶/滚动/滚动条/滑块复位/行悬停压暗/开关联动是 6 轮补丁叠出的隐式约定。
|
||
3. **P5**:共享 top-down 32bpp `CreateDIBSection` + `SurfaceView{dc,rect,stride,bits,version}`,
|
||
把 56 处 `InvalidateRect` 收敛成脏区并集。
|
||
4. 每阶段单独提交,提交信息写清"改了什么 / 为什么 / 怎么验证"。
|