#ifndef __UI_LAYOUT_HPP__ #define __UI_LAYOUT_HPP__ #include #include #include #include #include #include #include #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 四边外扩(用于“描边允许超出自身矩形”的裁剪补偿)。 */ gd::RectF UiRectInflate(const gd::RectF& rect, float amount); /** * @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& 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& sizes, float gap, std::vector& 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& sizes, float gap, std::vector& 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 rects; /**< 逐项矩形。 */ std::vector 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 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__