Files
DeepSeek V4.1 Flash 044b87383b 体积 L3(-fno-exceptions + MemProbe + LTO)与 L1 几何产物层;产物 909,824 -> 788,992 B
## 为什么

用户 2026-10-29 要求:按既定计划完整实现改进,但
① **不得把编译产物拆成 DLL**(项目定位是单文件轻量工具,最终只能有一个 .exe);
② 异常处理改用**预检验**而不是 try/catch(-fno-exceptions 省体积,且 try/catch 本身开销大);
③ 先删除 Documents 内三份 UI 重构文档再按"计划 / 当前进度 / 最终报告"重建;
④ 顺便审计代码,删除无用/过时注释,修复名实不符;
⑤ 提交到本地仓库,期间不要占用用户的鼠标/键盘/屏幕。

## 改了什么

### 1. 体积(909,824 B -> 788,992 B,-120,832 B,-13.3%)

Makefile 新增三个可覆盖开关(默认全为 1):

- `LAYOUT_BOUNDS`  编入/剔除「显示布局边界」叠加层
- `NO_EXCEPTIONS`  追加 `-fno-exceptions`
- `LTO`            追加 `-flto`(编译 + 链接)

派生:`FUNC_SECTIONS`(LTO 下为空)、`GC_SECTIONS`(`-Wl,--gc-sections`)、
`ASM_LISTING`/`ASM_LISTING_C`(LTO 下为空,因为 lto-wrapper 会丢弃 `-Wa` 清单)。

两个实测复现的坑(已写进 Makefile 注释):

- **切换 LTO 必须 `make clean`**:LTO 与非 LTO 目标文件混链会让 GCC 15.2.0 在
  `binds_to_current_def_p (symtab.cc:2598)` 处 ICE。
- **`-flto` 与 `-ffunction-sections -fdata-sections` 同时打开也 ICE**(干净重编也必崩,
  报错落点是 `DataModel.hpp:393`,但那是 COMDAT 解析落点、不是原因)⇒
  LTO 下 `FUNC_SECTIONS` 置空,并加 `-flto-partition=one`:默认 11 个分区的 LTRANS
  稳定复现,单分区从未复现。**这是稳定性选项,不是性能选项。**

### 2. 关异常必须成对交付:预检验 + 兜底

新增 `Utilities/Inc/MemProbe.hpp` + `Utilities/Src/MemProbe.cpp`:

- `MemAvailablePhysical()`:`GlobalMemoryStatusEx` + 200ms 缓存
- `MemCanAllocate(bytes)`:预留后仍须保有 1/8 可用内存
- `MemReserveIfRoom(container, n)`:含 `capacity()` 早退与乘法溢出保护
- `MemInstallTerminateHandler()`:OOM 时弹一次带建议的模态提示后 `_Exit`(而不是静默消失)

接入:

- `Utilities/Src/DataModel.cpp`:全仓**唯一**的 `try/catch`(`extremes.reserve` 捕
  `std::bad_alloc`)改为 `MemReserveIfRoom(extremes, count / 8)`
- `Components/PlotView/Src/PlotView.cpp`:`AvailablePhysicalBytes()` 原有一份**完整重复**
  的 `GlobalMemoryStatusEx` + 缓存实现 → 转发 `MemAvailablePhysical()`
- `Components/Application/Src/Application.cpp`:`Initialize()` 开头安装 terminate handler

### 3. L1 几何产物层

新增 `Components/Shared/Inc/UiBox.hpp` + `Src/UiBox.cpp`:

- `struct UiBox { rect, clip, depth, layer }` —— 纯值类型,不继承任何东西
  (继承会和 PlotView 4 层位图缓存 / SignalPanel 行位图缓存的失效体系打架)
- `class UiBoxTable : public UiTextRectSink` —— 只累计几何 + 裁剪/层/遮挡,**不含绘制代码**

`UiComponent::CollectDebugBoxes` → **`virtual void BuildBoxes(UiBoxTable&)`**:
组件从"收集布局边界(调试用)"改为"上报几何产物",不再需要知道"调试"这个概念存在。
`UiDebugBounds.hpp/.cpp` 退化为唯一的消费者(配色 + 包角描边 + 遮挡挖洞)。

