## 为什么
用户 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` 的文档里。
667 lines
16 KiB
C++
667 lines
16 KiB
C++
#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
|