新增 Components/Shared 下的 UI 基础层: - UiLayout:矩形原语 / 对齐 / 设备像素内侧对齐 / Stack / Flow / 布局缓存(deque 保证引用稳定、 定向失效、按最久未命中淘汰)。 - UiWidget:控件树基类。两段式布局(Measure 自下而上带缓存、Arrange 自上而下)、 脏区下探绘制(dirty 为空=全画;与子树矩形无交集=整棵跳过;有效裁剪=祖先裁剪切∩自身∩脏区)、 只沿命中链派发的冒泡模型(绝不分发给同层兄弟)、capture/modal/hover/focus 仲裁、 光标认领链、统一动画 tick、内容指纹入口。 新增 Tools/uicheck 离屏回归网(62 项断言,全程不创建窗口、不注入输入): - 布局数学:对齐/STRETCH/超尺寸夹取、Stack 固定与自动均分且绝不产生负尺寸、Flow 换行与 overflow。 - 缓存:引用稳定、定向失效、容量淘汰。 - 绘制:全画 / 子树跳过 / 裁剪收窄 / 不可见跳过 / 失效冒泡。 - 派发:重叠子节点只投最上层、hit-transparent 穿透、消费不上抛、capture 越过边界仍投递、 仅持有者可释放、modal 屏蔽其余子树、祖先兜底、光标由最上层认领、键盘沿焦点链。 - 动画:时间戳统一下发、活跃状态汇总。 过程中修正自身两处缺陷:补齐 AdvanceAnimationSelf/AnimationActiveSelf 声明; 把"遍历同层兄弟"改为"只沿命中链派发 + 祖先兜底"(否则一次点击可能被视觉上位于下方的控件也处理)。 验证:make release TARGET_FILE_NAME=TargetCheck 零警告(873,984 B,低于 900KB 阈值); Tools/uicheck 62 passed / 0 failed。 本提交不改动任何现有组件,Toolbar/TabBar/SignalPanel/PlotView 仍走旧路径,产品像素零变化。 文档:Documents/UI_Refactor_Plan.md(蓝图)、Documents/UI_Refactor_Progress.md(台账)。
221 lines
8.5 KiB
C++
221 lines
8.5 KiB
C++
#ifndef __UI_LAYOUT_HPP__
|
||
#define __UI_LAYOUT_HPP__
|
||
|
||
#include <windows.h>
|
||
#include <objidl.h>
|
||
#include <gdiplus.h>
|
||
|
||
#include <cstdint>
|
||
#include <cstddef>
|
||
#include <deque>
|
||
#include <vector>
|
||
|
||
#include "Theme.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
/**
|
||
* @brief 布局方向。
|
||
*/
|
||
enum class UiAxis : int
|
||
{
|
||
HORIZONTAL = 0, /**< 主轴为水平(从左到右排列)。 */
|
||
VERTICAL = 1 /**< 主轴为垂直(从上到下排列)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 在容器中的对齐方式(单一方向)。
|
||
*
|
||
* `STRETCH` = 占满该方向可用空间(尺寸由容器决定,忽略自身期望尺寸)。
|
||
*/
|
||
enum class UiAlign : int
|
||
{
|
||
BEGIN = 0, /**< 起始边(左 / 上)。 */
|
||
CENTER = 1, /**< 居中。 */
|
||
END = 2, /**< 结束边(右 / 下)。 */
|
||
STRETCH = 3 /**< 拉伸占满。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 四边内边距(逻辑像素,使用时由调用方乘缩放系数)。
|
||
*/
|
||
struct UiInsets
|
||
{
|
||
float left = 0.0f; /**< 左内边距。 */
|
||
float top = 0.0f; /**< 上内边距。 */
|
||
float right = 0.0f; /**< 右内边距。 */
|
||
float bottom = 0.0f; /**< 下内边距。 */
|
||
|
||
/** @brief 四边同值。 */
|
||
static UiInsets All(float value);
|
||
/** @brief 左右 / 上下分别指定。 */
|
||
static UiInsets Symmetric(float horizontal, float vertical);
|
||
/** @brief 按缩放系数放大(DPR 1.25 等)。 */
|
||
UiInsets Scaled(float scale) const;
|
||
/** @brief 水平方向占用量。 */
|
||
float Horizontal() const { return left + right; }
|
||
/** @brief 垂直方向占用量。 */
|
||
float Vertical() const { return top + bottom; }
|
||
};
|
||
|
||
// ------------------------------------------------------------------
|
||
// 矩形原语
|
||
// ------------------------------------------------------------------
|
||
|
||
/** @brief 矩形是否有面积(宽度与高度都 > 0)。 */
|
||
bool UiRectHasArea(const gd::RectF& rect);
|
||
|
||
/** @brief 两个矩形的交集;无交集时输出空矩形并返回 false。 */
|
||
bool UiRectIntersect(const gd::RectF& a, const gd::RectF& b, gd::RectF& out);
|
||
|
||
/** @brief 两个矩形的并集(任一为空时直接返回另一个)。 */
|
||
gd::RectF UiRectUnion(const gd::RectF& a, const gd::RectF& b);
|
||
|
||
/** @brief 内缩(结果可能为空矩形)。 */
|
||
gd::RectF UiRectDeflate(const gd::RectF& rect, const UiInsets& insets);
|
||
|
||
/**
|
||
* @brief 把矩形对齐到**设备像素整数边界**(上界 `floor`、下界 `ceil`)。
|
||
*
|
||
* ⚠️ 离屏位图(图层 / 装饰层 / 面板层)必须用这个结果做贴图源矩形:
|
||
* 直接对浮点矩形取整会让补丁多算一行,从而盖到相邻网格的分界线上(历史 1px 接缝 BUG)。
|
||
*/
|
||
gd::RectF UiRectSnapInward(const gd::RectF& rect, float scale);
|
||
|
||
/** @brief 把一个 `size` 的盒子按对齐方式放进 `outer`(`STRETCH` 时返回 `outer` 本身)。 */
|
||
gd::RectF UiAlignInRect(const gd::RectF& outer, float width, float height, UiAlign horizontal,
|
||
UiAlign vertical, float offsetX = 0.0f, float offsetY = 0.0f);
|
||
|
||
// ------------------------------------------------------------------
|
||
// 布局:Stack(一维流)/ Flow(换行流)/ Abs(绝对)
|
||
// ------------------------------------------------------------------
|
||
|
||
/** @brief 尺寸未知(由剩余空间均分)。 */
|
||
constexpr float kUiSizeAuto = -1.0f;
|
||
|
||
/**
|
||
* @brief 一维栈布局的**期望尺寸**。
|
||
* @param [IN] sizes 各项主尺寸(`kUiSizeAuto` 表示由剩余空间均分,不参与求和)。
|
||
* @param [IN] gap 项间距。
|
||
* @return 主尺寸合计(含间距)。
|
||
*/
|
||
float UiStackExtent(const std::vector<float>& sizes, float gap);
|
||
|
||
/**
|
||
* @brief 一维栈布局:把 `area` 沿主轴依次切分给各项。
|
||
*
|
||
* * 固定尺寸项按给定值取用;
|
||
* * `kUiSizeAuto` 项均分**剩余**空间(剩余为负时按 0 处理,避免负尺寸);
|
||
* * 交叉轴一律占满 `area`(需要单独对齐时调用方再 `UiAlignInRect`)。
|
||
*
|
||
* @param [IN] area 可用区域。
|
||
* @param [IN] axis 主轴方向。
|
||
* @param [IN] sizes 各项主尺寸(可含 `kUiSizeAuto`)。
|
||
* @param [IN] gap 项间距。
|
||
* @param [OUT] out 逐项矩形(数量与 `sizes` 一致)。
|
||
* @param [OUT] usedExtent 实际占用主尺寸(含间距),可为空。
|
||
*/
|
||
void UiStackRects(const gd::RectF& area, UiAxis axis, const std::vector<float>& sizes, float gap,
|
||
std::vector<gd::RectF>& out, float* usedExtent = nullptr);
|
||
|
||
/**
|
||
* @brief 换行流布局(图例条目、工具栏按钮组)。
|
||
*
|
||
* 沿主轴依次摆放,放不下时**换到下一行/列**;放不下的项(含之后的所有项)
|
||
* 不再输出,其下标通过 `overflow` 返回 —— 调用方据此绘制 `+N` 之类的省略提示。
|
||
*
|
||
* @param [IN] area 可用区域。
|
||
* @param [IN] axis 主轴方向(通常是水平)。
|
||
* @param [IN] sizes 各项 (宽, 高)。
|
||
* @param [IN] gap 项间距(主轴与交叉轴相同)。
|
||
* @param [OUT] out 已放下的项矩形。
|
||
* @param [OUT] overflow 首个未放下的项下标;全部放下时为 `sizes.size()`。
|
||
* @return 实际占用的交叉轴尺寸(行数 × 行高)。
|
||
*/
|
||
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);
|
||
|
||
/** @brief 绝对布局:`outer` 左上角偏移 `(dx, dy)` 后取 `width × height`。 */
|
||
gd::RectF UiAbsRect(const gd::RectF& outer, float dx, float dy, float width, float height);
|
||
|
||
// ------------------------------------------------------------------
|
||
// 布局缓存
|
||
// ------------------------------------------------------------------
|
||
|
||
/**
|
||
* @brief 一份被缓存下来的布局结果。
|
||
*
|
||
* `rects` / `values` 的含义由调用方约定(例如浮动面板的"标题栏 / ✕ / 徽标 / 逐行矩形")。
|
||
*/
|
||
struct UiLayoutResult
|
||
{
|
||
std::uint64_t key = 0; /**< 布局键(由调用方构造,必须覆盖**全部**影响几何的输入)。 */
|
||
gd::RectF bounds; /**< 整体矩形。 */
|
||
std::vector<gd::RectF> rects; /**< 逐项矩形。 */
|
||
std::vector<float> values; /**< 附带数值(列宽 / 行高 / 内容总高等)。 */
|
||
std::uint64_t stamp = 0; /**< 最近一次命中序号(用于淘汰最久未用项)。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 布局结果缓存(把"每帧被反复询问的几何"降到只算一次)。
|
||
*
|
||
* 设计要点:
|
||
* * 用 `std::deque` 存条目 ⇒ `Store` 之后**已有元素引用依然有效**(`vector` 扩容会失效);
|
||
* * 条目数量很小(面板种类 × 网格数),线性查找比哈希更快,也避免哈希碰撞带来的诡异行为;
|
||
* * **定向失效**:几何改变的那条路径必须显式 `Invalidate(key)`,
|
||
* **禁止**为了让缓存命中率好看而整体 `Clear()`(会让所有面板在同一帧全部重算)。
|
||
*/
|
||
class UiLayoutCache
|
||
{
|
||
public:
|
||
explicit UiLayoutCache(std::size_t capacity = 32);
|
||
|
||
/**
|
||
* @brief 查找命中项。
|
||
* @param [IN] key 布局键。
|
||
* @return 命中返回条目指针(**内容不可修改**),未命中返回 nullptr。
|
||
* @note 命中会刷新该条目的使用序号(因此本函数不是 `const`)。
|
||
*/
|
||
const UiLayoutResult* Find(std::uint64_t key);
|
||
|
||
/**
|
||
* @brief 存入(同键覆盖)并返回稳定引用。
|
||
* @param [IN] result 待存条目(`key` 必须已填)。
|
||
* @return 缓存内条目的引用(有效期到下一次 `Store` 引发淘汰为止)。
|
||
*/
|
||
const UiLayoutResult& Store(UiLayoutResult result);
|
||
|
||
/** @brief 定向失效。 */
|
||
void Invalidate(std::uint64_t key);
|
||
|
||
/** @brief 全部清空(只在"数据层结构变化"这类场景使用)。 */
|
||
void Clear();
|
||
|
||
/** @brief 当前条目数(探针用)。 */
|
||
std::size_t Size() const { return m_entries.size(); }
|
||
|
||
private:
|
||
std::size_t m_capacity; /**< 条目上限。 */
|
||
std::uint64_t m_stamp = 0; /**< 递增使用序号。 */
|
||
std::deque<UiLayoutResult> m_entries; /**< 条目(deque 保证引用稳定)。 */
|
||
|
||
/** @brief 淘汰最久未使用的条目,直到不超过上限。 */
|
||
void Trim();
|
||
};
|
||
|
||
/**
|
||
* @brief 组合布局键(FNV-1a 风格的增量混合)。
|
||
*
|
||
* ⚠️ 键必须覆盖**全部**影响几何的输入;漏一个字段的症状是"布局不更新"
|
||
* (与历史上"缓存键漏混字段 ⇒ 贴旧像素"是同一类 BUG)。
|
||
*/
|
||
std::uint64_t UiMixKey(std::uint64_t seed, std::uint64_t value);
|
||
|
||
/** @brief 浮点参与成键(按 1/1024 量化,避免亚像素抖动导致键频繁变化)。 */
|
||
std::uint64_t UiMixKeyFloat(std::uint64_t seed, float value);
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __UI_LAYOUT_HPP__
|