新增 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。
626 lines
15 KiB
C++
626 lines
15 KiB
C++
#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
|