UI布局原理¶
canvas系统¶
canvas管理一块区域内的绘制、动画、用户交互以及中间状态。
canvas仅处理region和primitive。primitive即基本图元,如文字、矩形、线条等,是绘制的基本单元;region则是primitive和子region的容器,主要提供裁剪矩形参数,是处理用户输入的基本单元。
region和primitive通过identifier(uint64_t)唯一表示,主要用于识别两帧之间同一对象,利用identifier可以存储中间状态在全局哈希表中。
animgui本身没有布局系统的专用类,而是通过扩展canvas的功能来提供布局功能。
除了顶层canvas外,其余canvas实例均通过调用顶层canvas的功能来实现特殊功能。
class canvas {
public:
// 计算图元的大小,简单转发至内部emitter
[[nodiscard]] virtual vec2 calculate_bounds(const primitive& primitive) const = 0;
// 生成当前region的子标识符,即在相同region下,第i个region_sub_uid和上一帧的第i个region_sub_uid是相等的,用于创建匿名region,多用于UI组件
[[nodiscard]] virtual identifier region_sub_uid() = 0;
// 将当前区域压入栈中
// uid: 唯一标识符,将与父区域的uid混合得到真实标识符,可视为目录名
// reserved_bounds: 预分配区域包围盒,若不支持自动布局或者子区域需要知道预分配区域,则必须指定此值;否则可在pop_region中再次指定该值
// 返回值:push_region在命令缓冲中的下标,可通过commands()访问并修改;该region的真实标识符,用于中间状态存储,可视为目录绝对路径
virtual std::pair<size_t, identifier> push_region(identifier uid, const std::optional<bounds_aabb>& reserved_bounds = std::nullopt) = 0;
// 将当前区域弹出栈
// new_bounds: 自动布局计算的最终区域包围盒。push_region与pop_region至少要指定一次包围盒
virtual void pop_region(const std::optional<bounds_aabb>& new_bounds = std::nullopt) = 0;
// 在当前区域中绘制图元
// uid: 图元的唯一标识符,将与父区域的uid混合得到真实标识符,可视为文件名
// primitive: 图元信息,如文字、矩形等。
// 返回值:该primitive在命令缓冲中的下标,可通过commands()访问并修改;该primitive的真实标识符,用于中间状态存储,可视为文件绝对路径
virtual std::pair<size_t, identifier> add_primitive(identifier uid, primitive primitive) = 0;
// 当前区域的预分配区域大小,由push_region指定
[[nodiscard]] virtual vec2 reserved_size() const noexcept = 0;
// 当前指令缓冲
virtual span<operation> commands() noexcept = 0;
// 根据唯一标识符获取一个中间状态的引用。如果该状态尚未初始化,则调用默认初始化。
// 注意:每帧相同对象的唯一标识符应该相等
// 注意:同一个uid不能存储多类型的状态,可通过mix与""_id生成子标识符来存储多类型的状态
template <typename T>
T& storage(const identifier uid);
// 获取本帧风格配置
[[nodiscard]] virtual const style& global_style() const noexcept = 0;
// 获取当前内存分配器,一般是arena allocator
[[nodiscard]] virtual std::pmr::memory_resource* memory_resource() const noexcept = 0;
// 获取输入后端
[[nodiscard]] virtual input_backend& input() const noexcept = 0;
// 获取当前区域的估计包围盒(源自上一帧的数据)
[[nodiscard]] virtual const bounds_aabb& region_bounds() const = 0;
// 获取当前区域的绝对偏移
[[nodiscard]] virtual vec2 region_offset() const = 0;
// 判断鼠标是否悬停在当前区域上
[[nodiscard]] virtual bool region_hovered() const = 0;
// 判断鼠标是否悬停在指定区域上
[[nodiscard]] virtual bool hovered(const bounds_aabb& bounds) const = 0;
// 声明当前区域是可获取焦点的,用于游戏手柄的焦点移动功能。接口尚不稳定,故此处不介绍。
[[nodiscard]] virtual bool region_request_focus(bool force = false) = 0;
// 动画系统尚未完成,故此处不介绍
[[nodiscard]] virtual float step(identifier id, float dest) = 0;
};
简单UI布局实现¶
简单地利用region并重用已有布局函数来实现新的布局。
比如下面的layout_row_center (animgui/builtins/layouts.cpp)
bounds_aabb layout_row_center(canvas& parent, const std::function<void(row_layout_canvas&)>& render_function) {
// 压入新region,暂不指定包围盒
parent.push_region("layout_center_region"_id);
// 由用户定义区域内的组件,并拿到区域内组件的包围盒大小
const auto [w, h] = layout_row(parent, row_alignment::middle, render_function);
// 计算真实包围盒,以将区域内的所有内容移动至中心
const auto offset_y = (parent.reserved_size().y - h) / 2.0f;
const auto bounds = bounds_aabb{ 0.0f, parent.reserved_size().x, offset_y, offset_y + h };
// 弹出region,指定包围盒
parent.pop_region(bounds);
// 返回region的包围盒
return bounds;
}
复杂UI布局实现¶
复杂UI布局则需要定义新的canvas,给canvas提供新功能并hook canvas的某些方法。
为了减少代码重复,layouts.hpp中提供了layout_proxy用于默认方法转发,可以理解为动态多态。 警告:与真正的多态不同的是,父方法调用canvas的某个方法时,该方法无法被子方法覆写。原因很简单但这一点很容易被忽略。即不支持自上而下调用时的多态。
具体示例参见layout_row的实现 (animgui/builtins/layouts.cpp)