338 lines
16 KiB
Rust
338 lines
16 KiB
Rust
//! 定义从托管对象提取的不可变视觉树。共享业务对象可以有多次展示,树节点与 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],
|
||
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 {
|
||
positions: Vec<[f32; 2]>,
|
||
children: Vec<Arc<UiNode>>,
|
||
},
|
||
// 按钮是有子内容的容器,不把标签文本硬编码进 UiKind;交互资格另由 UiNode 标志表达。
|
||
Button {
|
||
padding: f32,
|
||
gap: f32,
|
||
children: Vec<Arc<UiNode>>,
|
||
},
|
||
// 文字叶节点保留源字号等级与颜色文本;排版及最终设备比例由后续布局/显示列表转换处理。
|
||
Text {
|
||
// 已读取的文本内容,字体测量与软件绘制使用同一字符串。
|
||
value: String,
|
||
// 宿主字号等级,实际字体像素大小由 text/布局映射规则确定。
|
||
size: u32,
|
||
// 源颜色格式已在提取路径验证,绘制阶段再转成原生颜色值。
|
||
color: String,
|
||
},
|
||
// 纯绘制叶节点;几何尺寸与圆角供布局/显示列表共用,不含托管对象句柄。
|
||
Rect {
|
||
width: f32,
|
||
height: f32,
|
||
radius: f32,
|
||
color: String,
|
||
},
|
||
// Column 沿纵轴累加孩子位置,Row 沿横轴;二者都保存统一内边距和孩子间距。
|
||
Column {
|
||
padding: f32,
|
||
gap: f32,
|
||
children: Vec<Arc<UiNode>>,
|
||
},
|
||
Row {
|
||
padding: f32,
|
||
gap: f32,
|
||
children: Vec<Arc<UiNode>>,
|
||
},
|
||
// 无视觉内容的间隔叶节点,提供布局占用尺寸。
|
||
Spacer(f32),
|
||
// 滚动盒保存有限分配尺寸、允许轴和普通内容;实际偏移按 Mount 出现分别维护,不放到共享节点里。
|
||
Scroll {
|
||
// axis 指定允许消费的滚动方向;实际可消费距离还取决于具体 Mount 的内容/视口范围。
|
||
axis: crate::scroll::Axis,
|
||
width: f32,
|
||
height: f32,
|
||
padding: f32,
|
||
gap: f32,
|
||
children: Vec<Arc<UiNode>>,
|
||
},
|
||
}
|