//! 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, } impl ObjectPayload for Block { /// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。 fn reference_assignment_only(&self) -> bool { true } /// 沿 Block 的共享 Cell 定位属性存储,使别名替换后读取到同一份新 Data。 fn attribute_target(&self, store: &ObjectStore) -> Result, 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::() { Ok(Some(value)) } else { store .payload::(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, 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::() { Ok(Some(value)) } else { store.payload::(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, VmError> { // VM 询问赋值是否共享 Cell 时,只接收另一 Block 包装;其他值交回常规赋值规则。 if !store.object(source)?.payload().as_any().is::() { return Ok(None); } // Page 与普通组件不能交叉共享赋值,防止把窗口/页面生命周期能力替换成普通叶组件。 if store.payload::(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, // 容器统一内边距,单边 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, // 子项外边距,参与父固有尺寸与分配,不包含在本项边框盒宽高中。 pub margin: crate::layout::Insets, // 边框盒 auto/fill/显式尺寸及 min/max 限制,解码后由布局应用。 pub sizing: crate::box_model::Sizing, // 四边覆盖顺序为上、右、下、左;None 表示回退统一 padding,不等于零。 pub padding_edges: [Option; 4], // 四边边框厚度,占据边框盒内部空间,并生成独立绘制命令。 pub border: crate::layout::Insets, // 已验证颜色文本,供边框绘制使用;不是业务内容自身的填充色。 pub border_color: String, // overflow=clip 时限制后代的绘制与命中,不自动赋予滚动能力。 pub clip_children: bool, // CanvasViewport/Viewport3D 使用的相机强引用;共享相机需源代码显式复用对象。 pub camera: Option, // 三维视口的 Scene3D 业务根,面板 UI 由场景提取流程展开。 pub scene3d: Option, // 三维视口自定义控制器;提取时检查名义 update,输入路径才调用它。 pub controller: Option, // 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, // Page 当前导航历史位置,与 history 一起维护前进/后退关系。 pub history_index: usize, // 目标拥有的属性连接,连接再持有 compute;取消令牌不反向保活目标。 pub bindings: Vec, // 旧 textOf 源对象与字段名推导入口;通用 text 绑定建立后清除以避免竞争。 pub text_binding: Option<(ObjectRef, String)>, // 兼容 onClick 直接回调,事件路径调用,GC 需保活该函数。 pub click: Option, // 对象拥有的事件连接,派发按这些连接决定监听器资格。 pub subscriptions: Vec, // 源可读写的视觉字段存储;Data 自身保存结构边与构造默认值,提取时再将当前属性覆盖到派生副本。 pub properties: Option, } impl Data { /// 建立组件业务描述,为初始孩子分配稳定 slot,并初始化无绑定、无订阅的默认布局策略。 pub fn new(kind: Kind, children: Vec, 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 { // 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), 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::(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::() { dependencies.push(value); return Ok(( value, store.payload::(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 次展开的组成。 /// allocate/Scene3D 面板构造传 None,只检查已有图;共享赋值传目标 Cell, /// Children 的 append/insert/replace 和 Page 的 render/navigate 则传目标 Data。 /// 因此 forbidden_state 虽沿用 state 名称,实际可以是 Cell 或 Data 身份;二者都不能在新子图中出现。 pub fn validate( store: &ObjectStore, root: ObjectRef, forbidden_state: Option, ) -> 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")?.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")); }; // Children/Page 编辑传入的是 Data 身份,要在此比较 Cell 的值;共享赋值传入的 Cell 已在上方比较。 // 例如给容器追加它自己的别名时,包装身份可能不同,最终 Data 相同仍须拒绝。 if forbidden_state == Some(value) { return Err(VmError::InvalidArgument( "Block composition would create a cycle", )); } // Data 展开孩子,非 Data 则沿共享赋值转发链继续;显式 Exit 在子树完成后清除祖先标记。 if let Ok(data) = store.payload::(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, 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. /// 列出本类型可投影到托管属性的原生字段及默认值。 /// allocate 用返回值初始化 properties;visual_data 用字段名重读当前值;RenderCache 用同一集合登记精确失效依赖。 /// 新增原生视觉字段时需同时补这里与 apply_visual_property,否则可能出现可解码但不进入快照/缓存观察的字段。 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 { // 只改本轮派生副本,逐个读取实际属性并验证;读取字段名集合来自 native_properties,不遍历任意用户字段。 let mut data = data.clone(); // 提取必须有托管字段存储;没有时说明分配图不完整,不能退回构造默认值。 let properties = data .properties .ok_or(VmError::InvalidArgument("missing Block properties"))?; // 字段集合由原生种类确定,每项从当前存储读取;RenderCache 调用方另用同一字段集合登记精确缓存依赖。 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) } /// 按属性名检查和解码源值,再更新对应视觉字段;非法尺寸、枚举或颜色被拒绝。 /// binding::refresh 在写入托管属性前用副本试解码;visual_data 在全量覆盖后另查 min/max 和视口协议。 /// 本函数只验证本次值并更新传入 Data,不自行写 properties,也不保证多个相关字段组合起来一定合法。 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 { // 仅接受名义 Number,拒绝可格式化成数字的字符串/对象。 let Value::Number(value) = value else { return Err(bad()); }; // 转为布局使用的 f32 后再检查有限性和上界,防止大数溢出为无穷。 let n = value.to_string().parse::().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") }