Files
DeepSeek V4.1 Flash 24706c906a feat(UI): 布局边界改为四角粗/四边细包角 + 全元素覆盖 + 几何同源(RowRect O(1))
## 1. 边界观感:四角粗、四边细

用户反馈"边框就是实心矩形、粗细不一,效果不佳"。旧实现是四条等宽实心矩形
(层级 0 用 2px、其余 1px),同一元素的角与边粗细不一致,父子矩形共边时还会叠成更粗的一条。

新规则:
- 四条边固定 1 设备像素(四条独立实心矩形;不用 DrawRectangle,笔宽奇偶会偏半像素);
- 四个角额外加粗:层级 0 = 4px、其余 3px,沿水平/垂直各延伸 cornerLen(≤14px 且 ≤ 短边一半);
- 层级 > 0 整体内缩 1px,父子矩形共边不再叠粗;
- 绝不填充(几十层嵌套填充会把内容糊住);
- 整像素对齐 + PixelOffsetModeNone + 关抗锯齿。

四角粗不只是观感:多条边重合时靠角的形状分辨层次(父=青蓝大包角、子=同位置品红小包角)。

## 2. 协议:组件"收集 box",宿主统一绘制

UiComponent::CollectDebugBoxes(UiDebugBoxList&)  默认只登记 Area()
UiWidget::CollectDebugBoxes(boxes, depth)        递归整棵控件树
Application::PaintLayoutBounds                   收集后按 depth 升序一次性描边

刻意保持 box 为**值类型**而不是让它继承控件基类:PlotView 的 4 层位图缓存、
SignalPanel 的 192 槽行位图缓存都要求"绘制由组件自己负责",几何进继承体系会与
缓存键/失效体系打架。box 不参与绘制,因此能让绘制/命中/调试共用同一份几何。

## 3. 覆盖补齐(此前大量元素被裹在外层单位里)

- Toolbar:区域 / 每个按钮 / **按钮内容(文字或齿轮图标,新增 UiButtonWidget::ContentRect())**
- TabBar:区域 / 标签条 / 新建按钮 / 每个 ✕
- SignalPanel:4 分区 + 筛选输入框/清空✕/全部显示 + 滚动轨道与滑块
  + 样式区 5 行及其 12 色块/自定义/3 模式/3 调整/线宽与透明度轨道和数值
  + 目标行/名称/GX 徽标 + **每个可见行**的折起三角/复选框/色块/名称/GX 徽标/✕
  (组行另有组名/计数)+ 内联编辑框
- PlotView:区域 / 每个网格 / GX 徽标 / 装饰层矩形 / 图例托底+色块+文本
  / **5 类浮动面板全部 20+ 字段**(标题/✕/折起三角/徽标/编辑行/按钮/内容区/滚动轨道与滑块
  /透明度轨道与数值/两个开关/各行复选框与数值与滑轨与复位与名称与✕/分组栏/表头/固定点行)
- PopupMenu:浮层 / 每个条目 / 可滚动可视区 / 轨道 / 滑块 / 二级子菜单与条目
- ColorPickerPopup:面板 / 标题 / 预览 / 色号 / 色域 / 色相条 / 3 条滑轨与数值 / 输入框

标签增加**尺寸门槛**(< 56×16 逻辑像素不画):复选框/色块/✕ 紧邻,逐个小框贴字会糊成一片。

## 4. 设置按钮配色 + 齿轮图标

- ToolbarAction::SETTINGS 改为 primary = true,与「导入文件」同为强调色主按钮。
- 齿轮几何重做:齿占最外一圈(0.66R→1.0R)、外环中心线 0.54R/笔宽 0.24R、轴孔 0.17R。
  旧版让齿从环中心线伸出,齿被环的描边吃掉 ⇒ 看起来只剩一个"甜甜圈"。

## 5. 几何同源与性能

- RowRect(row) 原本每次调用都从头累加行高(O(row)),而画一行要调它 **5 次**
  (✕/徽标/名称/组计数/折起三角各一次)⇒ 200 行只见 30 行时每帧上万次加法,
  命中测试(每次鼠标移动)同样走这条路。
  新增 m_rowTop 前缀和(由 UpdateAnimGeometry() 统一重建)⇒ **O(1)**;
  长度与行集合不一致时自动退回逐行累加(正确性优先)。
