## 1. 新功能:齿轮按钮 -> 应用设置菜单 -> 显示布局边界 - ToolbarAction::SETTINGS 位于「导入文件」左侧;工具栏布局改为 「左侧固定簇(kLeftClusterCount = 2:齿轮 + 导入文件)+ 右侧按钮组」。 - 齿轮是矢量化绘制(8 齿 + 外环 + 轴孔,全部按 half 的比例推导), 不用字体字符「⚙」——该字符在不同回退字体里大小与基线不一致,会忽大忽小、垂直不居中。 UiButtonWidget 新增 UiButtonIcon(NONE / GEAR)与 SetIcon。 - MenuScope::APP_SETTINGS(新增)+ 设置菜单:HEADER「调试」+ 勾选切换项「显示布局边界」 (keepOpen:点击后菜单保持打开,勾选态由 UpdateItemLabel 回写)。 锚点取 Toolbar::ButtonRect(SETTINGS) 的控件实测矩形,宿主不再自己推算按钮几何。 ## 2. 布局边界:基类给协议,组件自己上报 - UiComponent 新增 virtual PaintDebugBounds(g, scale),默认只画 Area()。 - Toolbar / TabBar 覆盖:跳过与区域重合的根控件,遍历 m_root->Children()(层级 1); TabBar 另上报每个 ✕(层级 2)。 - SignalPanel 覆盖:表头 / 筛选框 / 列表 / 样式区(1)+ 滚动条、样式行、当前可见列表行(2)。 - PlotView 覆盖:每个 LaneVisual::rect(1);不上报随数据变化的瞬态几何(曲线/胶囊/提示)。 - UiWidget 新增 PaintDebugBounds(g, scale, depth) 递归整棵控件树 —— 已上树的控件零成本获得边界。 - 宿主 PaintLayoutBounds 补客户区外框、分隔条、打开的菜单/拾色器。 视觉与实现约束(Android「Show layout bounds」风): - 只描边、不填充(几十个嵌套矩形叠加填充会把内容糊住); - 层级配色 青蓝(2px) / 品红 / 琥珀 / 绿(1px); - 线宽按设备像素固定(不乘 scale),临时 PixelOffsetModeNone + 关抗锯齿 + 整像素对齐, 保证 1px 就是 1px; - ⚠️ 只在 Render 最后一步叠加,绝不进入任何位图缓存(PlotView 4 层缓存 / SignalPanel 行位图缓存), 否则关掉开关后线条会残留; - ⚠️ fullFrame 并入 m_showLayoutBounds:开启时必须整帧回送,否则「只回送绘图区」的快路径 会让装饰区边界停在旧帧上。 ## 3. 可见收益优化(行为不变) - Toolbar::Layout:控件树改为按需重建(m_treeDirty),只在规格/字体/主题/缩放/动态文本 变化时重建。拖动面板分隔条时 Relayout 每帧调用一次 -> 不再每帧 10 次堆分配 + 8 次测量缓存失效。 - Toolbar::SetActionLabel:只写 m_labelOverrides(唯一权威)+ 置树脏,消除「覆盖表与控件文本 双写」可能不同步的问题。 - UiWidget::DispatchRedraw(event):新增;收敛 Toolbar / TabBar 里重复的 Dispatch(); return UiRectHasArea(TakeDirty());(TakeDirty 会清零,漏一次就把下一次真实变化 误判成「无变化」)。 ## 4. 验证 - make release / make debug:零 error 零 warning;TargetCheck.exe 897,024 B(< 900 KB)。 - 新增离屏探针 .preflight/lbcheck:21 / 21 PASS (齿轮在导入文件左侧/同高/更窄;开启后青蓝 24,930 px、品红 19,734 px;关闭后逐像素还原; 菜单 scope、锚定贴合按钮、宽度未被撑大、条目初始未勾选、点击翻转开关、菜单保持打开)。 - Tools/rendercheck:8 个场景的「工具栏带以下区域」哈希与改造前**逐一相同** (证明改动像素被完全限制在工具栏带内);整帧哈希因新增按钮整体变化,属预期。 FUNC 断言 7 / 7 PASS。 - Tools/uicheck:69 passed / 0 failed。 ## 5. 文档 Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第六阶段; Documents/agent_mem.md 同步变更日志与「2026-10-28 第八轮」硬约束与验证坑。
667 lines
16 KiB
C++
667 lines
16 KiB
C++
#include "UiWidget.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
#include "UiDebugBounds.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::PaintDebugBounds(gd::Graphics& g, float scale, int depth) const
|
||
{
|
||
if (!m_visible || !UiRectHasArea(m_bounds))
|
||
{
|
||
// 不可见 / 未摆放的节点没有边界可画,其子树也不会被 `Paint` 画出来 ⇒ 一并跳过。
|
||
return;
|
||
}
|
||
|
||
UiDebugPaintBounds(g, m_bounds, depth, scale);
|
||
|
||
// 子节点按 z 序递归(末尾最上层)—— 与 `Paint` 的顺序一致,重叠时上层压住下层。
|
||
for (const std::unique_ptr<UiWidget>& child : m_children)
|
||
{
|
||
if (child != nullptr)
|
||
{
|
||
child->PaintDebugBounds(g, scale, 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
|