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。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-28 12:29:08 +08:00
1 parent 7e1852c2d2
commit 25264f9074
9 files changed
+663 -223

No files matched your search

+18 -10
View File
@@ -5,11 +5,15 @@
#include <objidl.h>
#include <gdiplus.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "PaintUtil.hpp"
#include "Theme.hpp"
#include "UiButtonWidget.hpp"
#include "UiWidget.hpp"
namespace jdi
{
@@ -37,6 +41,7 @@ namespace jdi
{
public:
Toolbar();
~Toolbar();
/**
* @brief 绑定字体与主题资源。
@@ -135,19 +140,17 @@ namespace jdi
private:
/**
* @brief 单个工具栏按钮的运行时状态。
* @brief 单个工具栏按钮的**静态配置**(矩形与外观状态由控件自己维护)。
*/
struct Button
{
ToolbarAction action = ToolbarAction::NONE; /**< 按钮动作。 */
std::wstring label; /**< 按钮文本。 */
gd::RectF rect; /**< 按钮矩形。 */
float width = 0.0f; /**< 按钮宽度。 */
UiButtonWidget* widget = nullptr; /**< 对应控件(由 `m_root` 拥有)。 */
bool primary = false; /**< 是否为主按钮(强调色填充)。 */
bool needsDocument = true; /**< 是否依赖已加载数据。 */ bool toggle = false; /**< 是否为可切换按钮。 */
bool needsDocument = true; /**< 是否依赖已加载数据。 */
bool toggle = false; /**< 是否为可切换按钮。 */
bool checked = false; /**< 可切换按钮的选中状态。 */
bool hover = false; /**< 悬停状态(当前由 `Toolbar::m_hoverIndex` 派生,字段保留供未来使用)。 */
bool pressed = false; /**< 按下状态。 */
float width = 0.0f; /**< 按钮宽度(由控件实测得出)。 */
};
/**
@@ -157,15 +160,20 @@ namespace jdi
*/
bool IsEnabled(const Button& button) const;
/** @brief 按固定规格表重建控件树(文本变化/首次布局时调用)。 */
void RebuildWidgets();
/** @brief 把可用性同步到控件(未加载数据时禁用依赖数据的按钮)。 */
void SyncEnabledState();
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
const Theme* m_theme = nullptr; /**< 主题资源。 */
float m_scale = 1.0f; /**< DPI 缩放比例。 */
bool m_documentLoaded = false; /**< 是否已加载数据。 */
std::vector<Button> m_buttons; /**< 按钮集合。 */
std::vector<Button> m_buttons; /**< 按钮配置集合。 */
std::vector<std::pair<ToolbarAction, std::wstring>> m_labelOverrides; /**< 动态文本覆盖表。 */
gd::RectF m_area; /**< 工具栏整体区域。 */
int m_hoverIndex = -1; /**< 当前悬停按钮下标。 */
int m_pressedIndex = -1; /**< 当前按下按钮下标。 */
std::unique_ptr<UiWidget> m_root; /**< 控件树根(负责底色与底部分隔线)。 */
ToolbarAction m_pendingAction = ToolbarAction::NONE; /**< 最近一次成功点击的动作。 */
};
} // namespace jdi