- ContentOffsetBeforeRow 同样改用前缀和。
- PanelBounds() 收敛 6 处重复的 (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area。

## 6. 验证

- make release / make debug 零 error 零 warning;TargetCheck.exe 910,336 B(889 KiB < 900 KiB)。
- .preflight/lbcheck:21 / 21 PASS(含 bounds-off-restores-pixels:关闭后逐像素回到开启前)。
- Tools/rendercheck:「工具栏带以下」区域哈希 8/8 与上一轮**逐一相同**
  (证明本轮改动只落在工具栏带内);FUNC 断言 7/7 PASS。
- Tools/uicheck:69 passed / 0 failed。

## 7. 文档

Documents/UI_Refactor_Plan.md / UI_Refactor_Progress.md / UI_Refactor_Report.md 追加第七阶段;
Documents/agent_mem.md 同步变更日志与「2026-10-28 第九轮」硬规则与踩坑记录。
2026-10-28 16:28:57 +08:00

113 lines
4.4 KiB
C++
Raw Permalink 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.
#ifndef __UI_BUTTON_WIDGET_HPP__
#define __UI_BUTTON_WIDGET_HPP__
#include <functional>
#include <string>
#include "UiWidget.hpp"
namespace jdi
{
/**
* @brief 按钮图标(非 `NONE` 时按钮**只画图标**,忽略文本)。
*
* 图标用矢量绘制(不用字体里的符号):`⚙` 这类字符在中文/西文回退字体里
* 宽高与基线各不相同,会出现"图标忽大忽小、垂直不居中"的问题。
*/
enum class UiButtonIcon : int
{
NONE = 0, /**< 无图标(只画文本)。 */
GEAR = 1 /**< 齿轮:设置菜单入口。 */
};
/**
* @brief 一个"文字按钮"控件(工具栏按钮的原语)。
*
* 视觉规则与旧 `Toolbar` 的按钮**逐像素一致**:
* * 主按钮(`primary`)= 强调色实底 + `textOnAccent` 文字,按下 / 悬停时整体提亮或压暗;
* * 普通按钮 = 透明底 + 1px 描边,可切换按钮选中时用 `accentSoft` 底 + `accent` 描边;
* * 禁用时底色回退为工具栏底色、文字用 `textMuted`、描边与底色混 50%。
*
* 交互:按下即 `CaptureMouse()` 并进入按下态;**在自身范围内**松开才触发 `onClick`
*(与"按住移开再松开 = 放弃"的既有语义一致)。
*/
class UiButtonWidget : public UiWidget
{
public:
UiButtonWidget() = default;
/** @brief 一次性注入绘制资源与外观参数(尺寸由 `OnMeasure` 按文本实测得出)。 */
void Configure(const UiFonts* fonts, const Theme* theme, float scale, std::wstring label,
bool primary, bool toggle, float textPadding, float height);
/** @brief 设置图标(`NONE` 表示回到纯文本按钮)。 */
void SetIcon(UiButtonIcon icon);
/** @brief 当前图标。 */
UiButtonIcon Icon() const { return m_icon; }
/** @brief 修改文本(宽度会在下一次 `InvalidateMeasure` 后重新实测)。 */
void SetLabel(std::wstring label);
/** @brief 文本(只读)。 */
const std::wstring& Label() const { return m_label; }
/** @brief 设置可切换按钮的选中态。 */
void SetChecked(bool checked);
/** @brief 选中态。 */
bool Checked() const { return m_checked; }
/** @brief 是否可切换按钮。 */
bool IsToggle() const { return m_toggle; }
/** @brief 设置可用性(禁用时不可点、不可命中)。 */
void SetEnabled(bool enabled);
/** @brief 是否可用。 */
bool Enabled() const { return m_enabled; }
/** @brief 是否处于按下态(按下未松开)。 */
bool Pressed() const { return m_pressed; }
/** @brief 放弃当前按下(指针离开窗口等不会再收到抬起的情形)。 */
void CancelPress();
/**
* @brief 按钮**内容**(文本或图标)实际占用的矩形(客户区坐标)。
*
* 内容按居中绘制,因此它的矩形与按钮矩形不同:调试层用它显示"文字/图标本身的边界"。
* 未摆放或文本为空时返回空矩形。
*
* @return 内容矩形(客户区坐标)。
*/
gd::RectF ContentRect() const;
/** @brief 点击回调(在自身范围内松开时触发)。 */
std::function<void()> onClick;
/** @brief 工具栏底色(普通按钮的底色与禁用配色都以它为基准)。 */
void SetCanvasColor(std::uint32_t color) { m_canvasColor = color; }
protected:
gd::SizeF OnMeasure(const UiMeasureContext& ctx) override;
void OnPaint(UiPaintContext& ctx) override;
UiEventResult OnEvent(UiEvent& event) override;
HCURSOR OnQueryCursor(int x, int y) override;
std::uint64_t ContentKey() const override;
private:
const UiFonts* m_fonts = nullptr; /**< 字体。 */
const Theme* m_theme = nullptr; /**< 主题。 */
float m_scale = 1.0f; /**< 缩放系数。 */
std::wstring m_label; /**< 按钮文本。 */
UiButtonIcon m_icon = UiButtonIcon::NONE; /**< 按钮图标(非 `NONE` 时忽略文本)。 */
bool m_primary = false; /**< 主按钮(强调色实底)。 */
bool m_toggle = false; /**< 是否可切换。 */
bool m_checked = false; /**< 可切换按钮的选中态。 */
bool m_enabled = true; /**< 是否可用。 */
bool m_pressed = false; /**< 按下态。 */
float m_textPadding = 14.0f; /**< 左右内边距(逻辑像素)。 */
float m_height = 30.0f; /**< 按钮高度(逻辑像素)。 */
std::uint32_t m_canvasColor = 0; /**< 工具栏底色(配色基准)。 */
};
} // namespace jdi
#endif // __UI_BUTTON_WIDGET_HPP__