## 1. 边界观感:四角粗、四边细 用户反馈"边框就是实心矩形、粗细不一,效果不佳"。旧实现是四条等宽实心矩形 (层级 0 用 2px、其余 1px),同一元素的角与边粗细不一致,父子矩形共边时还会叠成更粗的一条。 新规则: - 四条边固定 1 设备像素(四条独立实心矩形;不用 DrawRectangle,笔宽奇偶会偏半像素); - 四个角额外加粗:层级 0 = 4px、其余 3px,沿水平/垂直各延伸 cornerLen(≤14px 且 ≤ 短边一半); - 层级 > 0 整体内缩 1px,父子矩形共边不再叠粗; - 绝不填充(几十层嵌套填充会把内容糊住); - 整像素对齐 + PixelOffsetModeNone + 关抗锯齿。 四角粗不只是观感:多条边重合时靠角的形状分辨层次(父=青蓝大包角、子=同位置品红小包角)。 ## 2. 协议:组件"收集 box",宿主统一绘制 UiComponent::CollectDebugBoxes(UiDebugBoxList&) 默认只登记 Area() UiWidget::CollectDebugBoxes(boxes, depth) 递归整棵控件树 Application::PaintLayoutBounds 收集后按 depth 升序一次性描边 刻意保持 box 为**值类型**而不是让它继承控件基类:PlotView 的 4 层位图缓存、 SignalPanel 的 192 槽行位图缓存都要求"绘制由组件自己负责",几何进继承体系会与 缓存键/失效体系打架。box 不参与绘制,因此能让绘制/命中/调试共用同一份几何。 ## 3. 覆盖补齐(此前大量元素被裹在外层单位里) - Toolbar:区域 / 每个按钮 / **按钮内容(文字或齿轮图标,新增 UiButtonWidget::ContentRect())** - TabBar:区域 / 标签条 / 新建按钮 / 每个 ✕ - SignalPanel:4 分区 + 筛选输入框/清空✕/全部显示 + 滚动轨道与滑块 + 样式区 5 行及其 12 色块/自定义/3 模式/3 调整/线宽与透明度轨道和数值 + 目标行/名称/GX 徽标 + **每个可见行**的折起三角/复选框/色块/名称/GX 徽标/✕ (组行另有组名/计数)+ 内联编辑框 - PlotView:区域 / 每个网格 / GX 徽标 / 装饰层矩形 / 图例托底+色块+文本 / **5 类浮动面板全部 20+ 字段**(标题/✕/折起三角/徽标/编辑行/按钮/内容区/滚动轨道与滑块 /透明度轨道与数值/两个开关/各行复选框与数值与滑轨与复位与名称与✕/分组栏/表头/固定点行) - PopupMenu:浮层 / 每个条目 / 可滚动可视区 / 轨道 / 滑块 / 二级子菜单与条目 - ColorPickerPopup:面板 / 标题 / 预览 / 色号 / 色域 / 色相条 / 3 条滑轨与数值 / 输入框 标签增加**尺寸门槛**(< 56×16 逻辑像素不画):复选框/色块/✕ 紧邻,逐个小框贴字会糊成一片。 ## 4. 设置按钮配色 + 齿轮图标 - ToolbarAction::SETTINGS 改为 primary = true,与「导入文件」同为强调色主按钮。 - 齿轮几何重做:齿占最外一圈(0.66R→1.0R)、外环中心线 0.54R/笔宽 0.24R、轴孔 0.17R。 旧版让齿从环中心线伸出,齿被环的描边吃掉 ⇒ 看起来只剩一个"甜甜圈"。 ## 5. 几何同源与性能 - RowRect(row) 原本每次调用都从头累加行高(O(row)),而画一行要调它 **5 次** (✕/徽标/名称/组计数/折起三角各一次)⇒ 200 行只见 30 行时每帧上万次加法, 命中测试(每次鼠标移动)同样走这条路。 新增 m_rowTop 前缀和(由 UpdateAnimGeometry() 统一重建)⇒ **O(1)**; 长度与行集合不一致时自动退回逐行累加(正确性优先)。 - ContentOffsetBeforeRow 同样改用前缀和。 - PanelBounds() 收敛 6 处重复的 (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area。 ## 6. 验证 - make release / make debug 零 error 零 warning;TargetCheck.exe 910,336 B(889 KiB < 900 KiB)。 - .preflight/lbcheck:21 / 21 PASS(含 bounds-off-restores-pixels:关闭后逐像素回到开启前)。 - Tools/rendercheck:「工具栏带以下」区域哈希 8/8 与上一轮**逐一相同** (证明本轮改动只落在工具栏带内);FUNC 断言 7/7 PASS。 - Tools/uicheck:69 passed / 0 failed。 ## 7. 文档 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::CollectDebugBoxes(UiDebugBoxList& 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->CollectDebugBoxes(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
|