Files
Just-DataInspector/Documents/UI_Refactor_Progress.md
T
DeepSeek V4.1 Flash 25264f9074 UI 重构 P3:Toolbar 迁移到控件树(新增 UiButtonWidget,逐像素 0 差异 + 功能断言)
新增 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。
2026-10-28 12:29:08 +08:00

12 KiB
Raw Blame History

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)
场景缓存(Ensure/Capture/Blit/Invalidate/ReleaseSceneCache)
装饰层(Ensure/Release/Render/BlitDecorCache / ResolveAlphaBlend)
宽基底图层与预算(EnsureLaneLayer … BlitWideBuffer、并行重建、滚动补条、预热)
1 876
PlotViewCacheKey.cpp(新) MakeSceneCacheKey / SceneCacheMatches / SceneHaloRef
SceneContentHash / SceneGeometryHash / SceneDecorHash
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. 每阶段单独提交,提交信息写清"改了什么 / 为什么 / 怎么验证"。