除宿主 `ApplicationCommands.cpp` 外,所有组件源码现在只包含 `UiBox.hpp`。

### 4. 审计

- `MemProbe` 里"省 40~60KB"的估算 → 改为实测 27,136 B
- `MemProbe.hpp` 的 `<new>`、`MemProbe.cpp` 的 `<cstdio>`:未使用,删除
- `UiDebugPaintBounds(g, rect, depth)`:零调用者,删除(只保留带 clip 的版本)
- `TabBar`/`Toolbar`/`PlotView`/`SignalPanel` 的 `BuildBoxes` 文档"(调试用)"→"几何 box"
- `PlotView::SetForceFullRepaint` 补写"为什么保留而不优化"

### 5. 文档

`Documents/UI_Refactor_Plan.md` / `_Progress.md` / `_Report.md` 先 `git rm` 再按
「计划 / 当前进度 / 最终报告」重建;`Documents/agent_mem.md` 追加第十三轮记录。

## 怎么验证

```
make clean
make release TARGET_FILE_NAME=JustDataInspector -j16   # 零 error / 零 warning,788,992 B
make debug   TARGET_FILE_NAME=JustDataInspectorD -j16  # 零 warning

uicheck 69/69   lbcheck 21/21  lbresid 5/5  lbzorder 4/4  lbcovr 7/7  lbpanel 10/10
rendercheck: 8 个场景哈希与改造前逐一相同 + FUNC 7/7 PASS, exit=0
  A FAED59DF284153DB  B E6443BFE4E3955F8  C 96A0AA8C402F1729  D E3D5DBD998E43720
  E DA0583CB13C26A6F  F FFB822159F604D1D  G 19F8DE8FE3A8DF18  H DBBDDE4CB2C97C92
```

体积溯源(`objdump -h`,基线 → 最终):
`.text` 735,600 → 657,168;`.rdata` 49,184 → 35,016;
`.xdata` 48,132 → 27,512;`.pdata` 35,424 → 28,524;`.rsrc` 20,656 不变。

## 没做什么(有意为之)

- **没有拆 DLL**(用户明确要求单文件)
- **没有改优化等级**(`-Oz` 保持)
- **L2(`UiDamage` 脏矩形 + 独立叠加层)评估后缓做**:受益方只有"调试开关打开时的帧率",
  而正确实现必须把叠加层旧矩形注入底层组件重绘范围,穿透 PlotView 四条快路径 + 两层缓存;
  在用户反复报过"拖拽错位 / 贴图残留"的背景下,风险与收益不成比例。理由写在
  `Documents/UI_Refactor_Plan.md` §5 与 `PlotView::SetForceFullRepaint` 的文档里。
2026-10-29 11:53:54 +08:00

