#ifndef __UI_COMPONENT_HPP__ #define __UI_COMPONENT_HPP__ #include #include #include #include "Theme.hpp" namespace jdi { /** * @brief 顶层 UI 组件的统一协议(**纯接口,无状态**)。 * * ## 为什么需要它 * * 旧实现里宿主(`Application`)用**手写 if 链**逐个点名四个大组件,导致同一件事 * (设置缩放 / 摆放 / 悬停离开 / 光标仲裁 / 绘制)要在宿主的每个消息分支里各写一遍, * 新增组件必须记得改 5 处。这里把它们收敛成一张**有序组件表**, * 宿主只按表遍历,不再点名。 * * ## 为什么不是 `UiWidget` * * `UiWidget` 的契约是"**树上的一个节点**":尺寸由父节点 `Measure` 决定、脏区由父节点下探、 * 命中走树的命中链、`OnPaint` 只画自己不许遍历子节点。而顶层大组件的现实是: * * 区域由宿主给定,且**内部还要再切分**; * * `PlotView` 有 4 层自持位图缓存、`SignalPanel` 有行位图缓存 —— 与"父脏下探"是另一套失效体系; * * `PlotView` 一旦成为树节点,它的 `Paint` 就得接受父级 `dirty` 并与自己的缓存对齐, * 这正是历史上"后台渲染覆盖用户平移 / 命中错位"那类事故的温床。 * * ⇒ 两者是**兄弟关系**而不是父子关系: * `UiWidget`(树内节点,管**一块区域内部的**布局/命中/绘制) * `UiComponent`(顶层宿主,管**自己这一整块地盘**:区域、缓存、局部重绘策略)。 * `Toolbar` / `TabBar` 同时是两者 —— 对外实现本协议,对内持有一棵 `UiWidget` 树。 * * ## 实现要求 * * * 所有方法都必须**幂等且可在未布局时安全调用**(宿主会在首次 `Layout` 之前调 `SetScale`)。 * * `HitTest` 必须与 `Layout` 接受的区域一致(绘制与命中同源)。 * * `Paint` 必须把内容严格限制在 `Area()` 内(宿主可能只回送部分矩形)。 */ class UiComponent { public: virtual ~UiComponent() = default; /** @brief 设置 DPI 缩放系数(> 0)。 */ virtual void SetScale(float scale) = 0; /** @brief 在宿主给定的区域内摆放自身(内部布局由组件自己决定)。 */ virtual void Layout(const gd::RectF& area) = 0; /** @brief 当前占用区域(客户区坐标);未布局时返回空矩形。 */ virtual gd::RectF Area() const = 0; /** @brief 绘制自身(必须裁剪在 `Area()` 内)。 */ virtual void Paint(gd::Graphics& g) = 0; /** @brief 坐标是否落在本组件内(与 `Area()` 同源)。 */ virtual bool HitTest(int x, int y) const = 0; /** @brief 指针离开窗口/组件:清掉本组件的悬停态。返回是否需要重绘。 */ virtual bool OnMouseLeave() = 0; /** @brief 是否处于内联编辑态(宿主据此把鼠标/键盘优先交给它)。 */ virtual bool IsEditing() const { return false; } /** @brief 光标仲裁:返回 `nullptr` 表示不认领(由下层或宿主决定)。 */ virtual HCURSOR QueryCursor(int x, int y) { (void)x; (void)y; return nullptr; } }; } // namespace jdi #endif // __UI_COMPONENT_HPP__