#ifndef __UI_BOX_HPP__ #define __UI_BOX_HPP__ #include #include #include #include #include #include "PaintUtil.hpp" #include "Theme.hpp" namespace jdi { /** * @brief 一个几何 box(**纯值类型**,不含行为、不含生命周期)。 * * ## 它是什么 * * 描述"某块界面元素占据哪个矩形、属于第几层、在第几个绘制层、被裁在哪个矩形里"。 * 这就是界面在某一帧的**几何产物**:绘制、命中、调试描边理论上都应该从它派生, * 而不是各自再算一遍。 * * ## 为什么不是控件节点 * * 刻意不继承控件基类:`PlotView` 有 4 层自持位图缓存、`SignalPanel` 有行位图缓存, * 它们要求"像素由组件自己负责"。把几何塞进继承体系会和缓存失效体系打架, * 而 box 只是一个可复制的矩形 + 几个整数标签。 * * ## 为什么 `clip` 与 `rect` 分开存 * * 组件内部的元素可能落在地盘之外(典型:滚出可视区的网格,Y 是负数)。 * 消费方必须能区分"这块几何真实位置在哪"和"它最多能显示到哪", * 因此裁剪矩形跟着 box 走,而不是靠全局状态。 */ struct UiBox { gd::RectF rect; /**< 矩形(客户区坐标)。 */ gd::RectF clip; /**< 裁剪矩形(= 所属组件的区域;空 = 不裁剪)。 */ int depth = 0; /**< 层级:0 = 顶层组件区域,≥1 = 内部控件。深 = 更靠近叶子。 */ int layer = 0; /**< 绘制层:越大越靠上,由 `UiBoxTable::PushLayer` 分配。 */ }; /** * @brief 几何 box 的收集器(值语义容器,**不含任何绘制代码**)。 * * ## 职责边界 * * 这里只负责"**累积几何 + 维护裁剪/层/遮挡这些几何事实**"。 * 怎么把 box 变成屏幕上的线,是 `UiDebugBounds` 的事(那是调试专用的消费者)。 * 分开的原因:几何是**稳定事实**(命中、遮挡、将来的脏矩形都要用), * 而"用彩色包角描边"只服务于一个开关,两者不该长在同一个类里。 * * ## 两个排序键缺一不可 * * * `layer`:保证**后收集的内容压住先收集的**。没有它,"信号面板的行边界" * 会盖住"右键弹出的二级菜单"(菜单是后收集的浮层)。 * * `depth`:同一层内先画父(浅层)再画子(深层),子元素的包角才不被父覆盖。 * * ## 开销约束 * * 只在宿主开启「显示布局边界」时才被构造和填充 —— 关闭时**零开销** * (不进绘制路径、不进任何位图缓存)。若将来要用它做常驻命中表, * 必须显式改为"按需重建",不能每帧无限期累积。 */ class UiBoxTable : public UiTextRectSink { public: /** @brief 清空(复用同一实例时调用),同时把绘制层复位为 0。 */ void Clear(); /** * @brief 进入下一个绘制层。 * * 宿主在每个组件收集完之后调用一次;组件内部若有"浮层压住本体"的情形 * (例如菜单的二级子菜单)也应在收集子层之前调用。 */ void PushLayer() { ++m_layer; } /** * @brief 显式设置当前绘制层(绘制期间收集文本几何时用)。 * @param [IN] layer 层号(越大越靠上)。 */ void SetLayer(int layer) { m_layer = layer; } /** @brief 回报一段文本的墨迹矩形(由 `PaintUtil` 在绘制文本时调用)。 */ void OnTextRect(const gd::RectF& rect) override; /** * @brief 设置后续 box 的裁剪矩形(= 当前组件的区域)。 * * ⚠️ **必须像裁剪绘制一样裁剪几何**:组件内部元素可能落在地盘之外 * (典型:滚出可视区的网格 `lane.rect` 的 Y 会是负数),不裁剪就会登记到相邻组件 * (工具栏 / 标签栏)的地盘上 —— 这正是用户报告"工具栏上出现大量残留"的直接原因。 * * @param [IN] clip 裁剪矩形;传空矩形表示不裁剪。 */ void SetClip(const gd::RectF& clip) { m_clip = clip; } /** @brief 追加一个 box(空矩形 / 与裁剪矩形无交集的 box 自动忽略)。 */ void Add(const gd::RectF& rect, int depth); /** @brief 追加一组同层级的 box(空矩形逐个忽略)。 */ void AddAll(const std::vector& rects, int depth); /** * @brief 登记一个**遮挡矩形**(浮层本体,如弹出菜单 / 拾色器)。 * * ⚠️ 必须做这件事:box 的消费方是**在所有内容之后**工作的,仅靠 `layer` 排序 * 只能决定"两个 box 重叠时谁压谁",并不能阻止"下层组件的边框画在不透明浮层上面"。 * 登记为遮挡后,**绘制层比它低**的 box 在该区域内会被裁掉。 * * @param [IN] rect 遮挡矩形(客户区坐标;空矩形忽略)。 */ void AddOccluder(const gd::RectF& rect); /** @brief 是否为空。 */ bool Empty() const { return m_boxes.empty(); } /** @brief box 数量。 */ std::size_t Count() const { return m_boxes.size(); } /** @brief 只读访问(**未排序**,消费方需要顺序时自己按 `(layer, depth)` 排序)。 */ const std::vector& Items() const { return m_boxes; } /** @brief 一个遮挡矩形(含它所在的绘制层)。 */ struct Occluder { gd::RectF rect; /**< 遮挡矩形。 */ int layer = 0; /**< 所在绘制层(只遮挡更低的层)。 */ }; /** @brief 遮挡矩形集合(只读)。 */ const std::vector& Occluders() const { return m_occluders; } private: std::vector m_boxes; std::vector m_occluders; int m_layer = 0; /**< 当前绘制层。 */ gd::RectF m_clip; /**< 当前裁剪矩形(空 = 不裁剪)。 */ }; } // namespace jdi #endif // __UI_BOX_HPP__