Files
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

368 lines
9.5 KiB
C++

#include "UiLayout.hpp"
#include <algorithm>
#include <cmath>
namespace jdi
{
UiInsets UiInsets::All(float value)
{
UiInsets insets;
insets.left = value;
insets.top = value;
insets.right = value;
insets.bottom = value;
return insets;
}
UiInsets UiInsets::Symmetric(float horizontal, float vertical)
{
UiInsets insets;
insets.left = horizontal;
insets.right = horizontal;
insets.top = vertical;
insets.bottom = vertical;
return insets;
}
UiInsets UiInsets::Scaled(float scale) const
{
UiInsets insets;
insets.left = left * scale;
insets.top = top * scale;
insets.right = right * scale;
insets.bottom = bottom * scale;
return insets;
}
bool UiRectHasArea(const gd::RectF& rect)
{
return rect.Width > 0.0f && rect.Height > 0.0f;
}
bool UiRectIntersect(const gd::RectF& a, const gd::RectF& b, gd::RectF& out)
{
if (!UiRectHasArea(a) || !UiRectHasArea(b))
{
out = gd::RectF();
return false;
}
const float left = std::max(a.X, b.X);
const float top = std::max(a.Y, b.Y);
const float right = std::min(a.X + a.Width, b.X + b.Width);
const float bottom = std::min(a.Y + a.Height, b.Y + b.Height);
if (right <= left || bottom <= top)
{
out = gd::RectF();
return false;
}
out = gd::RectF(left, top, right - left, bottom - top);
return true;
}
gd::RectF UiRectUnion(const gd::RectF& a, const gd::RectF& b)
{
if (!UiRectHasArea(a))
{
return b;
}
if (!UiRectHasArea(b))
{
return a;
}
const float left = std::min(a.X, b.X);
const float top = std::min(a.Y, b.Y);
const float right = std::max(a.X + a.Width, b.X + b.Width);
const float bottom = std::max(a.Y + a.Height, b.Y + b.Height);
return gd::RectF(left, top, right - left, bottom - top);
}
gd::RectF UiRectDeflate(const gd::RectF& rect, const UiInsets& insets)
{
const float width = rect.Width - insets.Horizontal();
const float height = rect.Height - insets.Vertical();
if (width <= 0.0f || height <= 0.0f)
{
return gd::RectF();
}
return gd::RectF(rect.X + insets.left, rect.Y + insets.top, width, height);
}
gd::RectF UiRectInflate(const gd::RectF& rect, float amount)
{
if (!UiRectHasArea(rect) || amount <= 0.0f)
{
return rect;
}
return gd::RectF(rect.X - amount, rect.Y - amount, rect.Width + amount * 2.0f,
rect.Height + amount * 2.0f);
}
gd::RectF UiRectSnapInward(const gd::RectF& rect, float scale)
{
const float safeScale = (scale > 0.0f) ? scale : 1.0f;
const float x0 = std::ceil(rect.X * safeScale - 0.001f) / safeScale;
const float y0 = std::ceil(rect.Y * safeScale - 0.001f) / safeScale;
const float x1 = std::floor((rect.X + rect.Width) * safeScale + 0.001f) / safeScale;
const float y1 = std::floor((rect.Y + rect.Height) * safeScale + 0.001f) / safeScale;
if (x1 <= x0 || y1 <= y0)
{
return gd::RectF();
}
return gd::RectF(x0, y0, x1 - x0, y1 - y0);
}
gd::RectF UiAlignInRect(const gd::RectF& outer, float width, float height, UiAlign horizontal,
UiAlign vertical, float offsetX, float offsetY)
{
float boxWidth = width;
float boxHeight = height;
if (horizontal == UiAlign::STRETCH)
{
boxWidth = outer.Width;
}
if (vertical == UiAlign::STRETCH)
{
boxHeight = outer.Height;
}
boxWidth = std::min(boxWidth, outer.Width);
boxHeight = std::min(boxHeight, outer.Height);
float x = outer.X;
switch (horizontal)
{
case UiAlign::CENTER:
x = outer.X + (outer.Width - boxWidth) * 0.5f;
break;
case UiAlign::END:
x = outer.X + outer.Width - boxWidth;
break;
default:
break;
}
float y = outer.Y;
switch (vertical)
{
case UiAlign::CENTER:
y = outer.Y + (outer.Height - boxHeight) * 0.5f;
break;
case UiAlign::END:
y = outer.Y + outer.Height - boxHeight;
break;
default:
break;
}
return gd::RectF(x + offsetX, y + offsetY, boxWidth, boxHeight);
}
float UiStackExtent(const std::vector<float>& sizes, float gap)
{
float total = 0.0f;
std::size_t fixedCount = 0;
for (float size : sizes)
{
if (size >= 0.0f)
{
total += size;
++fixedCount;
}
}
if (fixedCount > 0 && sizes.size() > 1)
{
total += gap * static_cast<float>(sizes.size() - 1);
}
return total;
}
void UiStackRects(const gd::RectF& area, UiAxis axis, const std::vector<float>& sizes, float gap,
std::vector<gd::RectF>& out, float* usedExtent)
{
out.clear();
if (sizes.empty())
{
if (usedExtent != nullptr)
{
*usedExtent = 0.0f;
}
return;
}
const bool horizontal = (axis == UiAxis::HORIZONTAL);
const float totalExtent = horizontal ? area.Width : area.Height;
const float totalGap = (sizes.size() > 1) ? gap * static_cast<float>(sizes.size() - 1) : 0.0f;
const float fixedExtent = UiStackExtent(sizes, gap) - totalGap;
std::size_t autoCount = 0;
for (float size : sizes)
{
if (size < 0.0f)
{
++autoCount;
}
}
// 剩余空间不足时按 0 处理(**不允许**出现负尺寸:负宽高的矩形会画出反向的线)。
const float remaining = std::max(0.0f, totalExtent - fixedExtent - totalGap);
const float autoExtent = (autoCount > 0) ? (remaining / static_cast<float>(autoCount)) : 0.0f;
float cursor = horizontal ? area.X : area.Y;
out.reserve(sizes.size());
for (std::size_t index = 0; index < sizes.size(); ++index)
{
const float extent = (sizes[index] < 0.0f) ? autoExtent : std::max(0.0f, sizes[index]);
if (horizontal)
{
out.push_back(gd::RectF(cursor, area.Y, extent, area.Height));
}
else
{
out.push_back(gd::RectF(area.X, cursor, area.Width, extent));
}
cursor += extent;
if (index + 1 < sizes.size())
{
cursor += gap;
}
}
if (usedExtent != nullptr)
{
*usedExtent = cursor - (horizontal ? area.X : area.Y);
}
}
float UiFlowRects(const gd::RectF& area, UiAxis axis, const std::vector<gd::SizeF>& sizes, float gap,
std::vector<gd::RectF>& out, std::size_t& overflow)
{
out.clear();
overflow = sizes.size();
if (sizes.empty())
{
return 0.0f;
}
const bool horizontal = (axis == UiAxis::HORIZONTAL);
const float mainExtent = horizontal ? area.Width : area.Height;
const float crossExtent = horizontal ? area.Height : area.Width;
float main = 0.0f; // 当前行内已用主轴长度
float cross = 0.0f; // 当前行起点(交叉轴)
float lineCross = 0.0f; // 当前行最大交叉尺寸
out.reserve(sizes.size());
for (std::size_t index = 0; index < sizes.size(); ++index)
{
const float itemMain = horizontal ? sizes[index].Width : sizes[index].Height;
const float itemCross = horizontal ? sizes[index].Height : sizes[index].Width;
const float needed = (main > 0.0f) ? (main + gap + itemMain) : itemMain;
if (needed > mainExtent && main > 0.0f)
{
// 换行
cross += lineCross + gap;
main = 0.0f;
lineCross = 0.0f;
}
if (itemMain > mainExtent || cross + itemCross > crossExtent)
{
overflow = index;
break;
}
const float offset = (main > 0.0f) ? (main + gap) : 0.0f;
const float x = horizontal ? (area.X + offset) : (area.X + cross);
const float y = horizontal ? (area.Y + cross) : (area.Y + offset);
out.push_back(gd::RectF(x, y, horizontal ? itemMain : itemCross,
horizontal ? itemCross : itemMain));
main += offset + itemMain;
lineCross = std::max(lineCross, itemCross);
}
return cross + lineCross;
}
gd::RectF UiAbsRect(const gd::RectF& outer, float dx, float dy, float width, float height)
{
return gd::RectF(outer.X + dx, outer.Y + dy, std::max(0.0f, width), std::max(0.0f, height));
}
std::uint64_t UiMixKey(std::uint64_t seed, std::uint64_t value)
{
// FNV-1a 64 位的一份变体:与项目里其它指纹(SceneContentHash 等)同一风格。
seed ^= value + 0x9E3779B97F4A7C15ull + (seed << 6) + (seed >> 2);
return seed;
}
std::uint64_t UiMixKeyFloat(std::uint64_t seed, float value)
{
// 量化到 1/1024 逻辑像素:亚像素抖动不应该让布局缓存反复失效。
const long long quantized = static_cast<long long>(std::llround(value * 1024.0f));
return UiMixKey(seed, static_cast<std::uint64_t>(quantized));
}
UiLayoutCache::UiLayoutCache(std::size_t capacity)
: m_capacity(std::max<std::size_t>(1, capacity))
{
}
const UiLayoutResult* UiLayoutCache::Find(std::uint64_t key)
{
for (UiLayoutResult& entry : m_entries)
{
if (entry.key == key)
{
entry.stamp = ++m_stamp;
return &entry;
}
}
return nullptr;
}
const UiLayoutResult& UiLayoutCache::Store(UiLayoutResult result)
{
for (UiLayoutResult& entry : m_entries)
{
if (entry.key == result.key)
{
entry = std::move(result);
entry.stamp = ++m_stamp;
return entry;
}
}
result.stamp = ++m_stamp;
m_entries.push_back(std::move(result));
Trim();
// 淘汰只会从队首进行,而新条目在队尾 ⇒ 刚存入的条目一定仍然有效。
return m_entries.back();
}
void UiLayoutCache::Invalidate(std::uint64_t key)
{
for (auto iterator = m_entries.begin(); iterator != m_entries.end(); ++iterator)
{
if (iterator->key == key)
{
m_entries.erase(iterator);
return;
}
}
}
void UiLayoutCache::Clear()
{
m_entries.clear();
}
void UiLayoutCache::Trim()
{
while (m_entries.size() > m_capacity)
{
// 淘汰"最久未被命中"的条目(stamp 最小)。
auto oldest = m_entries.begin();
for (auto iterator = m_entries.begin(); iterator != m_entries.end(); ++iterator)
{
if (iterator->stamp < oldest->stamp)
{
oldest = iterator;
}
}
m_entries.erase(oldest);
}
}
} // namespace jdi