667 lines
16 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "UiWidget.hpp"
#include <algorithm>
#include <cmath>
#include "UiBox.hpp"
namespace jdi
{
UiEventResult UiEventResult::Handled()
{
UiEventResult result;
result.consumed = true;
result.redraw = true;
return result;
}
UiEventResult UiEventResult::Swallowed()
{
UiEventResult result;
result.consumed = true;
result.redraw = false;
return result;
}
UiPaintContext UiPaintContext::WithClip(const gd::RectF& rect) const
{
UiPaintContext child = *this;
gd::RectF intersection;
if (UiRectIntersect(clip, rect, intersection))
{
child.clip = intersection;
}
else
{
child.clip = gd::RectF();
}
return child;
}
UiWidget::UiWidget() = default;
UiWidget::~UiWidget()
{
// 子节点析构前先把"指向自己的根级状态"清掉:否则根的 m_capture / m_modal / m_hover
// 会留下悬空指针(子节点被移除后又被派发事件 = 访问违例)。
UiWidget* root = Root();
if (root != nullptr && root != this)
{
if (root->m_capture == this)
{
root->m_capture = nullptr;
}
if (root->m_modal == this)
{
root->m_modal = nullptr;
}
if (root->m_hover == this)
{
root->m_hover = nullptr;
}
}
}
// ------------------------------------------------------------------
// 树结构
// ------------------------------------------------------------------
UiWidget& UiWidget::AddChild(std::unique_ptr<UiWidget> child)
{
if (child == nullptr)
{
return *this;
}
UiWidget* raw = child.get();
raw->m_parent = this;
m_children.push_back(std::move(child));
// 子节点的期望尺寸会影响自己 ⇒ 向上失效。
InvalidateMeasure();
return *this;
}
void UiWidget::RemoveChild(UiWidget* child)
{
if (child == nullptr)
{
return;
}
for (auto iterator = m_children.begin(); iterator != m_children.end(); ++iterator)
{
if (iterator->get() == child)
{
(*iterator)->m_parent = nullptr;
m_children.erase(iterator);
InvalidateMeasure();
return;
}
}
}
UiWidget* UiWidget::Root()
{
UiWidget* node = this;
while (node->m_parent != nullptr)
{
node = node->m_parent;
}
return node;
}
const UiWidget* UiWidget::Root() const
{
const UiWidget* node = this;
while (node->m_parent != nullptr)
{
node = node->m_parent;
}
return node;
}
// ------------------------------------------------------------------
// 布局
// ------------------------------------------------------------------
void UiWidget::InvalidateMeasure()
{
if (!m_measureValid)
{
// 已经失效 ⇒ 不必继续上溯(祖先此前也一定被失效过)。
return;
}
m_measureValid = false;
if (m_parent != nullptr)
{
m_parent->InvalidateMeasure();
}
}
gd::SizeF UiWidget::Measure(const UiMeasureContext& ctx)
{
const gd::SizeF size = m_measureValid ? m_desiredSize : OnMeasure(ctx);
m_desiredSize = size;
m_measureValid = true;
return m_desiredSize;
}
void UiWidget::Arrange(const gd::RectF& bounds)
{
m_bounds = bounds;
OnArrange();
}
void UiWidget::MeasureChildren(const UiMeasureContext& ctx)
{
for (const std::unique_ptr<UiWidget>& child : m_children)
{
if (child == nullptr || !child->m_visible)
{
continue;
}
child->Measure(ctx);
}
}
gd::SizeF UiWidget::ChildDesiredSize(std::size_t index) const
{
if (index >= m_children.size() || m_children[index] == nullptr)
{
return gd::SizeF(0.0f, 0.0f);
}
return m_children[index]->m_desiredSize;
}
// ------------------------------------------------------------------
// 绘制
// ------------------------------------------------------------------
bool UiWidget::DirtyIntersects(const gd::RectF& dirty) const
{
gd::RectF intersection;
return UiRectIntersect(dirty, m_bounds, intersection);
}
void UiWidget::Paint(UiPaintContext& ctx)
{
if (!m_visible || ctx.g == nullptr || !UiRectHasArea(m_bounds))
{
return;
}
// (1) 与脏区求交:**脏区为空表示"未知 ⇒ 全画"**。
gd::RectF effectiveDirty = ctx.dirty;
if (UiRectHasArea(effectiveDirty) && !DirtyIntersects(effectiveDirty))
{
// 本节点及其整棵子树都不在脏区内 ⇒ 直接跳过(这是本框架唯一真正省时间的机制)。
return;
}
// (2) 裁剪 = 祖先裁剪切 ∩ (自身矩形外扩 1px);脏区非空时再与脏区求交。
// ⚠️ 外扩 1px 是刻意设计的:边框/描边是**以矩形边界为中心**画的(`UiDrawRoundRect`
// 的 1px 笔宽有一半在矩形外),严格按矩形裁剪会把外半侧切掉 ⇒ 控件看起来变细。
// 这是与旧绘制代码保持“逐像素一致”的必要保证。
UiPaintContext local = ctx;
gd::RectF clip;
if (!UiRectIntersect(ctx.clip, UiRectInflate(m_bounds, kUiClipBleed), clip))
{
return; // 完全被祖先裁掉
}
if (UiRectHasArea(effectiveDirty))
{
gd::RectF clipWithDirty;
if (!UiRectIntersect(clip, effectiveDirty, clipWithDirty))
{
return;
}
clip = clipWithDirty;
}
local.clip = clip;
// (3) 收紧 GDI+ 裁剪。⚠️ 这里**只**约束 GDI+ 矢量绘制;GDI 绘制(`UiText` /
// `UiFillRect` 的快路径 / `UiDrawSolidLines`)**不受**该裁剪约束 ⇒ 派生类必须把
// `local.clip` 传给 `UiTextClipped` 一类带 `ETO_CLIPPED` 的辅助函数,
// 否则文字会画到本节点之外(本项目"滚动时文字串到标题栏/工具栏"就是这类 BUG)。
const int saveState = ctx.g->Save();
ctx.g->SetClip(local.clip, gd::CombineModeIntersect);
local.dirty = UiRectHasArea(effectiveDirty) ? clip : gd::RectF();
OnPaint(local);
// (4) 子节点:顺序 = z 序(末尾最上层),脏区向下取交集。
for (const std::unique_ptr<UiWidget>& child : m_children)
{
if (child == nullptr)
{
continue;
}
UiPaintContext childContext = local;
gd::RectF childClip;
if (!UiRectIntersect(local.clip, UiRectInflate(child->m_bounds, kUiClipBleed), childClip))
{
continue;
}
childContext.clip = childClip;
childContext.dirty = local.dirty;
child->Paint(childContext);
}
ctx.g->Restore(saveState);
}
bool UiWidget::DispatchRedraw(UiEvent& event)
{
Dispatch(event);
return UiRectHasArea(TakeDirty());
}
void UiWidget::BuildBoxes(UiBoxTable& boxes, int depth) const
{
if (!m_visible || !UiRectHasArea(m_bounds))
{
// 不可见 / 未摆放的节点没有边界可画,其子树也不会被 `Paint` 画出来 ⇒ 一并跳过。
return;
}
boxes.Add(m_bounds, depth);
// 子节点按 z 序递归(末尾最上层)—— 与 `Paint` 的顺序一致,重叠时上层压住下层。
for (const std::unique_ptr<UiWidget>& child : m_children)
{
if (child != nullptr)
{
child->BuildBoxes(boxes, depth + 1);
}
}
}
void UiWidget::Invalidate(const gd::RectF& rect)
{
if (!UiRectHasArea(rect))
{
return;
}
m_dirty = UiRectUnion(m_dirty, rect);
if (m_parent != nullptr)
{
// 向上冒泡的是"客户区坐标"的同一块矩形;父层只并集,**不清空**子节点的脏区。
m_parent->Invalidate(rect);
}
}
void UiWidget::InvalidateAll()
{
m_dirty = m_bounds;
if (m_parent != nullptr)
{
m_parent->Invalidate(m_bounds);
}
for (const std::unique_ptr<UiWidget>& child : m_children)
{
if (child != nullptr)
{
child->InvalidateAll();
}
}
}
gd::RectF UiWidget::TakeDirty()
{
const gd::RectF dirty = m_dirty;
m_dirty = gd::RectF();
return dirty;
}
// ------------------------------------------------------------------
// 事件
// ------------------------------------------------------------------
bool UiWidget::Contains(int x, int y) const
{
if (!m_visible || m_hitTransparent || !UiRectHasArea(m_bounds))
{
return false;
}
const float fx = static_cast<float>(x);
const float fy = static_cast<float>(y);
return fx >= m_bounds.X && fx < m_bounds.X + m_bounds.Width && fy >= m_bounds.Y &&
fy < m_bounds.Y + m_bounds.Height;
}
UiWidget* UiWidget::HitTestDeep(int x, int y)
{
if (!Contains(x, y))
{
return nullptr;
}
// 反 z 序:末尾(最上层)优先。
for (auto iterator = m_children.rbegin(); iterator != m_children.rend(); ++iterator)
{
if (*iterator == nullptr)
{
continue;
}
if (UiWidget* hit = (*iterator)->HitTestDeep(x, y))
{
return hit;
}
}
return this;
}
UiEventResult UiWidget::Dispatch(UiEvent& event)
{
UiWidget* root = Root();
if (root == nullptr)
{
return UiEventResult();
}
const bool mouseEvent = (event.kind == UiEventKind::MOUSE_MOVE ||
event.kind == UiEventKind::MOUSE_DOWN ||
event.kind == UiEventKind::MOUSE_UP ||
event.kind == UiEventKind::MOUSE_DOUBLE_CLICK ||
event.kind == UiEventKind::MOUSE_WHEEL ||
event.kind == UiEventKind::QUERY_CURSOR);
// (1) 捕获优先:拖拽期间所有鼠标事件直接投给捕获者(与 Windows `SetCapture` 同义)。
if (mouseEvent && root->m_capture != nullptr)
{
return root->m_capture->OnEvent(event);
}
// (2) 模态:只向模态子树派发(菜单/拾色器等浮层)。
if (root->m_modal != nullptr)
{
return root->m_modal->DispatchAlongHitPath(event);
}
// (3) 悬停链(只在根节点维护:鼠标移动会改变悬停者)。
if (event.kind == UiEventKind::MOUSE_MOVE || event.kind == UiEventKind::MOUSE_LEAVE)
{
root->UpdateHover(event.x, event.y);
}
// (4) 键盘/字符:沿焦点链派发(无焦点时交给根,保证宿主级快捷键仍可用)。
if (event.kind == UiEventKind::KEY_DOWN || event.kind == UiEventKind::CHAR)
{
UiWidget* node = (root->m_focus != nullptr) ? root->m_focus : root;
for (; node != nullptr; node = node->m_parent)
{
UiEventResult result = node->OnEvent(event);
if (result.consumed)
{
return result;
}
}
return UiEventResult();
}
return DispatchAlongHitPath(event);
}
void UiWidget::CollectHitChain(int x, int y, std::vector<UiWidget*>& chain)
{
if (!Contains(x, y))
{
return;
}
chain.push_back(this);
// 反 z 序:末尾(最上层)优先;只进入**真正命中**的那一个子节点。
for (auto iterator = m_children.rbegin(); iterator != m_children.rend(); ++iterator)
{
if (*iterator == nullptr)
{
continue;
}
if ((*iterator)->Contains(x, y))
{
(*iterator)->CollectHitChain(x, y, chain);
return;
}
}
}
UiEventResult UiWidget::DispatchAlongHitPath(UiEvent& event)
{
if (!m_visible)
{
return UiEventResult();
}
const bool positional = (event.kind == UiEventKind::MOUSE_MOVE ||
event.kind == UiEventKind::MOUSE_DOWN ||
event.kind == UiEventKind::MOUSE_UP ||
event.kind == UiEventKind::MOUSE_DOUBLE_CLICK ||
event.kind == UiEventKind::MOUSE_WHEEL ||
event.kind == UiEventKind::QUERY_CURSOR);
if (!positional)
{
// 非位置事件(理论上已被 `Dispatch` 分流):自身优先,再逐个子节点。
UiEventResult result = OnEvent(event);
if (result.consumed)
{
return result;
}
for (const std::unique_ptr<UiWidget>& child : m_children)
{
if (child == nullptr || !child->m_visible)
{
continue;
}
result = child->DispatchAlongHitPath(event);
if (result.consumed)
{
return result;
}
}
return UiEventResult();
}
std::vector<UiWidget*> chain;
CollectHitChain(event.x, event.y, chain);
if (chain.empty())
{
return UiEventResult();
}
// 自最深的命中节点向上冒泡:命中者优先,未消费才交给祖先(合法的"兜底"语义)。
for (UiWidget* node : chain)
{
if (event.kind == UiEventKind::QUERY_CURSOR)
{
if (HCURSOR cursor = node->OnQueryCursor(event.x, event.y))
{
event.cursor = cursor;
return UiEventResult::Swallowed();
}
continue;
}
UiEventResult result = node->OnEvent(event);
if (result.consumed)
{
return result;
}
}
if (event.kind == UiEventKind::QUERY_CURSOR && event.cursor != nullptr)
{
return UiEventResult::Swallowed();
}
return UiEventResult();
}
void UiWidget::UpdateHover(int x, int y)
{
UiWidget* target = (m_visible) ? HitTestDeep(x, y) : nullptr;
if (target == m_hover)
{
return;
}
UiWidget* previous = m_hover;
m_hover = target;
if (previous != nullptr)
{
previous->m_hovered = false;
previous->OnHoverChanged(false);
previous->Invalidate(previous->m_bounds);
}
if (target != nullptr)
{
target->m_hovered = true;
target->m_hoverX = x;
target->m_hoverY = y;
target->OnHoverChanged(true);
target->Invalidate(target->m_bounds);
}
}
// ------------------------------------------------------------------
// 交互仲裁
// ------------------------------------------------------------------
void UiWidget::CaptureMouse()
{
UiWidget* root = Root();
if (root != nullptr)
{
root->m_capture = this;
}
}
void UiWidget::ReleaseMouse()
{
UiWidget* root = Root();
if (root != nullptr && root->m_capture == this)
{
root->m_capture = nullptr;
}
}
UiWidget* UiWidget::MouseCaptureWidget() const
{
const UiWidget* root = Root();
return (root != nullptr) ? root->m_capture : nullptr;
}
void UiWidget::SetModal(bool modal)
{
UiWidget* root = Root();
if (root == nullptr)
{
return;
}
if (modal)
{
root->m_modal = this;
}
else if (root->m_modal == this)
{
root->m_modal = nullptr;
}
}
UiWidget* UiWidget::ModalWidget() const
{
const UiWidget* root = Root();
return (root != nullptr) ? root->m_modal : nullptr;
}
UiWidget* UiWidget::HoverWidget() const
{
const UiWidget* root = Root();
return (root != nullptr) ? root->m_hover : nullptr;
}
bool UiWidget::HoverPoint(int& x, int& y) const
{
if (!m_hovered)
{
return false;
}
x = m_hoverX;
y = m_hoverY;
return true;
}
void UiWidget::RequestFocus()
{
UiWidget* root = Root();
if (root == nullptr || root->m_focus == this)
{
return;
}
if (root->m_focus != nullptr)
{
root->m_focus->Invalidate(root->m_focus->m_bounds);
}
root->m_focus = this;
Invalidate(m_bounds);
}
void UiWidget::ReleaseFocus()
{
UiWidget* root = Root();
if (root != nullptr && root->m_focus == this)
{
root->m_focus = nullptr;
Invalidate(m_bounds);
}
}
UiWidget* UiWidget::FocusWidget() const
{
const UiWidget* root = Root();
return (root != nullptr) ? root->m_focus : nullptr;
}
// ------------------------------------------------------------------
// 动画
// ------------------------------------------------------------------
bool UiWidget::AdvanceAnimation(std::int64_t nowMs)
{
bool active = AdvanceAnimationSelf(nowMs);
for (const std::unique_ptr<UiWidget>& child : m_children)
{
if (child != nullptr && child->AdvanceAnimation(nowMs))
{
active = true;
}
}
return active;
}
bool UiWidget::AnimationActive() const
{
if (AnimationActiveSelf())
{
return true;
}
for (const std::unique_ptr<UiWidget>& child : m_children)
{
if (child != nullptr && child->AnimationActive())
{
return true;
}
}
return false;
}
// ------------------------------------------------------------------
// 缓存键
// ------------------------------------------------------------------
std::uint64_t UiWidget::BaseContentKey() const
{
std::uint64_t key = 0xCBF29CE484222325ull;
key = UiMixKeyFloat(key, m_bounds.X);
key = UiMixKeyFloat(key, m_bounds.Y);
key = UiMixKeyFloat(key, m_bounds.Width);
key = UiMixKeyFloat(key, m_bounds.Height);
key = UiMixKey(key, m_visible ? 1u : 0u);
return key;
}
} // namespace jdi