1030 lines
49 KiB
Rust
1030 lines
49 KiB
Rust
//! Block 的共享赋值、底层 Data 与视觉属性实现。Block 通过 Cell 间接指向 Data,使别名能观察到同一次替换。
|
||
//! Data 保存子组件 slot、布局配置、绑定和订阅。属性读写需要沿有效的类型/存储路径解析,不能把快照当成可变业务对象。
|
||
//! 提取视觉数据时将托管属性覆盖到原生描述;校验负责拒绝循环、非法尺寸和不符合类型的组成。
|
||
//!
|
||
//! 调用路径:UI 构造器经 allocate 建立托管图;Children/赋值入口经 validate 检查新组成。
|
||
//! RenderCache 经 resolve_tracked/visual_data 提取本轮描述,绑定刷新也用 apply_visual_property 预校验结果。
|
||
//! ObjectPayload 钩子由 Aster 属性查询、赋值及 GC 调用,不直接保存窗口或每次出现的几何。
|
||
//!
|
||
//! Live visual objects. All semantic references are owned by Aster's heap;
|
||
//! native Arcs are reserved for immutable rendering snapshots.
|
||
use std::sync::Arc;
|
||
|
||
use endor_core::{NativeContext, ObjectPayload, ObjectRef, ObjectStore, Value, VmError};
|
||
|
||
use crate::ui_bridge::UiNode;
|
||
|
||
#[derive(Debug, Clone)]
|
||
/// 稳定组成 slot 与子 Block 的配对;slot 不依赖孩子当前的屏幕位置。
|
||
pub struct Child {
|
||
// slot 标识父组件中这条组成边,移动行序不改变它;同一 Block 可通过不同 slot 出现多次。
|
||
pub slot: u64,
|
||
// 组成边强持有的业务 Block;同一对象可通过不同 slot 多次出现。
|
||
pub block: ObjectRef,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
/// 原生组件分类,只描述业务组件种类;每种展示几何由布局和 Mount 另行生成。
|
||
pub enum Kind {
|
||
// 原生文字默认内容/离散样式;换行宽高来自各出现位置的约束。
|
||
Text {
|
||
value: String,
|
||
size: u32,
|
||
color: String,
|
||
},
|
||
// 原生矩形自然宽高、圆角和填充色,最终像素尺寸由布局分配。
|
||
Rect {
|
||
width: f32,
|
||
height: f32,
|
||
radius: f32,
|
||
color: String,
|
||
},
|
||
// 单一自然间距叶节点,不持有子组成。
|
||
Spacer(f32),
|
||
// 有限滚动视口的自然尺寸,滚动偏移和范围属于 Mount。
|
||
Scroll {
|
||
width: f32,
|
||
height: f32,
|
||
},
|
||
// 线性横向容器,孩子组成顺序由 Data.children 保存。
|
||
Row,
|
||
// 线性纵向容器,与 Row 共享通用盒模型及弹性策略。
|
||
Column,
|
||
// 叠放容器,子项共享内容区域,各自独立对齐而非线性累积。
|
||
Overlay,
|
||
// 二维世界组成根,子项业务 x/y 可带符号。
|
||
Scene2D,
|
||
// 承载 Scene3D 的有限视口,场景与相机引用保存于 Data。
|
||
Viewport3D {
|
||
width: f32,
|
||
height: f32,
|
||
},
|
||
// 承载唯一 Scene2D 根的有限二维视口,独立相机负责世界到视图投影。
|
||
CanvasViewport {
|
||
width: f32,
|
||
height: f32,
|
||
},
|
||
// 具有默认按钮视觉/事件协议的容器种类。
|
||
Button,
|
||
// 页面业务根,包含导航和兼容表面状态,生命周期有别于普通 Block。
|
||
Page,
|
||
}
|
||
|
||
/// The VM owns the Block -> Cell -> Data relationship. Shared assignment
|
||
/// changes the Cell to point at another Block, retaining the original identity.
|
||
#[derive(Debug, Clone)]
|
||
/// 对共享 Cell 的稳定包装。赋值可让所有别名一起改指向,而每次出现的 Mount 仍独立。
|
||
pub struct Block {
|
||
// 共享赋值单元 Cell;其值可为最终 Data 或另一个 Block,从而保留别名身份并转发属性/类型查询。
|
||
pub state: ObjectRef,
|
||
// 包装是否为 Page,限制共享赋值不能跨越页面与普通组件的生命周期边界。
|
||
pub page: bool,
|
||
// 该业务 owner 的任务作用域,独立于 Data 替换保留;trace 负责向 VM 暴露拥有边。
|
||
pub tasks: Option<ObjectRef>,
|
||
}
|
||
|
||
impl ObjectPayload for Block {
|
||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||
fn reference_assignment_only(&self) -> bool {
|
||
true
|
||
}
|
||
/// 沿 Block 的共享 Cell 定位属性存储,使别名替换后读取到同一份新 Data。
|
||
fn attribute_target(&self, store: &ObjectStore) -> Result<Option<ObjectRef>, VmError> {
|
||
// VM 属性查询先读共享 Cell;不是对象意味着内部状态损坏,不能返回一个空属性集来掩盖。
|
||
let Value::Object(value) = store.cell_get(self.state)? else {
|
||
return Err(VmError::InvalidArgument("Block state is not an object"));
|
||
};
|
||
// 若 Cell 指向替代 Block,将属性查询继续转发;否则读取 Data.properties 的普通托管字段。
|
||
if store.object(value)?.payload().as_any().is::<Block>() {
|
||
Ok(Some(value))
|
||
} else {
|
||
store
|
||
.payload::<Data>(value, "Block data")?
|
||
.properties
|
||
.map(Some)
|
||
.ok_or(VmError::InvalidArgument(
|
||
"Block has no managed property storage",
|
||
))
|
||
}
|
||
}
|
||
|
||
/// 从 Block 当前 Data 解析其类型转发目标,使共享替换后的类型查询仍一致。
|
||
fn type_target(&self, store: &ObjectStore) -> Result<Option<ObjectRef>, VmError> {
|
||
// VM 类型查询同样沿 Cell 解析,防止别名的属性与名义类型指向不同替代对象。
|
||
let Value::Object(value) = store.cell_get(self.state)? else {
|
||
return Err(VmError::InvalidArgument("Block state is not an object"));
|
||
};
|
||
// 若仍是包装 Block 则继续转发;到最终 Data 时保留当前包装本身的名义类型。
|
||
if store.object(value)?.payload().as_any().is::<Block>() {
|
||
Ok(Some(value))
|
||
} else {
|
||
store.payload::<Data>(value, "Block data")?;
|
||
// 到达最终 Data 时停止类型转发,使用当前 Block 自身名义类型;只有替换为其他 Block 才继续转发类型。
|
||
Ok(None)
|
||
}
|
||
}
|
||
|
||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||
fn as_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||
fn kind_name(&self) -> &'static str {
|
||
"tessera-block"
|
||
}
|
||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||
fn allows_attributes(&self) -> bool {
|
||
true
|
||
}
|
||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||
// Aster GC 追踪共享 Cell 与 owner 任务作用域;别名链不能隐藏在宿主 Arc 图中。
|
||
visit(self.state);
|
||
// 存在业务 owner 任务作用域时强持有它,替换视觉 Data 不会把该作用域漏出追踪。
|
||
if let Some(tasks) = self.tasks {
|
||
visit(tasks);
|
||
}
|
||
}
|
||
/// 向 VM 暴露共享赋值单元;替换的是指向关系,不是逐项复制组件业务字段。
|
||
fn assignment_cell(
|
||
&self,
|
||
source: ObjectRef,
|
||
store: &ObjectStore,
|
||
) -> Result<Option<ObjectRef>, VmError> {
|
||
// VM 询问赋值是否共享 Cell 时,只接收另一 Block 包装;其他值交回常规赋值规则。
|
||
if !store.object(source)?.payload().as_any().is::<Self>() {
|
||
return Ok(None);
|
||
}
|
||
// Page 与普通组件不能交叉共享赋值,防止把窗口/页面生命周期能力替换成普通叶组件。
|
||
if store.payload::<Self>(source, "Block")?.page != self.page {
|
||
return Err(VmError::InvalidArgument(
|
||
"Page assignment requires another Page",
|
||
));
|
||
}
|
||
// 建立转发前检查源展开图不能回到本 Cell,禁止共享赋值在别名或组成关系中形成环。
|
||
validate(store, source, Some(self.state))?;
|
||
Ok(Some(self.state))
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
/// 组件当前业务描述与组成。与一次求出的屏幕几何分开保存,替换或属性变化会触发派生快照失效。
|
||
pub struct Data {
|
||
// 构造时原生种类及其默认视觉值;当前托管属性由 visual_data 覆盖到副本。
|
||
pub kind: Kind,
|
||
// 有顺序的组成边,slot 保留身份,Vec 顺序决定布局/绘制展开顺序。
|
||
pub children: Vec<Child>,
|
||
// 容器统一内边距,单边 padding_edges 为 Some 时优先覆盖。
|
||
pub padding: f32,
|
||
// 线性子项间距,参与自然尺寸、排列和剩余空间分配。
|
||
pub gap: f32,
|
||
// 父主轴有剩余空间时的增长权重,不是已分配像素宽高。
|
||
pub flex: f32,
|
||
// 主轴 basis、min/max 与 shrink 规则,供布局求解分配尺寸。
|
||
pub flex_policy: crate::layout::FlexPolicy,
|
||
// 容器主轴剩余空间的对齐策略,与子项 flex 尺寸分配区分。
|
||
pub main_align: crate::layout::MainAlign,
|
||
// 容器交叉轴默认对齐,子项 align_self 可单独覆盖。
|
||
pub cross_align: crate::layout::CrossAlign,
|
||
// None 编码 auto,表示继承当前父出现位置策略;共享孩子在不同父下可有不同几何。
|
||
pub align_self: Option<crate::layout::CrossAlign>,
|
||
// 子项外边距,参与父固有尺寸与分配,不包含在本项边框盒宽高中。
|
||
pub margin: crate::layout::Insets,
|
||
// 边框盒 auto/fill/显式尺寸及 min/max 限制,解码后由布局应用。
|
||
pub sizing: crate::box_model::Sizing,
|
||
// 四边覆盖顺序为上、右、下、左;None 表示回退统一 padding,不等于零。
|
||
pub padding_edges: [Option<f32>; 4],
|
||
// 四边边框厚度,占据边框盒内部空间,并生成独立绘制命令。
|
||
pub border: crate::layout::Insets,
|
||
// 已验证颜色文本,供边框绘制使用;不是业务内容自身的填充色。
|
||
pub border_color: String,
|
||
// overflow=clip 时限制后代的绘制与命中,不自动赋予滚动能力。
|
||
pub clip_children: bool,
|
||
// CanvasViewport/Viewport3D 使用的相机强引用;共享相机需源代码显式复用对象。
|
||
pub camera: Option<ObjectRef>,
|
||
// 三维视口的 Scene3D 业务根,面板 UI 由场景提取流程展开。
|
||
pub scene3d: Option<ObjectRef>,
|
||
// 三维视口自定义控制器;提取时检查名义 update,输入路径才调用它。
|
||
pub controller: Option<ObjectRef>,
|
||
// ScrollBlock 可滚动方向;实际偏移与有限范围按每个 Mount 保存。
|
||
pub scroll_axis: crate::scroll::Axis,
|
||
// Scene2D/canvas 等定位使用的带符号业务坐标,不能当作已投影屏幕位置。
|
||
pub position: [f32; 2],
|
||
// Page 兼容表面模式及 pan/zoom,具体页面身份在发布时补入。
|
||
pub surface: crate::page::Surface,
|
||
// 下次插入的单调 slot,初始孩子按 0..len 编号,后续重排不把旧 slot 重新编号。
|
||
pub next_slot: u64,
|
||
// Page 导航历史是真实托管拥有边;后退/前进可重新展示同一个对象,而非复制快照。
|
||
pub history: Vec<ObjectRef>,
|
||
// Page 当前导航历史位置,与 history 一起维护前进/后退关系。
|
||
pub history_index: usize,
|
||
// 目标拥有的属性连接,连接再持有 compute;取消令牌不反向保活目标。
|
||
pub bindings: Vec<ObjectRef>,
|
||
// 旧 textOf 源对象与字段名推导入口;通用 text 绑定建立后清除以避免竞争。
|
||
pub text_binding: Option<(ObjectRef, String)>,
|
||
// 兼容 onClick 直接回调,事件路径调用,GC 需保活该函数。
|
||
pub click: Option<ObjectRef>,
|
||
// 对象拥有的事件连接,派发按这些连接决定监听器资格。
|
||
pub subscriptions: Vec<ObjectRef>,
|
||
// 源可读写的视觉字段存储;Data 自身保存结构边与构造默认值,提取时再将当前属性覆盖到派生副本。
|
||
pub properties: Option<ObjectRef>,
|
||
}
|
||
|
||
impl Data {
|
||
/// 建立组件业务描述,为初始孩子分配稳定 slot,并初始化无绑定、无订阅的默认布局策略。
|
||
pub fn new(kind: Kind, children: Vec<ObjectRef>, padding: f32, gap: f32) -> Self {
|
||
// 先为输入孩子分配稳定 slot,next_slot 从当前长度继续,后续追加不会复用被删除条目的编号。
|
||
let next_slot = children.len() as u64;
|
||
// UI 构造器传入的种类/孩子/统一 padding 与 gap 形成默认描述,其余布局策略先取无约束默认值。
|
||
// 顺序编号孩子形成独立组成边,后续插入/删除只移动 Vec,不重编号旧 slot。
|
||
Self {
|
||
kind,
|
||
children: children
|
||
.into_iter()
|
||
.enumerate()
|
||
.map(|(slot, block)| Child {
|
||
slot: slot as u64,
|
||
block,
|
||
})
|
||
.collect(),
|
||
padding,
|
||
gap,
|
||
flex: 0.0,
|
||
flex_policy: crate::layout::FlexPolicy::default(),
|
||
main_align: crate::layout::MainAlign::Start,
|
||
cross_align: crate::layout::CrossAlign::Start,
|
||
align_self: None,
|
||
margin: crate::layout::Insets::default(),
|
||
sizing: crate::box_model::Sizing::default(),
|
||
padding_edges: [None; 4],
|
||
border: crate::layout::Insets::default(),
|
||
border_color: "#D8DCE2".into(),
|
||
clip_children: false,
|
||
camera: None,
|
||
scene3d: None,
|
||
controller: None,
|
||
scroll_axis: crate::scroll::Axis::Both,
|
||
position: [0.0, 0.0],
|
||
surface: crate::page::Surface::new(crate::page::Mode::Window),
|
||
next_slot,
|
||
history: vec![],
|
||
history_index: 0,
|
||
bindings: vec![],
|
||
text_binding: None,
|
||
click: None,
|
||
subscriptions: vec![],
|
||
properties: None,
|
||
}
|
||
}
|
||
/// 检查原生组件种类是否承载子组成,供树修改和布局选择使用。
|
||
pub fn is_container(&self) -> bool {
|
||
// 这些类别允许通过通用 Children 方法编辑;Viewport3D 的面板内容来自 Scene3D 提取,不是普通 children 列表。
|
||
matches!(
|
||
self.kind,
|
||
Kind::Row
|
||
| Kind::Column
|
||
| Kind::Overlay
|
||
| Kind::Scene2D
|
||
| Kind::CanvasViewport { .. }
|
||
| Kind::Button
|
||
| Kind::Page
|
||
| Kind::Scroll { .. }
|
||
)
|
||
}
|
||
}
|
||
|
||
impl ObjectPayload for Data {
|
||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||
fn reference_assignment_only(&self) -> bool {
|
||
true
|
||
}
|
||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||
fn as_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||
fn kind_name(&self) -> &'static str {
|
||
"tessera-block-data"
|
||
}
|
||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||
// 所有结构字段中出现的托管强引用必须枚举:场景/相机、属性存储、孩子、历史、绑定、订阅和回调。
|
||
// 三维场景根属于业务拥有边,不能只在渲染快照中借用而漏掉 GC。
|
||
if let Some(scene) = self.scene3d {
|
||
visit(scene);
|
||
}
|
||
// 自定义控制器可能持有源模型/闭包,需要进入循环回收图。
|
||
if let Some(controller) = self.controller {
|
||
visit(controller);
|
||
}
|
||
// 相机可被多个视口共享,Data 对它的每条强边都要被追踪。
|
||
if let Some(camera) = self.camera {
|
||
visit(camera);
|
||
}
|
||
// 普通源字段存储可能反向引用组件或页面,必须让 VM 看到这类环。
|
||
if let Some(properties) = self.properties {
|
||
visit(properties);
|
||
}
|
||
// 每条组成边都拥有孩子,不能因业务身份重复而漏计托管引用。
|
||
for child in &self.children {
|
||
visit(child.block);
|
||
}
|
||
// 页面历史需要保留可再次展示的业务对象,不是临时绘制快照。
|
||
for item in &self.history {
|
||
visit(*item);
|
||
}
|
||
// 旧 textOf 推导源仍是强引用,直到该入口被清除。
|
||
if let Some((source, _)) = &self.text_binding {
|
||
visit(*source);
|
||
}
|
||
// 连接由目标拥有,连接 payload 再追踪有效 compute。
|
||
for binding in &self.bindings {
|
||
visit(*binding);
|
||
}
|
||
// 事件连接持有回调所需图,取消和回收由事件连接状态决定。
|
||
for token in &self.subscriptions {
|
||
visit(*token);
|
||
}
|
||
// 兼容单回调同样要保活,不能只追踪通用 subscriptions。
|
||
if let Some(handler) = self.click {
|
||
visit(handler);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 分配 Data、共享 Cell 和 Block 包装,并让托管图拥有必要引用。
|
||
pub fn allocate(cx: &mut NativeContext<'_>, mut data: Data) -> Result<ObjectRef, VmError> {
|
||
// UI 构造器调用 allocate;先固定包装的 Page 标记,后续共享赋值据此检查类别兼容。
|
||
let page = matches!(data.kind, Kind::Page);
|
||
// 分配拥有边前检查每个孩子都是有效有限 Block 图;非法组成直接失败,不截断部分 UI。
|
||
for child in &data.children {
|
||
validate(cx.store(), child.block, None)?;
|
||
}
|
||
// 把原生默认参数显式写成普通属性,使源赋值、绑定及精确依赖跟踪观察到同一存储。
|
||
let properties = cx.new_object()?;
|
||
// 将当前 kind 允许的默认字段写入新 properties,使源写入与精确依赖观察同一个对象。
|
||
for (name, value) in native_properties(&data) {
|
||
cx.set_attr(properties, name, value)?;
|
||
}
|
||
data.properties = Some(properties);
|
||
// 托管化完整描述后再创建 Cell/Block,拥有链由 VM 分配机制维护。
|
||
let data = cx.allocate(data)?;
|
||
// 最终建立 Block -> Cell -> Data -> properties/children 拥有链,均在 VM 中参与追踪和循环回收。
|
||
let state = cx.new_cell(Some(Value::Object(data)))?;
|
||
cx.allocate(Block {
|
||
state,
|
||
page,
|
||
tasks: None,
|
||
})
|
||
}
|
||
|
||
/// Borrowed final data; forwarding is part of the managed graph, never an Arc
|
||
/// ownership graph hidden from the VM's cycle collector.
|
||
/// 解析一个 Block 当前指向的 Data,返回底层身份和借用描述。
|
||
pub fn resolve(store: &ObjectStore, block: ObjectRef) -> Result<(ObjectRef, &Data), VmError> {
|
||
// 编辑、绑定和协议辅助调用只需最终 Data;有缓存需求的调用方应保留 tracked 返回的依赖路径。
|
||
let (reference, data, _) = resolve_tracked(store, block)?;
|
||
Ok((reference, data))
|
||
}
|
||
|
||
/// 解析 Block 的完整引用路径,并返回快照失效所需的依赖身份。
|
||
pub fn resolve_tracked(
|
||
store: &ObjectStore,
|
||
block: ObjectRef,
|
||
) -> Result<(ObjectRef, &Data, Vec<ObjectRef>), VmError> {
|
||
// 当前待解析包装,从调用方给定的业务别名开始,而非直接信任旧 Data 句柄。
|
||
let mut current = block;
|
||
// 解析转发链的全局去重集合;同一包装再次出现表示纯转发循环。
|
||
let mut seen = std::collections::HashSet::new();
|
||
// RenderCache 借用这份身份路径建立失效观察,不把依赖列表当作持久 GC 根。
|
||
let mut dependencies = Vec::new();
|
||
// 逐层走 Block→Cell→替代 Block,直到最终 Data 或明确错误,不按固定深度截断。
|
||
loop {
|
||
// 解析整条转发路径防环;依赖收集同时记录每层 Block 与 Cell,任一转发变更都会让视觉缓存失效。
|
||
if !seen.insert(current) {
|
||
return Err(VmError::InvalidArgument("cyclic Block reference"));
|
||
}
|
||
// 每一层都检查确为 Block payload,禁止任意对象被当成转发节点。
|
||
let state = store.payload::<Block>(current, "Block")?.state;
|
||
dependencies.extend([current, state]);
|
||
// Cell 内容须为托管对象,Nil/数值等状态不能产生合法视觉描述。
|
||
let Value::Object(value) = store.cell_get(state)? else {
|
||
return Err(VmError::InvalidArgument("Block state is not an object"));
|
||
};
|
||
// 遇到 Data 返回最终存储身份与借用引用;否则下一轮要求该值必须是 Block,不接受任意对象。
|
||
if store.object(value)?.payload().as_any().is::<Data>() {
|
||
dependencies.push(value);
|
||
return Ok((
|
||
value,
|
||
store.payload::<Data>(value, "Block data")?,
|
||
dependencies,
|
||
));
|
||
}
|
||
current = value;
|
||
}
|
||
}
|
||
|
||
/// Validate the entire finite expansion before establishing a new composition
|
||
/// edge. The node count is an explicit rejected resource quota, not truncated UI.
|
||
/// 完整展开 Block/Cell/孩子图,拒绝环、禁止目标回边及超过 10000 次展开的组成。
|
||
pub fn validate(
|
||
store: &ObjectStore,
|
||
root: ObjectRef,
|
||
forbidden_state: Option<ObjectRef>,
|
||
) -> Result<(), VmError> {
|
||
// 显式进入/退出栈模拟深度优先遍历;退出时移除祖先链标记,允许共享子树。
|
||
enum Step {
|
||
Enter(ObjectRef),
|
||
Exit(ObjectRef),
|
||
}
|
||
// validate 由新组成/共享赋值入口调用;显式栈保存尚待展开的进入/退出步骤。
|
||
let mut work = vec![Step::Enter(root)];
|
||
// 只记录当前 DFS 祖先 Cell,兄弟分支复用同一孩子是合法共享而非循环。
|
||
let mut path = std::collections::HashSet::new();
|
||
// 计数实际展开工作量,重复共享出现也计入 10000 上限。
|
||
let mut visits = 0;
|
||
// 弹出后进先出工作项,避免用宿主递归栈承载深层业务图。
|
||
while let Some(step) = work.pop() {
|
||
// 退出清祖先标记;进入检查预算、禁用目标和所有孩子,不产出任何部分 UI。
|
||
match step {
|
||
// 子树已完成后移除祖先,允许后面的兄弟再次使用同一 Cell。
|
||
Step::Exit(state) => {
|
||
path.remove(&state);
|
||
}
|
||
// 每次组成或转发出现都检查,不能按不同对象数去重而绕过展开预算。
|
||
Step::Enter(block) => {
|
||
// 预算按实际展开次数而非不同对象数计算,共享子树重复出现也计入资源工作量。
|
||
visits += 1;
|
||
// 展开超限明确拒绝,不能返回部分孩子让 UI 悄悄缺项。
|
||
if visits > 10_000 {
|
||
return Err(VmError::ExecutionLimit("expanded Block graph"));
|
||
}
|
||
// 要求每个待展开节点为 Block,取共享 Cell 作为路径身份。
|
||
let state = store.payload::<Block>(block, "Block")?.state;
|
||
// path 只追踪当前祖先 Cell,允许其他分支共享同一组件;forbidden_state 额外阻止新边回到将被修改的目标。
|
||
if forbidden_state == Some(state) || !path.insert(state) {
|
||
return Err(VmError::InvalidArgument(
|
||
"Block composition would create a cycle",
|
||
));
|
||
}
|
||
work.push(Step::Exit(state));
|
||
// 与 resolve 一样拒绝非对象 Cell,避免非法状态混进组成拥有链。
|
||
let Value::Object(value) = store.cell_get(state)? else {
|
||
return Err(VmError::InvalidArgument("invalid Block state"));
|
||
};
|
||
// Cell 当前值若直接指向将被修改的禁用目标,同样形成回边,不能只比较当前 Cell。
|
||
if forbidden_state == Some(value) {
|
||
return Err(VmError::InvalidArgument(
|
||
"Block composition would create a cycle",
|
||
));
|
||
}
|
||
// Data 展开孩子,非 Data 则沿共享赋值转发链继续;显式 Exit 在子树完成后清除祖先标记。
|
||
if let Ok(data) = store.payload::<Data>(value, "Block data") {
|
||
// 逆序压栈让检查按原孩子顺序展开;每次出现都验证,重复子树不能跳过预算。
|
||
for child in data.children.iter().rev() {
|
||
work.push(Step::Enter(child.block));
|
||
}
|
||
} else {
|
||
work.push(Step::Enter(value));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 用一次性空 RenderCache 提取不可变视觉树;不跨调用保存快照缓存。
|
||
pub fn snapshot(store: &ObjectStore, root: ObjectRef) -> Result<Arc<UiNode>, VmError> {
|
||
// 一次性提取使用空缓存,不保留跨调用结果;有增量复用需求的窗口应使用自己的 RenderCache。
|
||
crate::snapshot::RenderCache::default().snapshot(store, root)
|
||
}
|
||
|
||
/// Native visual parameters are ordinary managed attributes too. Composition,
|
||
/// histories and connections remain structural payload edges.
|
||
/// 列出本类型可投影到托管属性的原生字段及默认值。
|
||
pub fn native_properties(data: &Data) -> Vec<(&'static str, Value)> {
|
||
/// 把已有 f32 默认字段编码为源 Number;这里不做范围校验,读取解码由 apply_visual_property 处理。
|
||
fn number(value: f32) -> Value {
|
||
Value::Number((value as f64).into())
|
||
}
|
||
// 先枚举每种 Block 都有的定位、弹性、盒尺寸、边框与对齐属性,再按 kind 增补专有字段。
|
||
let mut fields = vec![
|
||
("x", number(data.position[0])),
|
||
("y", number(data.position[1])),
|
||
("flex", number(data.flex)),
|
||
(
|
||
"flexBasis",
|
||
data.flex_policy
|
||
.basis
|
||
.map_or_else(|| Value::Text("auto".into()), number),
|
||
),
|
||
("flexMin", number(data.flex_policy.min)),
|
||
("flexMax", number(data.flex_policy.max)),
|
||
("flexShrink", number(data.flex_policy.shrink)),
|
||
("marginTop", number(data.margin.top)),
|
||
("marginRight", number(data.margin.right)),
|
||
("marginBottom", number(data.margin.bottom)),
|
||
("marginLeft", number(data.margin.left)),
|
||
("boxWidth", data.sizing.width.value()),
|
||
("boxHeight", data.sizing.height.value()),
|
||
("minWidth", number(data.sizing.min_width)),
|
||
("minHeight", number(data.sizing.min_height)),
|
||
("maxWidth", number(data.sizing.max_width)),
|
||
("maxHeight", number(data.sizing.max_height)),
|
||
("borderTop", number(data.border.top)),
|
||
("borderRight", number(data.border.right)),
|
||
("borderBottom", number(data.border.bottom)),
|
||
("borderLeft", number(data.border.left)),
|
||
("borderColor", Value::Text(data.border_color.clone())),
|
||
(
|
||
"overflow",
|
||
Value::Text(
|
||
// 布尔内部状态编码成公开 overflow 策略文本,供源字段读取与后续解析一致往返。
|
||
if data.clip_children {
|
||
"clip"
|
||
} else {
|
||
"visible"
|
||
}
|
||
.into(),
|
||
),
|
||
),
|
||
(
|
||
"alignSelf",
|
||
Value::Text(
|
||
data.align_self
|
||
.map_or("auto", crate::layout::CrossAlign::name)
|
||
.into(),
|
||
),
|
||
),
|
||
];
|
||
// 按上右下左为四边输出覆盖值,None 与显式零必须保持可区分。
|
||
for (name, edge) in ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"]
|
||
.into_iter()
|
||
.zip(data.padding_edges)
|
||
{
|
||
fields.push((
|
||
name,
|
||
// 单边 padding 的 auto 编码成文本,不用数值哨兵,读取时才能区分“继承统一值”和显式零。
|
||
edge.map_or_else(|| Value::Text("auto".into()), number),
|
||
));
|
||
}
|
||
// 只为实际原生 kind 公开专有默认字段;没有对应字段的种类不凭同名自定义属性改变行为。
|
||
match &data.kind {
|
||
// 三维视口带有限宽高、场景、控制器和相机,None 控制器编码为明确文本 none。
|
||
Kind::Viewport3D { width, height } => fields.extend([
|
||
("width", number(*width)),
|
||
("height", number(*height)),
|
||
("scene", data.scene3d.map_or(Value::Nil, Value::Object)),
|
||
(
|
||
"controller",
|
||
data.controller
|
||
.map_or_else(|| Value::Text("none".into()), Value::Object),
|
||
),
|
||
("camera", data.camera.map_or(Value::Nil, Value::Object)),
|
||
]),
|
||
// 二维有限视口公开宽高和 Camera2D,Scene2D 仍通过结构 children 持有。
|
||
Kind::CanvasViewport { width, height } => fields.extend([
|
||
("width", number(*width)),
|
||
("height", number(*height)),
|
||
("camera", data.camera.map_or(Value::Nil, Value::Object)),
|
||
]),
|
||
// 兼容 Page 表面公开 mode/pan/zoom,不把导航历史作为普通视觉字段。
|
||
Kind::Page => fields.extend([
|
||
("mode", Value::Text(data.surface.mode.name().into())),
|
||
("panX", number(data.surface.pan.x)),
|
||
("panY", number(data.surface.pan.y)),
|
||
("zoom", number(data.surface.zoom)),
|
||
]),
|
||
// 文字的内容、离散字号及颜色进入普通属性存储,允许绑定和精确读观察。
|
||
Kind::Text { value, size, color } => fields.extend([
|
||
("text", Value::Text(value.clone())),
|
||
("size", Value::Number((*size as i64).into())),
|
||
("color", Value::Text(color.clone())),
|
||
]),
|
||
// 矩形默认几何与填充色可由托管属性覆盖,出现位置的实际分配另由布局决定。
|
||
Kind::Rect {
|
||
width,
|
||
height,
|
||
radius,
|
||
color,
|
||
} => fields.extend([
|
||
("width", number(*width)),
|
||
("height", number(*height)),
|
||
("radius", number(*radius)),
|
||
("color", Value::Text(color.clone())),
|
||
]),
|
||
// 滚动容器公开初始尺寸和轴策略,实际偏移不放到共享业务字段。
|
||
Kind::Scroll { width, height } => fields.extend([
|
||
("width", number(*width)),
|
||
("height", number(*height)),
|
||
("axis", Value::Text(data.scroll_axis.name().into())),
|
||
]),
|
||
// Spacer 仅公开自身自然间距 size。
|
||
Kind::Spacer(size) => fields.push(("size", number(*size))),
|
||
_ => {}
|
||
}
|
||
// 容器才暴露统一 padding/gap 与主交叉轴策略;叶节点不因同名自定义字段被当成通用容器。
|
||
if data.is_container() {
|
||
fields.extend([
|
||
("padding", number(data.padding)),
|
||
("gap", number(data.gap)),
|
||
("mainAlign", Value::Text(data.main_align.name().into())),
|
||
("crossAlign", Value::Text(data.cross_align.name().into())),
|
||
]);
|
||
}
|
||
fields
|
||
}
|
||
|
||
/// 复制原生描述并用当前托管属性覆盖它;结果只用于展示,不反向修改业务对象。
|
||
pub fn visual_data(store: &ObjectStore, data: &Data) -> Result<Data, VmError> {
|
||
// 只改本轮派生副本,逐个读取实际属性并验证;读取字段名集合来自 native_properties,不遍历任意用户字段。
|
||
let mut data = data.clone();
|
||
// 提取必须有托管字段存储;没有时说明分配图不完整,不能退回构造默认值。
|
||
let properties = data
|
||
.properties
|
||
.ok_or(VmError::InvalidArgument("missing Block properties"))?;
|
||
// 字段集合由原生种类确定,每项从当前存储读取,保留业务属性依赖以驱动后续失效。
|
||
for (name, _) in native_properties(&data) {
|
||
// 借用读取的源值先解码到副本,错误不会反写已存在的业务描述。
|
||
let value = store.get_attr(properties, name)?;
|
||
apply_visual_property(&mut data, name, &value)?;
|
||
}
|
||
// 各单值合法仍可能存在跨字段矛盾,必须在全部覆盖完成后校验 min/max 关系。
|
||
if data.flex_policy.min > data.flex_policy.max {
|
||
return Err(VmError::InvalidArgument("flexMin exceeds flexMax"));
|
||
}
|
||
// 全部字段解码后才比较两轴 min/max,避免字段覆盖次序造成错误地提前拒绝。
|
||
if data.sizing.min_width > data.sizing.max_width
|
||
|| data.sizing.min_height > data.sizing.max_height
|
||
{
|
||
return Err(VmError::InvalidArgument("box minimum exceeds maximum"));
|
||
}
|
||
// 有限二维视口另要求合法 Camera2D 与唯一 Scene2D 根,不能仅靠字段类型通过完整协议。
|
||
if matches!(data.kind, Kind::CanvasViewport { .. }) {
|
||
crate::viewport::camera(
|
||
store,
|
||
data.camera.ok_or(VmError::InvalidArgument(
|
||
"CanvasViewport requires a Camera2D",
|
||
))?,
|
||
)?;
|
||
// CanvasViewport 只容纳一个 Scene2D 根,允许其内部无限空间布局,但不接受多个普通孩子冒充场景。
|
||
if data.children.len() != 1
|
||
|| !matches!(
|
||
resolve(store, data.children[0].block)?.1.kind,
|
||
Kind::Scene2D
|
||
)
|
||
{
|
||
return Err(VmError::InvalidArgument(
|
||
"CanvasViewport requires exactly one Scene2D",
|
||
));
|
||
}
|
||
}
|
||
// 有自定义控制器时先验证名义方法;未指定 controller 则沿默认相机手势路径。
|
||
if matches!(data.kind, Kind::Viewport3D { .. })
|
||
&& let Some(controller) = data.controller
|
||
{
|
||
// 控制器必须有名义类型,不能依赖实例上的同名动态字段冒充 update 方法。
|
||
let definition = store.type_of(controller)?.ok_or(VmError::InvalidArgument(
|
||
"3D controller requires a nominal update method",
|
||
))?;
|
||
// 3D 控制器要求名义类型上的 update,接受含接收者在内三参;调用执行与预算在输入分发路径处理。
|
||
let method = store
|
||
.lookup_method(definition, "update")?
|
||
.ok_or(VmError::InvalidArgument(
|
||
"3D controller must define update(camera, gesture)",
|
||
))?;
|
||
// 包括 receiver 的三参形状需可接受;这里只校验,不在展示提取期间执行控制器副作用。
|
||
if store
|
||
.call_arity(method)?
|
||
.is_none_or(|arity| !arity.accepts(3))
|
||
{
|
||
return Err(VmError::InvalidArgument(
|
||
"3D controller update expects camera and CameraGesture",
|
||
));
|
||
}
|
||
}
|
||
Ok(data)
|
||
}
|
||
|
||
/// 按属性名检查和解码源值,再更新对应视觉字段;非法尺寸、枚举或颜色被拒绝。
|
||
pub fn apply_visual_property(data: &mut Data, name: &str, value: &Value) -> Result<(), VmError> {
|
||
// 绑定刷新在写回前、visual_data 在读取时调用本解码器;通用类型/范围失败统一为视觉属性错误。
|
||
let bad = || VmError::InvalidArgument("invalid native visual property");
|
||
// 常规视觉长度/系数只接受非负有限值;坐标和 pan 在下面走允许负数的独立路径。
|
||
let number = || -> Result<f32, VmError> {
|
||
// 仅接受名义 Number,拒绝可格式化成数字的字符串/对象。
|
||
let Value::Number(value) = value else {
|
||
return Err(bad());
|
||
};
|
||
// 转为布局使用的 f32 后再检查有限性和上界,防止大数溢出为无穷。
|
||
let n = value.to_string().parse::<f32>().map_err(|_| bad())?;
|
||
// 拒绝 NaN、无穷、负数和超过全局视觉长度预算的数,保护后续布局/像素尺寸计算。
|
||
if !n.is_finite() || !(0.0..=crate::ui_bridge::MAX_DIMENSION as f32).contains(&n) {
|
||
return Err(bad());
|
||
}
|
||
Ok(n)
|
||
};
|
||
// x/y 对所有组件生效;页面 pan/zoom 只作用 Page。这里不是任意字段存在性校验,不适用种类会跳过更新。
|
||
if matches!(name, "x" | "y" | "panX" | "panY" | "zoom") {
|
||
// 坐标路径允许带符号数,但仍使用 coordinate 的有限值/范围检查。
|
||
let n = crate::scroll::coordinate(value)?;
|
||
// x/y 更新业务位置;pan/zoom 只对 Page 解释,其他同名字段不影响原生描述。
|
||
match name {
|
||
"x" => data.position[0] = n,
|
||
"y" => data.position[1] = n,
|
||
"panX" if matches!(data.kind, Kind::Page) => data.surface.pan.x = n,
|
||
"panY" if matches!(data.kind, Kind::Page) => data.surface.pan.y = n,
|
||
"zoom" if matches!(data.kind, Kind::Page) => {
|
||
// Page 兼容相机缩放范围不同于独立 Camera2D 的 0.01..100,保持两套源协议各自限制。
|
||
if !(0.1..=4.0).contains(&n) {
|
||
return Err(invalid_zoom());
|
||
}
|
||
data.surface.zoom = n;
|
||
}
|
||
_ => {}
|
||
}
|
||
return Ok(());
|
||
}
|
||
// 旧 Page mode 仅接受可解析字符串,其他组件的自定义 mode 不在此解释。
|
||
if matches!(data.kind, Kind::Page) && name == "mode" {
|
||
// mode 的枚举解析只接受字符串;Number 等类型不做隐式转换。
|
||
let Value::Text(name) = value else {
|
||
return Err(bad());
|
||
};
|
||
data.surface.mode = crate::page::Mode::parse(name)?;
|
||
return Ok(());
|
||
}
|
||
// 先处理跨种类盒模型字段及视口引用,成功分支立即返回,避免落入后面的 kind 专有解码。
|
||
match name {
|
||
// controller 接受对象或明确的字符串 none,不能拿 Nil 隐式取消;后续 visual_data 检查其名义方法。
|
||
"controller" if matches!(data.kind, Kind::Viewport3D { .. }) => {
|
||
data.controller = match value {
|
||
Value::Object(controller) => Some(*controller),
|
||
Value::Text(text) if text == "none" => None,
|
||
_ => return Err(bad()),
|
||
};
|
||
return Ok(());
|
||
}
|
||
// camera/scene 这里仅检查 Object 并记录身份,实际 payload/结构验证由各视口快照入口继续执行。
|
||
"camera"
|
||
// 只有二维/三维视口把 camera 当作原生相机引用,其他种类不会因同名自定义字段改变协议。
|
||
if matches!(
|
||
data.kind,
|
||
Kind::CanvasViewport { .. } | Kind::Viewport3D { .. }
|
||
) =>
|
||
{
|
||
// 相机先要求对象身份,具体 2D/3D payload 与名义类型交由相应快照路径验证。
|
||
let Value::Object(camera) = value else {
|
||
return Err(bad());
|
||
};
|
||
data.camera = Some(*camera);
|
||
return Ok(());
|
||
}
|
||
"scene" if matches!(data.kind, Kind::Viewport3D { .. }) => {
|
||
// Scene3D 引用先保留业务身份,完整图验证发生在三维提取入口。
|
||
let Value::Object(scene) = value else {
|
||
return Err(bad());
|
||
};
|
||
data.scene3d = Some(*scene);
|
||
return Ok(());
|
||
}
|
||
"paddingTop" | "paddingRight" | "paddingBottom" | "paddingLeft" => {
|
||
// 四边覆盖数组固定按上右下左索引,保证与 native_properties 输出次序一致。
|
||
let index = match name {
|
||
"paddingTop" => 0,
|
||
"paddingRight" => 1,
|
||
"paddingBottom" => 2,
|
||
_ => 3,
|
||
};
|
||
// 单边 auto 清除覆盖值;数值零保留 Some(0),从而确实覆盖统一 padding。
|
||
data.padding_edges[index] = if matches!(value, Value::Text(text) if text == "auto") {
|
||
None
|
||
} else {
|
||
Some(number()?)
|
||
};
|
||
return Ok(());
|
||
}
|
||
// 上边框厚度占据边框盒内的顶侧空间,使用非负有限长度。
|
||
"borderTop" => {
|
||
data.border.top = number()?;
|
||
return Ok(());
|
||
}
|
||
// 右边框厚度用于内容区扣减和独立边框绘制。
|
||
"borderRight" => {
|
||
data.border.right = number()?;
|
||
return Ok(());
|
||
}
|
||
// 下边框厚度影响内容可用高,不能当成外 margin。
|
||
"borderBottom" => {
|
||
data.border.bottom = number()?;
|
||
return Ok(());
|
||
}
|
||
// 左边框厚度进入内容起点和裁剪区的横向内缩。
|
||
"borderLeft" => {
|
||
data.border.left = number()?;
|
||
return Ok(());
|
||
}
|
||
"borderColor" => {
|
||
// 边框颜色必须为字符串,再由 parse_color 验证实际颜色格式。
|
||
let Value::Text(color) = value else {
|
||
return Err(bad());
|
||
};
|
||
crate::document::parse_color(color).map_err(|_| bad())?;
|
||
data.border_color.clone_from(color);
|
||
return Ok(());
|
||
}
|
||
"overflow" => {
|
||
// overflow 是策略枚举,不接受 Bool 等近似表示,未知字符串明确报错。
|
||
let Value::Text(overflow) = value else {
|
||
return Err(bad());
|
||
};
|
||
// overflow 只切换可见/裁剪,不暗中启用滚动;需要偏移与输入消费必须使用 ScrollBlock。
|
||
data.clip_children = match overflow.as_str() {
|
||
"clip" => true,
|
||
"visible" => false,
|
||
_ => {
|
||
return Err(VmError::InvalidArgument(
|
||
"overflow expects visible or clip; use ScrollBlock for scrolling",
|
||
));
|
||
}
|
||
};
|
||
return Ok(());
|
||
}
|
||
// 边框盒宽可为 auto/fill/显式尺寸,由 Dimension 解析保留策略而非提前求像素。
|
||
"boxWidth" => {
|
||
data.sizing.width = crate::box_model::Dimension::parse(value)?;
|
||
return Ok(());
|
||
}
|
||
// 边框盒高同样保留 auto/fill 语义,有限父分配由布局阶段提供。
|
||
"boxHeight" => {
|
||
data.sizing.height = crate::box_model::Dimension::parse(value)?;
|
||
return Ok(());
|
||
}
|
||
// 记录宽度下限,min/max 的组合一致性等全部字段覆盖后检查。
|
||
"minWidth" => {
|
||
data.sizing.min_width = number()?;
|
||
return Ok(());
|
||
}
|
||
// 记录高度下限,参与布局夹取且不能为负。
|
||
"minHeight" => {
|
||
data.sizing.min_height = number()?;
|
||
return Ok(());
|
||
}
|
||
// 记录有限宽度上限,约束 fill/stretch 与显式尺寸的最终结果。
|
||
"maxWidth" => {
|
||
data.sizing.max_width = number()?;
|
||
return Ok(());
|
||
}
|
||
// 记录有限高度上限,避免 stretch 绕过盒尺寸边界。
|
||
"maxHeight" => {
|
||
data.sizing.max_height = number()?;
|
||
return Ok(());
|
||
}
|
||
// 更新顶侧外边距,影响父交叉轴/主轴定位而不改子项边框盒厚度。
|
||
"marginTop" => {
|
||
data.margin.top = number()?;
|
||
return Ok(());
|
||
}
|
||
// 更新右侧外边距,参与父自然宽及剩余主轴空间扣减。
|
||
"marginRight" => {
|
||
data.margin.right = number()?;
|
||
return Ok(());
|
||
}
|
||
// 更新底侧外边距,参与父自然高及后续子项排列。
|
||
"marginBottom" => {
|
||
data.margin.bottom = number()?;
|
||
return Ok(());
|
||
}
|
||
// 更新左侧外边距,参与子项起点和父宽度分配。
|
||
"marginLeft" => {
|
||
data.margin.left = number()?;
|
||
return Ok(());
|
||
}
|
||
// basis 的 Nil 与 auto 都表示内容基准;其他值使用非负长度检查。flexMin/Max 的相互关系在完整提取后验证。
|
||
"flexBasis" => {
|
||
data.flex_policy.basis = if matches!(value, Value::Nil)
|
||
|| matches!(value, Value::Text(text) if text == "auto")
|
||
{
|
||
None
|
||
} else {
|
||
Some(number()?)
|
||
};
|
||
return Ok(());
|
||
}
|
||
// 弹性主轴分配下限,与 flexMax 的大小关系由 visual_data 整体检查。
|
||
"flexMin" => {
|
||
data.flex_policy.min = number()?;
|
||
return Ok(());
|
||
}
|
||
// 弹性主轴分配上限,达到边界后剩余空间可重新分给其他项。
|
||
"flexMax" => {
|
||
data.flex_policy.max = number()?;
|
||
return Ok(());
|
||
}
|
||
// 主轴不足时的收缩权重,零表示不参与该项加权收缩。
|
||
"flexShrink" => {
|
||
data.flex_policy.shrink = number()?;
|
||
return Ok(());
|
||
}
|
||
_ => {}
|
||
}
|
||
// 通用增长权重也按非负有限数解码;它不等价于 boxWidth=fill。
|
||
if name == "flex" {
|
||
data.flex = number()?;
|
||
return Ok(());
|
||
}
|
||
// 后续只有容器解释 mainAlign/crossAlign/padding/gap,叶子同名自定义字段保持业务含义。
|
||
let container = data.is_container();
|
||
// alignSelf 可由任何孩子独立声明,crossAlign 只解释容器配置;auto 恢复继承父交叉轴策略。
|
||
if name == "alignSelf" || (container && name == "crossAlign") {
|
||
// alignSelf/crossAlign 都是字符串策略,先检查类型再解析 auto 或具体对齐方式。
|
||
let Value::Text(value) = value else {
|
||
return Err(bad());
|
||
};
|
||
// 孩子可用 auto 取消自身覆盖;父容器 crossAlign 则必须是具体策略。
|
||
if name == "alignSelf" {
|
||
data.align_self = if value == "auto" {
|
||
None
|
||
} else {
|
||
Some(crate::layout::CrossAlign::parse(value)?)
|
||
};
|
||
} else {
|
||
data.cross_align = crate::layout::CrossAlign::parse(value)?;
|
||
}
|
||
return Ok(());
|
||
}
|
||
// 主轴对齐仅供容器使用,要求枚举文本并交给统一解析器验证。
|
||
if container && name == "mainAlign" {
|
||
// 主轴策略不能用数值或对象代替文本,统一从 MainAlign 解析允许集合。
|
||
let Value::Text(value) = value else {
|
||
return Err(bad());
|
||
};
|
||
data.main_align = crate::layout::MainAlign::parse(value)?;
|
||
return Ok(());
|
||
}
|
||
// 最后按 kind 解释自然尺寸、文本、颜色与滚动轴;不存在的原生字段不改业务属性本身。
|
||
match (&mut data.kind, name) {
|
||
// 三维视口自然宽高采用相同长度限制,实际分配还受盒模型与父约束影响。
|
||
(Kind::Viewport3D { width, .. }, "width") => *width = number()?,
|
||
(Kind::Viewport3D { height, .. }, "height") => *height = number()?,
|
||
// 二维视口自然尺寸有限,不能因世界内容无界而让视口本身无限展开。
|
||
(Kind::CanvasViewport { width, .. }, "width") => *width = number()?,
|
||
(Kind::CanvasViewport { height, .. }, "height") => *height = number()?,
|
||
(Kind::Text { value: text, .. }, "text") => {
|
||
// 原生文字内容只接受源字符串,不能将对象 repr 自动变为显示内容。
|
||
let Value::Text(value) = value else {
|
||
return Err(bad());
|
||
};
|
||
// 文本属性按 UTF-8 字节设 16 KiB 上限,防止单个 Block 超过原生视觉文本预算。
|
||
if value.len() > 16_384 {
|
||
return Err(bad());
|
||
}
|
||
text.clone_from(value);
|
||
}
|
||
(Kind::Text { size, .. }, "size") => {
|
||
// 文字 size 先通过通用数值界限,再限制为离散倍率,不能截断小数。
|
||
let n = number()?;
|
||
// Text size 是 1..8 的离散倍率,不能把任意浮点字号通过 as u32 悄悄截断。
|
||
if !(1.0..=8.0).contains(&n) || n.fract() != 0.0 {
|
||
return Err(bad());
|
||
}
|
||
*size = n as u32;
|
||
}
|
||
(Kind::Text { color, .. } | Kind::Rect { color, .. }, "color") => {
|
||
// 文字/矩形填充色共用字符串检查;通过格式验证后才替换派生值。
|
||
let Value::Text(value) = value else {
|
||
return Err(bad());
|
||
};
|
||
// 颜色赋值先验证文本格式再替换派生颜色,避免错误字符串进入软件渲染器。
|
||
crate::document::parse_color(value).map_err(|_| bad())?;
|
||
color.clone_from(value);
|
||
}
|
||
(Kind::Scroll { .. }, "axis") => {
|
||
// ScrollBlock.axis 必须为字符串,再验证允许的 both/vertical/horizontal 集合。
|
||
let Value::Text(name) = value else {
|
||
return Err(bad());
|
||
};
|
||
data.scroll_axis = crate::scroll::Axis::parse(name)?;
|
||
}
|
||
// 滚动视口自然宽高与内容尺寸分开;真实分配可能被源 arrange 或盒模型覆盖。
|
||
(Kind::Scroll { width, .. }, "width") => *width = number()?,
|
||
(Kind::Scroll { height, .. }, "height") => *height = number()?,
|
||
// 矩形自然尺寸及圆角都按非负有限长度解码,不直接写任何 Mount 的排列结果。
|
||
(Kind::Rect { width, .. }, "width") => *width = number()?,
|
||
(Kind::Rect { height, .. }, "height") => *height = number()?,
|
||
(Kind::Rect { radius, .. }, "radius") => *radius = number()?,
|
||
// Spacer 自然间距使用同一数值界限,方向由所在布局协议决定。
|
||
(Kind::Spacer(size), "size") => *size = number()?,
|
||
// 剩余 kind 专有尺寸与容器字段按各自规则覆盖;未知字段忽略,不在此处删除或修改用户自定义属性。
|
||
(_, "padding") if container => data.padding = number()?,
|
||
(_, "gap") if container => data.gap = number()?,
|
||
_ => {}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 统一返回非法页面缩放错误,确保属性写入与输入路径使用相同限制。
|
||
fn invalid_zoom() -> VmError {
|
||
VmError::InvalidArgument("Page.zoom expects a number from 0.1 to 4")
|
||
}
|