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

490 lines
14 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 "Toolbar.hpp"
#include <algorithm>
#include <cmath>
#include <utility>
#include "UiBox.hpp"
namespace jdi
{
namespace
{
const float kButtonHeight = 30.0f; /**< 按钮高度(逻辑像素)。 */
const float kButtonGap = 6.0f; /**< 按钮间距。 */
const float kButtonPadding = 14.0f; /**< 按钮左右内边距。 */
const float kGroupGap = 18.0f; /**< 分组间距。 */
const float kSidePadding = 12.0f; /**< 工具栏左右留白。 */
const float kTopPadding = 8.0f; /**< 工具栏上下留白。 */
/**
* @brief 工具栏的根控件:只负责"整条底色 + 底部分隔线"。
*
* 按钮是它的子节点(`UiButtonWidget`),由 `Toolbar::Layout` 逐个 `Arrange`。
* 之所以把根做成控件而不是让 `Toolbar` 自己遍历按钮绘制:命中 / 悬停 / 按下的仲裁
* 全部交给 `UiWidget` 的派发链与悬停链,`Toolbar` 只做"规格表 → 控件"的翻译。
*/
class ToolbarRootWidget : public UiWidget
{
public:
void Configure(const Theme* theme, float scale)
{
m_theme = theme;
m_scale = scale;
}
/** @brief 根尺寸由宿主(`Toolbar::Layout`)给定,不参与父级测量。 */
gd::SizeF OnMeasure(const UiMeasureContext& ctx) override
{
(void)ctx;
return gd::SizeF(Bounds().Width, Bounds().Height);
}
void OnPaint(UiPaintContext& ctx) override
{
if (m_theme == nullptr || ctx.g == nullptr || !UiRectHasArea(Bounds()))
{
return;
}
const gd::RectF& area = Bounds();
UiFillRect(*ctx.g, area, m_theme->toolbarBg);
// 底部分隔线:按钮不会覆盖到这一行,因此"底色 + 分隔线先画"与旧实现像素一致。
UiDrawLine(*ctx.g, area.X, area.Y + area.Height - 1.0f, area.X + area.Width,
area.Y + area.Height - 1.0f, m_theme->border, 1.0f);
}
private:
const Theme* m_theme = nullptr; /**< 主题。 */
float m_scale = 1.0f; /**< 缩放系数。 */
};
/** @brief 按钮规格(顺序即绘制顺序)。 */
struct ButtonSpec
{
ToolbarAction action; /**< 动作。 */
const wchar_t* label; /**< 默认文本(图标按钮留空)。 */
bool primary; /**< 是否主按钮。 */
bool needsDocument; /**< 是否依赖已加载数据。 */
bool toggle; /**< 是否可切换。 */
UiButtonIcon icon; /**< 图标(`NONE` = 纯文本按钮)。 */
};
// ⚠️ **前两项构成"左侧固定簇"**(设置齿轮 + 导入文件),其余按钮作为整体靠右。
// `Layout` 里用 `kLeftClusterCount` 引用这个约定的长度,增删左侧按钮时必须同步。
// 齿轮与「导入文件」**同为强调色主按钮**(用户 2026-10-28 要求配色一致),
// 两者视觉上构成一组"文件/设置"入口,因此 primary 都为 true。
const ButtonSpec kButtonSpecs[] = {
{ToolbarAction::SETTINGS, L"", true, false, false, UiButtonIcon::GEAR},
{ToolbarAction::OPEN_FILE, L"导入文件", true, false, false, UiButtonIcon::NONE},
{ToolbarAction::FIT_ALL, L"适应视图", false, true, false, UiButtonIcon::NONE},
{ToolbarAction::AUTO_FIT_Y, L"Y 轴自适应", false, true, true, UiButtonIcon::NONE},
{ToolbarAction::ZOOM_SELECT, L"选区放大", false, true, true, UiButtonIcon::NONE},
{ToolbarAction::CURSOR, L"光标", false, true, true, UiButtonIcon::NONE},
{ToolbarAction::MERGE_LANES, L"合并网格", false, true, false, UiButtonIcon::NONE},
{ToolbarAction::SPLIT_LANES, L"拆分网格", false, true, false, UiButtonIcon::NONE},
{ToolbarAction::CLEAR_ALL, L"清空网格", false, true, false, UiButtonIcon::NONE},
};
/** @brief 左侧固定簇的按钮数量(从 `kButtonSpecs` 开头算起)。 */
constexpr std::size_t kLeftClusterCount = 2;
} // namespace
Toolbar::Toolbar() = default;
// 控件树用 `std::unique_ptr<UiWidget>` 持有,而具体根类型只在本 TU 可见
// ⇒ 析构函数必须在这里显式定义(否则其它 TU 里实例化会因类型不完整而编译失败)。
Toolbar::~Toolbar() = default;
void Toolbar::Setup(const UiFonts* fonts, const Theme* theme)
{
m_fonts = fonts;
m_theme = theme;
m_treeDirty = true;
}
void Toolbar::SetScale(float scale)
{
const float next = (scale > 0.1f) ? scale : 1.0f;
if (next == m_scale)
{
return;
}
m_scale = next;
// 控件里缓存了旧的缩放(字体实测宽度、图标尺寸)⇒ 必须重建。
m_treeDirty = true;
}
void Toolbar::SetDocumentLoaded(bool hasDocument)
{
if (m_documentLoaded == hasDocument)
{
return;
}
m_documentLoaded = hasDocument;
SyncEnabledState();
}
float Toolbar::DesiredHeight() const
{
return (kButtonHeight + kTopPadding * 2.0f) * m_scale;
}
bool Toolbar::IsEnabled(const Button& button) const
{
return button.needsDocument ? m_documentLoaded : true;
}
void Toolbar::SyncEnabledState()
{
for (Button& button : m_buttons)
{
if (button.widget != nullptr)
{
button.widget->SetEnabled(IsEnabled(button));
}
}
}
void Toolbar::RebuildWidgets()
{
m_root = std::make_unique<ToolbarRootWidget>();
static_cast<ToolbarRootWidget*>(m_root.get())->Configure(m_theme, m_scale);
m_buttons.clear();
const std::size_t specCount = sizeof(kButtonSpecs) / sizeof(kButtonSpecs[0]);
m_buttons.reserve(specCount);
for (std::size_t index = 0; index < specCount; ++index)
{
const ButtonSpec& spec = kButtonSpecs[index];
// 动态文本覆盖(例如「光标」按当前模式显示 光标 / 光标x1 / 光标x2)。
std::wstring label = spec.label;
for (const std::pair<ToolbarAction, std::wstring>& entry : m_labelOverrides)
{
if (entry.first == spec.action)
{
label = entry.second;
break;
}
}
Button button;
button.action = spec.action;
button.primary = spec.primary;
button.needsDocument = spec.needsDocument;
button.toggle = spec.toggle;
button.icon = spec.icon;
button.checked = false;
auto widget = std::make_unique<UiButtonWidget>();
UiButtonWidget* raw = widget.get();
raw->Configure(m_fonts, m_theme, m_scale, label, spec.primary, spec.toggle, kButtonPadding,
kButtonHeight);
raw->SetIcon(spec.icon);
if (m_theme != nullptr)
{
raw->SetCanvasColor(m_theme->toolbarBg);
}
raw->SetEnabled(IsEnabled(button));
// 点击回调只登记动作,由宿主在鼠标抬起后统一执行(与旧语义一致)。
raw->onClick = [this, action = spec.action]() { m_pendingAction = action; };
button.widget = raw;
m_root->AddChild(std::move(widget));
m_buttons.push_back(button);
}
}
void Toolbar::Layout(const gd::RectF& area)
{
m_area = area;
if (m_fonts == nullptr || m_fonts->ui == nullptr || m_theme == nullptr)
{
return;
}
// 控件树**只在必要时重建**(先记下各 toggle 的勾选状态,否则重建会把选中态清掉)。
// 纯粹的尺寸变化不走重建:按钮几何由 `Arrange` 决定,与树结构无关。
std::vector<std::pair<ToolbarAction, bool>> previousChecked;
if (m_treeDirty || m_root == nullptr)
{
previousChecked.reserve(m_buttons.size());
for (const Button& button : m_buttons)
{
previousChecked.emplace_back(button.action, button.checked);
}
RebuildWidgets();
m_treeDirty = false;
}
m_root->Arrange(area);
UiMeasureContext measure;
measure.theme = m_theme;
measure.fonts = m_fonts;
measure.scale = m_scale;
// 宽度由控件实测(`内边距 × 2 + 文本宽`),与旧公式完全一致;中文标签因此不会被裁切。
for (Button& button : m_buttons)
{
button.width = button.widget->Measure(measure).Width;
for (const std::pair<ToolbarAction, bool>& entry : previousChecked)
{
if (entry.first == button.action)
{
button.checked = entry.second;
button.widget->SetChecked(entry.second);
break;
}
}
}
// 左侧固定簇(设置齿轮 + 导入文件)靠左,其余按钮作为整体靠右。
const float top = area.Y + kTopPadding * m_scale;
const float height = kButtonHeight * m_scale;
const float leftX = area.X + kSidePadding * m_scale;
float leftWidth = 0.0f;
for (std::size_t index = 0; index < kLeftClusterCount && index < m_buttons.size(); ++index)
{
leftWidth += m_buttons[index].width;
if (index > 0)
{
leftWidth += kButtonGap * m_scale;
}
}
float cursorX = leftX;
for (std::size_t index = 0; index < kLeftClusterCount && index < m_buttons.size(); ++index)
{
m_buttons[index].widget->Arrange(gd::RectF(cursorX, top, m_buttons[index].width, height));
cursorX += m_buttons[index].width + kButtonGap * m_scale;
}
float groupWidth = 0.0f;
for (std::size_t index = kLeftClusterCount; index < m_buttons.size(); ++index)
{
groupWidth += m_buttons[index].width;
if (index + 1 < m_buttons.size())
{
groupWidth += kButtonGap * m_scale;
}
}
const float rightEdge = area.X + area.Width - kSidePadding * m_scale;
cursorX = std::max(leftX + leftWidth + kGroupGap * m_scale, rightEdge - groupWidth);
for (std::size_t index = kLeftClusterCount; index < m_buttons.size(); ++index)
{
m_buttons[index].widget->Arrange(gd::RectF(cursorX, top, m_buttons[index].width, height));
cursorX += m_buttons[index].width + kButtonGap * m_scale;
}
}
bool Toolbar::ButtonRect(ToolbarAction action, gd::RectF& rect) const
{
for (const Button& button : m_buttons)
{
if (button.action == action && button.widget != nullptr)
{
rect = button.widget->Bounds();
return UiRectHasArea(rect);
}
}
return false;
}
void Toolbar::SetToggleState(ToolbarAction action, bool checked)
{
for (Button& button : m_buttons)
{
if (button.action == action && button.toggle)
{
button.checked = checked;
if (button.widget != nullptr)
{
button.widget->SetChecked(checked);
}
return;
}
}
}
bool Toolbar::SetActionLabel(ToolbarAction action, const std::wstring& label)
{
// ⚠️ 只更新**唯一权威来源**(`m_labelOverrides`)并置树脏,不再逐个回写控件:
// 旧实现既要写覆盖表又要写控件,两处一旦不同步,下一次重建就会把文本退回去。
// 调用方拿到 true 后会重排(`Relayout`),届时由 `RebuildWidgets` 统一生效。
for (std::pair<ToolbarAction, std::wstring>& entry : m_labelOverrides)
{
if (entry.first == action)
{
if (entry.second == label)
{
return false;
}
entry.second = label;
m_treeDirty = true;
return true;
}
}
m_labelOverrides.emplace_back(action, label);
m_treeDirty = true;
return true;
}
void Toolbar::Paint(gd::Graphics& g)
{
if (m_theme == nullptr || m_fonts == nullptr || m_root == nullptr)
{
return;
}
UiPaintContext ctx;
ctx.g = &g;
ctx.clip = m_area;
ctx.scale = m_scale;
m_root->Paint(ctx);
}
bool Toolbar::HitTest(int x, int y) const
{
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
}
void Toolbar::BuildBoxes(UiBoxTable& boxes)
{
UiComponent::BuildBoxes(boxes);
if (m_root == nullptr)
{
return;
}
// ⚠️ 跳过根控件:它的矩形与工具栏区域完全重合(已经被上一层登记过)。
for (const std::unique_ptr<UiWidget>& button : m_root->Children())
{
if (button == nullptr)
{
continue;
}
boxes.Add(button->Bounds(), 1);
// 按钮**内容**(文本或齿轮图标)单独上报:它是居中绘制的,与按钮矩形不同。
const UiButtonWidget* raw = dynamic_cast<const UiButtonWidget*>(button.get());
if (raw != nullptr)
{
boxes.Add(raw->ContentRect(), 2);
}
}
}
bool Toolbar::OnMouseMove(int x, int y)
{
if (m_root == nullptr)
{
return false;
}
UiEvent event;
event.kind = UiEventKind::MOUSE_MOVE;
event.x = x;
event.y = y;
// 悬停链切换会通过 `Invalidate` 把矩形冒泡到根 ⇒ 用脏区判断"是否需要重绘"
//(与旧实现"悬停下标变化才重绘"等价,但不再手工维护下标)。
return m_root->DispatchRedraw(event);
}
bool Toolbar::OnLButtonDown(int x, int y, ToolbarAction& action)
{
action = ToolbarAction::NONE;
if (m_root == nullptr)
{
return false;
}
UiEvent event;
event.kind = UiEventKind::MOUSE_DOWN;
event.x = x;
event.y = y;
event.button = UiMouseButton::LEFT;
m_root->Dispatch(event);
(void)m_root->TakeDirty();
for (const Button& button : m_buttons)
{
if (button.widget != nullptr && button.widget->Pressed())
{
action = button.action;
return true;
}
}
return false;
}
bool Toolbar::OnLButtonUp(int x, int y, ToolbarAction& action)
{
action = ToolbarAction::NONE;
if (m_root == nullptr)
{
return false;
}
// 旧语义:没有按下过就不进入抬起处理。
bool hadPressed = false;
for (const Button& button : m_buttons)
{
if (button.widget != nullptr && button.widget->Pressed())
{
hadPressed = true;
break;
}
}
if (!hadPressed)
{
return false;
}
m_pendingAction = ToolbarAction::NONE;
UiEvent event;
event.kind = UiEventKind::MOUSE_UP;
event.x = x;
event.y = y;
event.button = UiMouseButton::LEFT;
m_root->Dispatch(event);
// 旧实现会在抬起时按落点重算悬停态;派发一条 MOUSE_MOVE 让悬停链同步。
UiEvent sync;
sync.kind = UiEventKind::MOUSE_MOVE;
sync.x = x;
sync.y = y;
m_root->Dispatch(sync);
(void)m_root->TakeDirty();
action = m_pendingAction;
m_pendingAction = ToolbarAction::NONE;
return true;
}
bool Toolbar::OnMouseLeave()
{
if (m_root == nullptr)
{
return false;
}
UiEvent event;
event.kind = UiEventKind::MOUSE_LEAVE;
event.x = 0;
event.y = 0;
m_root->Dispatch(event);
// 指针离开窗口后不会再收到 MOUSE_UP ⇒ 在这里放弃"按下"(与旧实现一致)。
bool changed = false;
for (Button& button : m_buttons)
{
if (button.widget != nullptr && button.widget->Pressed())
{
button.widget->CancelPress();
changed = true;
}
}
changed = UiRectHasArea(m_root->TakeDirty()) || changed;
return changed;
}
} // namespace jdi