TabBar 的标签 / 「+」/ 覆盖层拆成控件,手势语义原样保留(自行判双击、3px 拖拽阈值、
关闭 ✕ 的「移开取消」、拖拽排序的 drop 位置),公开 API 未变:
- 新增(TabBar.cpp 内实现的内部控件)
* UiTabItemWidget:单格标签的绘制(底色/描边/激活指示条/文本或重命名输入框/关闭 ✕),
关闭按钮热区由基类的悬停点自行判定。
* UiAddWidget:「+」按钮绘制与悬停配色。
* UiOverlayWidget:命中透明的覆盖层,最后绘制「拖拽插入指示条 + 底部分隔线」
(必须晚于标签与「+」,否则会被它们压住)。
* UiTabRootWidget:只铺标签栏底色。
- TabBar 移除 m_hoverIndex / m_hoverClose / m_hoverAdd 三个手工下标,
改为 dispatch MOUSE_MOVE 后由 root->TakeDirty() 判断是否需要重绘。
- 绘制顺序保持:底色 → 各标签 → 「+」→ 拖拽指示条 → 底部分隔线(与旧实现逐像素一致)。
- UiWidget 新增 HoverPoint(x,y):派生控件据此自行判断「悬停在自身内部哪个热区」。
验证:
- 像素:8 场景哈希与迁移前逐一相同(A 2166933…、H 23F390D…)。
- 交互断言(全部 PASS):
* tabbar-hover-marks-dirty (1,0) —— 进入标签只标记一次脏区;
* tabbar-add-creates-tab (1 -> 2) —— 「+」新建标签;
* tabbar-click-selects-tab (active=0) —— 点击标签切换激活页;
* tabbar-close-removes-tab (2 -> 1) —— 点击 ✕ 关闭标签。
- Tools/uicheck 69 passed / 0 failed。
- make release / make debug 零警告;Target.exe 886,784 B(< 900KB)。
639 lines
15 KiB
C++
639 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->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
|