Files
Just-DataInspector/Components/Shared/Src/UiWidget.cpp
T
DeepSeek V4.1 Flash 25264f9074 UI 重构 P3:Toolbar 迁移到控件树(新增 UiButtonWidget,逐像素 0 差异 + 功能断言)
新增 Components/Shared/{Inc,Src}/UiButtonWidget.*:通用「文字按钮」控件,视觉规则与旧 Toolbar
按钮逐像素一致(主按钮强调色实底 / 普通按钮 1px 描边 / 可切换选中态 / 禁用态配色);
交互由控件自己处理:按下即 CaptureMouse 进入按下态,在自身范围内松开才触发 onClick,
指针离开(不会再收到抬起)时 CancelPress。

Toolbar 内部重写,公开 API 一字未改:
- Button 只保留静态配置 + 控件指针;矩形与外观状态归控件。
- 删除 m_hoverIndex / m_pressedIndex 与死字段 Button::hover,改由 UiWidget 的悬停链与 capture 维护。
- Paint 改为「根控件(底色 + 底部分隔线)+ 按钮子节点」,一次 root->Paint(ctx)。
- OnMouseMove/Down/Up/Leave 统一翻译成 UiEvent 后 root->Dispatch;动作经 onClick 回调登记、抬起后执行。

框架约定调整:UiWidget::Paint 的裁剪从「严格等于自身矩形」改为「自身矩形外扩 1px(kUiClipBleed)」。
边框/描边以矩形边界为中心绘制,严格裁剪会切掉外半侧使控件变细、与旧绘制不再像素一致。
uicheck 对应断言改为「裁剪 ⊇ 自身矩形且不超出外扩范围」。

验证:
- make release / make debug 零警告;Target.exe 881,664 B(< 900KB)。
- 逐像素 A/B(8 场景:全览 / 深放大+自由标记+滤波+弱峰+光标 / 平移 / 悬停按钮 / 按下 / 松开 /
  悬停主按钮 / 移出工具栏):迁移前后哈希逐一相同
  A 2166933039626F66 B C444ECFEB18FD73D C 7984446D8E7A4418 D BF855879CA12F289
  E 1C663E4118616016 F 0535292D8E07AE18 G 7A58172C46577729 H 23F390DD76141113。
- 功能断言:locate-cursor-button PASS (1291,73);click-cursor-changes-mode PASS (1 -> 2);
  release-outside-does-not-fire PASS (2 -> 2)。
- Tools/uicheck 62 passed / 0 failed。
2026-10-28 12:29:08 +08:00

626 lines
15 KiB
C++
Raw 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>
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);
}
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->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;
}
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