Files
endorphin-tessera-gui/src/bridge/ui_bridge/tree.rs
T

359 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 定义从托管对象提取的不可变视觉树。共享业务对象可以有多次展示,树节点与 Mount 出现的身份不是同一概念。
//! 节点保存排版、绘制和协议投影信息;浅比较用于决定是否复用已有快照。
//!
//! model::snapshot/Block 快照提取构造 UiNode;render::layout/source_layout、绘制和命中流程读取它。
//! 这里不持有 ObjectRef,因此 Arc 树可作为纯数据派生结果跨线程传递,不授权另一线程访问 VM。
//! shallow_eq 由快照缓存判断能否复用旧 Arc;children/has_flex/allocates_children 供结构遍历和布局选择使用。
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
/// 不可变视觉节点。block_id 关联业务身份,slots 保留子项出现的稳定组成;kind 和盒模型字段用于派生排版与绘制。
pub struct UiNode {
// 只有页面根带兼容 Page 相机;其余字段是普通有限盒子的布局策略。
pub surface: Option<crate::page::Surface>,
// 兼容 flex 权重,父布局据此分配剩余主轴空间;不表示节点自身已经测量出的尺寸。
pub flex: f32,
// 显式 grow/shrink 等分配策略;与 flex 数值一起参与缓存相等性和父级分配判断。
pub flex_policy: crate::layout::FlexPolicy,
// 父容器主轴空白的对齐策略,与孩子排列次序和具体尺寸计算分开。
pub main_align: crate::layout::MainAlign,
// 容器为未指定 align_self 的孩子提供的交叉轴缺省策略。
pub cross_align: crate::layout::CrossAlign,
// 孩子对父 cross_align 的单独覆盖;None 表示继承,不能解释为禁止 stretch。
pub align_self: Option<crate::layout::CrossAlign>,
// 父布局分配时需要预留的外边距,不作为孩子内容绘制的本地原点偏移。
pub margin: crate::layout::Insets,
// 两轴的自然/固定/填充尺寸规则,由盒模型参与测量和最终安排。
pub sizing: crate::box_model::Sizing,
// 单边可选覆盖值;None 表示沿用 kind 的统一 padding,不代表边距为零。
pub padding_edges: [Option<f32>; 4],
// 四边边框厚度;layout::insets 将其与有效 padding 相加,内容盒、相机锚点及绘制共用该内缩量。
pub border: crate::layout::Insets,
// 留原始颜色字符串供显示列表解码;颜色变更也必须使 shallow_eq 为 false。
pub border_color: String,
// 是否把后代限制在内容可视区域内,绘制/命中需要采用同一剪裁关系。
pub clip_children: bool,
// 这些字符串只是派生身份/协议版本键,不是可以操作托管堆的 ObjectRef。
pub block_id: String,
// 源视觉协议相关的派生版本键;即使盒模型未变,协议变化仍可能要求重新测量或绘制。
pub protocol_key: String,
// slot 保持组成身份;数组次序确定当前绘制/布局顺序,二者不是同一概念。
pub slots: Vec<u64>,
// 从按钮、点击连接或自定义事件等能力推导的交互资格,供命中宽阶段使用,不代表最终 hitTest 必然成功。
pub interactive: bool,
// 标识原始 pointer 事件能力,与高层 click 的可交互标记分别参与输入分派。
pub pointer_events: bool,
// 具体叶/容器/场景内容,原生测量与软件绘制按此枚举选择规则。
pub kind: UiKind,
}
impl UiNode {
/// 布局、快照和命中结构遍历调用,返回已有孩子切片,不分配新列表或递归复制子树。
pub fn children(&self) -> &[Arc<UiNode>] {
match &self.kind {
// 这些种类都显式包含 children;三维面板 UI 也复用普通 UiNode 子树遍历。
UiKind::Column { children, .. }
| UiKind::Overlay { children, .. }
| UiKind::Canvas { children, .. }
| UiKind::Scene2D { children, .. }
| UiKind::CanvasViewport { children, .. }
| UiKind::Viewport3D { children, .. }
| UiKind::Row { children, .. }
| UiKind::Button { children, .. }
| UiKind::Scroll { children, .. } => children,
// Text/Rect/Spacer 没有视觉子树;返回静态空切片,调用方无需按种类再分支。
_ => &[],
}
}
/// 父布局/allocates_children 查询此孩子是否需要分配空间;判断的是节点需求,不会在此实际改尺寸。
pub fn has_flex(&self) -> bool {
// 不仅旧 flex 数值,非默认 FlexPolicy 或任一轴 Fill 也要求父节点执行分配,不能仅按自然尺寸排布。
self.flex > 0.0
|| self.flex_policy != crate::layout::FlexPolicy::default()
|| self.sizing.width == crate::box_model::Dimension::Fill
|| self.sizing.height == crate::box_model::Dimension::Fill
}
/// source_layout 选择安排路径时调用,检查任一孩子的弹性需求或交叉轴拉伸是否要求明确分配。
pub fn allocates_children(&self) -> bool {
// 任何孩子需要弹性或拉伸即进入分配路径;align_self 优先于父 cross_align。
self.children().iter().any(|child| {
// any 短路即可确定是否需要分配;align_self 优先级高于容器 cross_align。
child.has_flex()
|| child.align_self.unwrap_or(self.cross_align)
== crate::layout::CrossAlign::Stretch
})
}
/// model::snapshot 缓存重建节点后调用,判断可否保留旧 Arc;返回 true 不会修改任何节点或 VM 对象。
/// 缓存调用方仍会用本轮读取重建依赖/父边;这里只证明视觉值可复用,不能证明依赖来源没有变化。
pub fn shallow_eq(&self, other: &Self) -> bool {
// 先比较所有公共视觉/协议配置,任一变化都不能复用旧节点 Arc,即使子内容完全相同。
if self.surface != other.surface
|| self.flex_policy != other.flex_policy
|| self.flex != other.flex
|| self.main_align != other.main_align
|| self.cross_align != other.cross_align
|| self.align_self != other.align_self
|| self.margin != other.margin
|| self.sizing != other.sizing
|| self.padding_edges != other.padding_edges
|| self.border != other.border
|| self.border_color != other.border_color
|| self.clip_children != other.clip_children
|| self.block_id != other.block_id
|| self.protocol_key != other.protocol_key
|| self.slots != other.slots
|| self.interactive != other.interactive
|| self.pointer_events != other.pointer_events
{
// 共有字段任一不同都足以否决复用,不再比较 kind 或递归访问后代。
return false;
}
// 直接孩子按 Arc 指针比较而不是递归深比较;依赖提取保证未变子树复用 Arc,父级只需验证其身份。
let same_children = || {
self.children().len() == other.children().len()
&& self
.children()
.iter()
.zip(other.children())
.all(|(a, b)| Arc::ptr_eq(a, b))
};
// 同类变体再比较其专有数据和直接子项;变体不同必为 false,避免布局种类切换后误复用缓存。
match (&self.kind, &other.kind) {
// a/b 与 d/e 是尺寸,c/f 是场景视图,g/h 是控制开关,i/j 是面板尺寸;孩子还需同身份。
(
UiKind::Viewport3D {
width: a,
height: b,
view: c,
controls: g,
panels: i,
..
},
UiKind::Viewport3D {
width: d,
height: e,
view: f,
controls: h,
panels: j,
..
},
) => a == d && b == e && c == f && g == h && i == j && same_children(),
// 二维视口除了宽高还比较相机快照;相机变了即使场景孩子没变也不能复用旧节点。
(
UiKind::CanvasViewport {
width: a,
height: b,
camera: c,
..
},
UiKind::CanvasViewport {
width: d,
height: e,
camera: f,
..
},
) => a == d && b == e && c == f && same_children(),
(UiKind::Scene2D { positions: a, .. }, UiKind::Scene2D { positions: b, .. }) => {
// 显式世界位置和孩子身份一起决定组成,不能因同一批对象只移动位置而漏掉刷新。
a == b && same_children()
}
(UiKind::Overlay { padding: a, .. }, UiKind::Overlay { padding: b, .. }) => {
// Overlay 没有 gap/累计主轴位置,专有属性只比 padding,再检查孩子序列。
a == b && same_children()
}
(UiKind::Canvas { positions: a, .. }, UiKind::Canvas { positions: b, .. }) => {
// 兼容 Page Canvas 与 Scene2D 相同地保留显式位置,但仍要求两边变体相同。
a == b && same_children()
}
// 文本叶没有孩子,内容、字号和颜色相等即可;共有字段已在上面验证。
(
UiKind::Text {
value: a,
size: sa,
color: ca,
},
UiKind::Text {
value: b,
size: sb,
color: cb,
},
) => a == b && sa == sb && ca == cb,
// 矩形依次比较宽、高、圆角和颜色,不以外框尺寸相同掩盖绘制变化。
(
UiKind::Rect {
width: a,
height: b,
radius: c,
color: d,
},
UiKind::Rect {
width: e,
height: f,
radius: g,
color: h,
},
) => (a, b, c, d) == (e, f, g, h),
// Spacer 唯一专有属性是占位长度。
(UiKind::Spacer(a), UiKind::Spacer(b)) => a == b,
// 滚动偏移属于 Mount,此处只比轴/有限尺寸/内容规则及孩子,避免共享节点携带单次出现偏移。
(
UiKind::Scroll {
axis: aa,
width: a,
height: b,
padding: pa,
gap: ga,
children: _,
},
UiKind::Scroll {
axis: ab,
width: c,
height: d,
padding: pb,
gap: gb,
children: _,
},
) => aa == ab && a == c && b == d && pa == pb && ga == gb && same_children(),
// 这三种容器共用 padding/gap 比较,但模式配对仍只允许 Column 对 Column 等同种比较。
(
UiKind::Column {
padding: a, gap: b, ..
},
UiKind::Column {
padding: c, gap: d, ..
},
)
| (
UiKind::Row {
padding: a, gap: b, ..
},
UiKind::Row {
padding: c, gap: d, ..
},
)
| (
UiKind::Button {
padding: a, gap: b, ..
},
UiKind::Button {
padding: c, gap: d, ..
},
) => a == c && b == d && same_children(),
// 任何不同变体都不能复用,哪怕专有数据恰巧具有相同数值。
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
/// 视觉内容的原生分类。叶节点保存绘制数据,容器保存子树,场景视口另外保存相机与世界投影。
pub enum UiKind {
// 有限 3D 视口包含不可变场景视图和面板 UI 孩子;panels 与 children/slots 按顺序对应。
Viewport3D {
// 有限布局盒子尺寸,不是三维世界的几何范围。
width: f32,
height: f32,
// 已提取的场景、相机和投影数据,render 不需要为每个像素回读 VM。
view: Arc<crate::scene3d::View>,
// 是否存在可用控制器,输入宽阶段据此判断相机手势能力。
controls: bool,
// 各世界 UI 面板的逻辑尺寸,与 children 次序对应,供投影与布局共用。
panels: Vec<crate::layout::Size>,
// 面板 UI 的普通二维树;业务对象身份仍由 UiNode.block_id/slots 描述。
children: Vec<Arc<UiNode>>,
},
// 二维世界组成保留孩子的显式位置,不使用行列的累计偏移定位。
Scene2D {
// 每个孩子的世界 x/y,数组下标与 children 对齐,不按叠放顺序推算位置。
positions: Vec<[f32; 2]>,
children: Vec<Arc<UiNode>>,
},
// 有限布局盒子观察独立 Scene2D,相机仅影响投影,不改变场景节点原始逻辑位置。
CanvasViewport {
// 参加父布局的有限盒子尺寸;不是 Scene2D 孩子的总包围盒。
width: f32,
height: f32,
// 纯数据相机快照,只影响本视口的世界到屏幕映射,不改写原世界树。
camera: crate::viewport::CameraView,
children: Vec<Arc<UiNode>>,
},
// 孩子重叠布置,顺序同时决定叠放次序;普通有限布局与世界 Canvas 不同。
Overlay {
// 统一内边距,单边覆盖仍由 UiNode.padding_edges 提供。
padding: f32,
children: Vec<Arc<UiNode>>,
},
// 旧 Page Canvas 模式的兼容投影,位置数组与孩子顺序一致,页面级 Surface 保存其相机。
Canvas {
// 页面根中每个孩子的业务位置,保持与 children 同长度同顺序;Surface 再统一变换到页面视图。
positions: Vec<[f32; 2]>,
// 业务组成提取的不可变子树;数组顺序决定遍历/绘制顺序,不按 x/y 自动排序。
children: Vec<Arc<UiNode>>,
},
// 按钮是有子内容的容器,不把标签文本硬编码进 UiKind;交互资格另由 UiNode 标志表达。
Button {
// 内容与边框之间的统一内边距;单边覆盖仍优先使用 UiNode.padding_edges。
padding: f32,
// 多个内容孩子的主轴间距,不含父节点外 margin 或按钮边框厚度。
gap: f32,
// 标签只是其中一个普通 Text 子树,按钮没有独立的硬编码文字字段。
children: Vec<Arc<UiNode>>,
},
// 文字叶节点保留源字号等级与颜色文本;排版及最终设备比例由后续布局/显示列表转换处理。
Text {
// 已读取的文本内容,字体测量与软件绘制使用同一字符串。
value: String,
// 宿主字号等级,实际字体像素大小由 text/布局映射规则确定。
size: u32,
// 源颜色格式已在提取路径验证,绘制阶段再转成原生颜色值。
color: String,
},
// 纯绘制叶节点;几何尺寸与圆角供布局/显示列表共用,不含托管对象句柄。
Rect {
// 自然宽高用于测量;父安排与盒尺寸策略仍可给实际出现分配其他尺寸。
width: f32,
height: f32,
// 圆角半径影响显示列表的矩形命令,不给孩子增加布局空间。
radius: f32,
// 已校验格式的填充色字符串,与 UiNode.border_color 分别控制填充和边框。
color: String,
},
// Column 沿纵轴累加孩子位置,Row 沿横轴;二者都保存统一内边距和孩子间距。
Column {
// 容器默认内边距,最终内容内缩还包括单边覆盖与边框厚度。
padding: f32,
// 相邻孩子间沿纵向增加的空隙,首尾不额外增加一份 gap。
gap: f32,
// 当前组成顺序;节点可共享 Arc,但每次出现的 Mount 和几何另行建立。
children: Vec<Arc<UiNode>>,
},
// 横排容器与 Column 使用相同盒模型,但 gap 和弹性分配沿水平主轴计算。
Row {
// 统一内边距,由 layout::insets 与单边覆盖/边框合成有效值。
padding: f32,
// 横向相邻孩子间距,不是孩子自身宽度或两边 margin 的替代值。
gap: f32,
// 源组成的逻辑顺序,不因视觉位置变化而重排 slot。
children: Vec<Arc<UiNode>>,
},
// 无视觉内容的间隔叶节点,提供布局占用尺寸。
Spacer(f32),
// 滚动盒保存有限分配尺寸、允许轴和普通内容;实际偏移按 Mount 出现分别维护,不放到共享节点里。
Scroll {
// axis 指定允许消费的滚动方向;实际可消费距离还取决于具体 Mount 的内容/视口范围。
axis: crate::scroll::Axis,
// 有限视口的自然尺寸;内容可能大于视口,溢出范围和偏移由布局与 Mount 派生。
width: f32,
height: f32,
// 内容布局的统一内边距,与视口外 margin 分开。
padding: f32,
// 内容子项的纵向间隔,保持与原生滚动容器布局规则一致。
gap: f32,
// 当前内容树;滚动仅改变出现投影,不把移动后的坐标写入共享 UiNode。
children: Vec<Arc<UiNode>>,
},
}