//! 每次展示的托管上下文:同一个 Block 出现在两个位置时,有两个独立 Mount。 //! Mount 保存父子关系、出现几何和布局期间的查询上下文;MountTable 用弱索引定位对象,不靠索引强行保活业务树。 //! 同步会安排 onMount/onUnmount。布局取消时使用 Checkpoint 恢复未发布状态,避免半完成的出现关系影响下一次刷新。 //! //! 调用路径:UiBridge 在布局前 checkpoint/synchronize(preparing=true),完成后提交几何,取消则 restore。 //! 源 measure/arrange/事件通过 Method 调用 Mount API;生命周期返回 vm_sequence 驱动,调用方安排实际执行。 //! 活动 Mount 由 desktop.mounts 强持有;MountTable 与反向层次关系仅保存弱索引。 //! //! Managed presentation contexts. The active desktop owns the mount dictionary; //! reverse links and the host registry are weak, so shared content cannot retain a Page. use crate::document::{Frame, MountGeometry}; use endor_core::{ ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef, Value, Vm, VmError, WeakObjectRef, }; use std::{ collections::HashMap, sync::{Arc, Mutex}, }; #[derive(Debug, Clone)] /// 一个组件在一个展示位置上的上下文。强持有 Block,反向页面/父子链接使用弱身份,避免共享组件把已关闭页面保活。 pub struct Mount { // 组件是强引用;页面、父节点和兄弟链接是弱引用,防止反向链接形成保活环。 pub block: ObjectRef, // 所属 Page 弱身份,用于区分换页后的出现上下文,不让共享内容反向保活页面。 pub page: Option, // 父出现弱身份,源 parent() 在目标已消失时返回 Nil。 pub parent: Option, // 首子出现弱身份,后续兄弟沿 next_sibling 查找。 pub first_child: Option, // 同级下一出现弱身份,几何帧中的 ID 链在同步第二遍解析为此链接。 pub next_sibling: Option, // 桥接同步的几何与每次出现的滚动位置分开保存;准备期可暂存结构几何,不能据此认定 GUI 已发布。 pub geometry: MountGeometry, // 此出现的用户请求偏移;帧中 geometry.scroll 才是钳制后的有效位置与上限。 pub scroll_offset: [f32; 2], // 最后同步的布局版本,Bounds 查询会一并返回;准备同步也会写本轮版本,不等于 GUI 已接收该版本。 pub version: u64, // false 后禁止依赖当前几何的操作;外部即使还持有 Mount 对象也不能把它当活动出现。 pub mounted: bool, // Mount 可以拥有任务;退役后任务交付还要检查 owner 是否仍有效。 pub tasks: Option, // Private derived scratch, installed before the read-only layout call and // cleared by geometry commit. It contains no managed references or roots. // 布局只读协议使用的临时派生上下文;不拥有托管根,提交或退役时清空。 pub layout: Option>>, } impl ObjectPayload for Mount { /// 声明此 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-mount" } /// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。 fn allows_attributes(&self) -> bool { true } /// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。 // 仅 block 和 tasks 是 payload 的强拥有边;page/parent/child/sibling 是弱边,不参与 trace。 fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { visit(self.block); // Aster GC 只额外追踪存在的任务作用域,弱页面/层次链不参与强引用追踪。 if let Some(tasks) = self.tasks { visit(tasks); } } } #[derive(Debug, Clone, Copy)] /// 该原生能力的查询/操作分类,方法包装用它选择具体实现。 enum Query { // 读取本出现已提交或排列期可用的 Bounds;测量阶段不可提前读几何。 Bounds, // 设置此出现的绝对滚动请求,实际范围由滚动层钳制。 ScrollTo, // 在此出现请求偏移上累加位移,不写共享 Block 的滚动状态。 ScrollBy, // 查询有效滚动位置和最大值,不直接返回未钳制请求。 ScrollPosition, // 联动滚动祖先,把此出现送入有效可见区域。 ScrollIntoView, // 用此视口有限尺寸和场景边界拟合相机。 FitView, // 从此三维视口查询射线交点和原托管 mesh。 Pick3D, // 本出现局部坐标经二维/面板投影变为页面视图坐标。 ToView, // 页面视图点逆投影到本出现局部坐标,失败可返回 Nil。 ToLocal, // 弱查询父 Mount,不制造新的父拥有关系。 Parent, // 弱查询首子 Mount,空或失效均返回 Nil。 FirstChild, // 弱查询下一同级出现,不按相同业务对象去重。 NextSibling, // 弱查询所属页面,退役上下文可能仍有页面弱观察。 Page, // 请求滚入可见并取得焦点,真正窗口发布在后续阶段完成。 Focus, // 只查询此 Mount 的提交几何包含关系,不执行源 Block.hitTest。 HitTest, } #[derive(Debug)] /// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。 struct Method { // 本桌面名义类型构造的绑定方法契约,Bounds 返回类型也从此提取。 signature: endor_core::CallSignature, // 方法实际绑定的 Mount,由 ContextNativeFunction captures 保活。 owner: ObjectRef, // 方法对象拥有 Mount,但不能经桌面反向保活整个窗口;需要桌面能力时再 upgrade。 desktop: WeakObjectRef, // 操作分派枚举,同时决定显式 arity、效果和返回契约。 query: Query, } impl NativeCallable for Method { /// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。 fn signature(&self) -> Option { Some(self.signature.clone()) } /// 适配器已绑定 Mount;坐标测试/转换与scrollTo/By接受两个数,其余层次/几何/焦点查询零参。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact( // 这些坐标/滚动方法需要两个 Number,其余操作无显式实参,receiver 已捕获。 if matches!( self.query, Query::HitTest | Query::Pick3D | Query::ToView | Query::ToLocal | Query::ScrollTo | Query::ScrollBy ) { 2 } else { 0 }, )) } /// 读取具体出现位置,先检查仍挂载与几何可用性,再执行层次、坐标、焦点、滚动或拾取操作,避免使用已退役上下文。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { // VM 调用绑定方法后先复制当前 payload,避免分配返回对象或改堆时持有旧存储借用。 let mount = cx.store().payload::(self.owner, "Mount")?.clone(); // 层次查询允许观察已经脱离的对象(通常得到 Nil);几何、焦点、滚动、拾取等必须仍挂接。 if !mount.mounted && matches!( self.query, Query::Bounds | Query::Focus | Query::HitTest | Query::ScrollTo | Query::ScrollBy | Query::ScrollIntoView | Query::FitView | Query::Pick3D | Query::ToView | Query::ToLocal | Query::ScrollPosition ) { return Err(VmError::InvalidArgument("Mount is no longer attached")); } // 除 VM 元数据检查外入口再核对实际参数数目;二维坐标/位移操作取两个 Number,其余零参。 let arity = if matches!( self.query, Query::HitTest | Query::Pick3D | Query::ToView | Query::ToLocal | Query::ScrollTo | Query::ScrollBy ) { 2 } else { 0 }; // 入口再次核对实际参数数目,防止绕过元数据路径导致 args 下标越界。 if args.len() != arity { return Err(VmError::InvalidArgument("incorrect Mount method arity")); } // 所有分支统一产生 Value,再交给 NativeResult::Return 的 VM 拥有关系协议。 let value = match self.query { // 坐标互转都要求已经提交的几何,不能混用本轮布局临时 bounds 与旧相机映射。 Query::ToView | Query::ToLocal => { // 布局期间几何尚未提交,坐标转换不能把临时 bounds 与上帧投影混用。 if mount.layout.is_some() { return Err(VmError::InvalidArgument( "coordinate conversion requires committed geometry", )); } // 显式输入是本地/视图坐标,按 coordinate 规则拒绝非法数值后再做空间转换。 let point = crate::geometry::Point::new( crate::scroll::coordinate(&args[0])?, crate::scroll::coordinate(&args[1])?, ); // 根据查询方向决定正向或逆向映射;Option 保留不可投影/遮挡的失败结果。 let point = // 查询方向决定先正投影局部点还是逆投影视图点;不能仅交换输出字段代替逆变换。 if matches!(self.query, Query::ToView) { // ToView 先将局部点加节点原点,再套二维 view;若在世界面板内,还继续向外投影到页面。 // 先加本节点布局原点再套累计二维 view,之后如有 panel 再投影到外层页面。 let point = mount.geometry.view.unwrap_or_default().point( crate::geometry::Point::new( point.x + mount.geometry.x, point.y + mount.geometry.y, ), ); mount .geometry .panel .as_ref() .map_or(Some(point), |panel| panel.project_point(point)) } else { // ToLocal 先撤销面板和视图投影,再减节点几何原点;不可逆时返回 Nil,而非虚构坐标。 mount.geometry.logical_point(point).map(|point| { crate::geometry::Point::new( point.x - mount.geometry.x, point.y - mount.geometry.y, ) }) }; // 转换成功才分配名义 Point;失败返回 Nil,不把无效数值写进结果属性。 if let Some(point) = point { // 成功后创建名义 Point 的字段存储,不把转换结果作为未标类型的近似对象返回。 let result = cx.new_object()?; // 需升级方法捕获的弱桌面以取得 Point 类型,桌面失效时明确报错。 let desktop = cx.upgrade(self.desktop)?.ok_or(VmError::InvalidArgument( "coordinate conversion desktop expired", ))?; crate::ui_bridge::attach_desktop_type(cx, result, "Point", desktop)?; cx.set_attr(result, "x", crate::native_visual::value(point.x))?; cx.set_attr(result, "y", crate::native_visual::value(point.y))?; Value::Object(result) } else { Value::Nil } } // 同一滚动入口区分绝对位置和相对增量,钳制、挂接状态与写屏障交由 scroll::set 统一执行。 Query::ScrollTo | Query::ScrollBy => { crate::scroll::set( cx, self.owner, self.desktop, [ crate::scroll::coordinate(&args[0])?, crate::scroll::coordinate(&args[1])?, ], matches!(self.query, Query::ScrollBy), )?; Value::Nil } // 滚动层负责嵌套祖先、裁剪与偏移夹取,此适配器只提供具体出现身份。 Query::ScrollIntoView => { crate::scroll::into_view(cx, self.owner, self.desktop)?; Value::Nil } // 拟合以 Mount 的有限视口尺寸为准;不是全局 Page 相机重置。 Query::FitView => { crate::viewport::fit(cx, &mount)?; Value::Nil } // 射线拾取需要所属桌面构造名义命中值,先确认弱桌面仍有效。 Query::Pick3D => { // 升级拾取所需桌面,不能通过旧 Mount 在已回收窗口环境里构造类型。 let desktop = cx .upgrade(self.desktop)? .ok_or(VmError::InvalidArgument("viewport desktop expired"))?; crate::scene3d_model::pick( cx, desktop, &mount, crate::geometry::Point::new( crate::scroll::coordinate(&args[0])?, crate::scroll::coordinate(&args[1])?, ), )? } // 返回帧中已钳制的有效位置与最大偏移,不直接暴露可能尚未投影的 scroll_offset 原值。 Query::ScrollPosition => { // 只有 geometry.scroll 存在的出现才可查询滚动状态,普通节点明确报错。 let p = mount .geometry .scroll .ok_or(VmError::InvalidArgument("Mount is not scrollable"))?; // 滚动查询返回 x/y/maxX/maxY 的新记录,字段源于帧有效投影。 let result = cx.new_object()?; // 逐项把有效偏移与上限写入返回记录,不暴露内部可变数组。 for (name, value) in [("x", p[0]), ("y", p[1]), ("maxX", p[2]), ("maxY", p[3])] { cx.set_attr(result, name, Value::Number((value as f64).into()))?; } Value::Object(result) } Query::Bounds => { // source_layout::Step 在调用 arrange 前填临时 Context.bounds;measure 阶段保持 None,避免读取尚未决定的分配。 // 布局只读调用中可查询当前安排阶段的临时 bounds;测量时尚无 bounds,明确报错。 // 非布局期则读取最后提交的 MountGeometry。 let geometry = if let Some(layout) = &mount.layout { layout .lock() .map_err(|_| VmError::InvalidArgument("layout context poisoned"))? .bounds .ok_or(VmError::InvalidArgument( "Mount geometry is unavailable during measurement", ))? } else { [ mount.geometry.x, mount.geometry.y, mount.geometry.width, mount.geometry.height, ] }; // 创建 Bounds 结果对象;名义定义由本方法签名保证与当前桌面一致。 let bounds = cx.new_object()?; // 从本方法返回签名取得 Bounds 的名义类型,避免生成一个仅字段相似的普通对象。 let endor_core::CallType::Single(endor_core::AnnotationType::Nominal(definition)) = &self.signature.returns else { return Err(VmError::InvalidArgument("invalid Mount bounds signature")); }; cx.attach_type(bounds, *definition)?; // 按 x/y/width/height 写入所选几何,另附 mount.version 作为 layoutVersion。 for (name, value) in [ ("x", geometry[0]), ("y", geometry[1]), ("width", geometry[2]), ("height", geometry[3]), ] { cx.set_attr(bounds, name, Value::Number((value as f64).into()))?; } cx.set_attr( bounds, "layoutVersion", Value::Number((mount.version as i64).into()), )?; Value::Object(bounds) } // 请求焦点先让出现进入可见范围,再检查可聚焦资格并写入桌面请求;后续发布负责实际生效。 Query::Focus => { crate::scroll::into_view(cx, self.owner, self.desktop)?; // 只有可交互出现可以聚焦;滚入可见请求先由滚动层处理,焦点资格不由字符串类型名决定。 if !mount.geometry.interactive { return Err(VmError::InvalidArgument("Mount is not focusable")); } // 焦点写入当前桌面的待处理请求,弱桌面过期则不能再设置。 let desktop = cx .upgrade(self.desktop)? .ok_or(VmError::InvalidArgument("Mount window has expired"))?; cx.set_attr( desktop, "focus", Value::Number((mount.geometry.id as i64).into()), )?; cx.set_attr(desktop, "focusRequested", Value::Bool(true))?; Value::Nil } // 这里是 Mount 已提交几何的 contains 查询;源 Block 的 hitTest 覆盖由 source_hit 驱动另行执行。 Query::HitTest => { // 布局临时上下文存在时 contains 不保证对应最终投影,必须拒绝源 Mount.hitTest。 if mount.layout.is_some() { return Err(VmError::InvalidArgument( "Mount hitTest is unavailable during layout", )); } // 命中坐标只要求 Number 可转有限 f32;与 scroll::coordinate 不同,此处不限制绝对值至一百万。 // hitTest 的输入只需有限 f32;此闭包刻意不用带一百万绝对值上限的滚动坐标解析。 let number = |value: &Value| -> Result { // 只接受 Number,不把字符串或其他值隐式转为坐标。 let Value::Number(number) = value else { return Err(VmError::InvalidArgument( "point coordinates must be numbers", )); }; // 解析到实际几何使用的 f32,溢出为无穷仍由下一步拒绝。 let number = number .to_string() .parse::() .map_err(|_| VmError::InvalidArgument("invalid point coordinate"))?; // NaN/无穷没有有效包含关系,不能送入 Rect 或面板射线计算。 if !number.is_finite() { return Err(VmError::InvalidArgument("invalid point coordinate")); } Ok(number) }; Value::Bool(mount.geometry.contains(crate::geometry::Point::new( number(&args[0])?, number(&args[1])?, ))) } // 其余枚举仅为弱层次查询,允许观察已脱离上下文;穷尽匹配保证不会把几何操作落到此处。 query => { // 选择对应弱边再尝试升级,不额外保存或强连父/子/页面。 let weak = match query { Query::Parent => mount.parent, Query::FirstChild => mount.first_child, Query::NextSibling => mount.next_sibling, Query::Page => mount.page, _ => unreachable!(), }; // 弱链接不存在或目标已回收都返回 Nil;升级本身失败才传播 VM 错误。 weak.map(|weak| cx.upgrade(weak)) .transpose()? .flatten() .map(Value::Object) .unwrap_or(Value::Nil) } }; Ok(NativeResult::Return(value)) } } /// 声明 Mount 查询方法的源类型契约,区分几何、层次与坐标操作。 fn mount_signature( store: &endor_core::ObjectStore, desktop: ObjectRef, query: Query, ) -> Result { use endor_core::{AnnotationType as T, CallEffect, CallSignature, CallType}; // 每个返回名义类型从该桌面 types 获取,不能拿另一 VM 同名类型拼接契约。 let nominal = |name| crate::ui_bridge::desktop_type(store, name, desktop).map(T::Nominal); // 层次、拾取和坐标转换可能没有结果,使用 nullable 名义类型;变更操作返回 Unit。 let returns = match query { // bounds 总返回 Bounds,测量期不可用时走错误而非 nullable。 Query::Bounds => nominal("Bounds")?.into(), // 层次弱边可能为空或目标已回收,因此结果允许 Nil。 Query::Parent | Query::FirstChild | Query::NextSibling => { CallType::nullable(nominal("Mount")?) } Query::Page => CallType::nullable(nominal("Page")?), Query::HitTest => T::Bool.into(), Query::Pick3D => CallType::nullable(nominal("RayHit3D")?), Query::ToView | Query::ToLocal => CallType::nullable(nominal("Point")?), // 滚动位置目前返回普通记录,契约 Any 与实现字段记录保持一致。 Query::ScrollPosition => T::Any.into(), _ => T::Unit.into(), }; // 滚动、相机拟合和焦点会写业务/桌面属性,声明 Mutating;其余查询允许在只读协议中调用。 let effect = if matches!( query, Query::ScrollTo | Query::ScrollBy | Query::ScrollIntoView | Query::FitView | Query::Focus ) { CallEffect::Mutating } else { CallEffect::ReadOnly }; // 显式参数契约与 Method.arity 保持相同分组,receiver 不计入已绑定实参。 let parameters = if matches!( query, Query::HitTest | Query::Pick3D | Query::ToView | Query::ToLocal | Query::ScrollTo | Query::ScrollBy ) { vec![T::Number, T::Number] } else { vec![] }; // 先构造参数与效果,再替换精确返回契约,保留 nullable 名义类型信息。 let mut signature = CallSignature::exact(parameters, T::Any, effect); signature.returns = returns; Ok(signature) } #[derive(Debug, Default, Clone)] /// 活动出现的弱索引。真实托管拥有关系在 desktop 的挂载字典中,索引不能充当隐藏 GC 根。 pub struct MountTable { // Mount ID 到托管弱身份,真实强拥有者是 desktop.mounts 字典;order 单独保存原布局顺序。 pub active: HashMap, // 原布局先序 ID 列表,弱 HashMap 本身无序,生命周期退役需要这个确定顺序。 order: Vec, } #[derive(Debug)] /// 同步出现关系时的共享临时工作状态,保存本轮重用、建立和退役安排。 /// MountTable::synchronize 包装它为零参 callable,让全部分配与写屏障经过 NativeContext;用户生命周期回调另由返回驱动执行。 struct Sync { // 本次同步所属桌面,由同步函数 captures 显式保活,持有活动挂载字典。 desktop: ObjectRef, // 本次活动 Page,可为空;同步内部转弱身份写入 Mount,避免反向保活。 page: Option, // 业务 Block 弱注册表,用它把发布帧中的身份文本解析为现存对象。 nodes: Vec, // 上一活动 Mount 弱表,用于按出现 ID 尝试复用。 old: HashMap, // 上一帧先序出现顺序,逆序退役用于保证孩子先于父卸载。 old_order: Vec, // 本轮拥有的原生几何帧,数据不依赖外部借用生命周期。 frame: Frame, // true 为布局准备阶段,安装临时 Context 并保留已有滚动偏移;false 为几何提交,清 Context。 preparing: bool, // 同步成功后交回新弱索引,宿主函数调用结束后由 MountTable 移出。 output: Arc>>, } #[derive(Debug)] /// 将一个生命周期回调及参数接入同一 VM 调用序列。 struct LifecycleCall { // 已经按 Block 名义类型解析到的生命周期源/原生方法。 function: ObjectRef, // 调用时显式 receiver,包装 captures 保持它到序列实际执行。 block: ObjectRef, // 当前具体出现上下文,作为第二参数传入同一个业务 Block 的钩子。 mount: ObjectRef, // 卸载包装先取消 owner 任务;挂载包装不做取消。 unmount: bool, } impl NativeCallable for LifecycleCall { /// Block、Mount 与用户钩子都已捕获,生命周期序列无参调用此适配器。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// 卸载时先取消 Mount 所有任务,再以 Block/Mount 为实参请求 VM 执行用户 onMount/onUnmount,不在 Rust 中直接运行源体。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 用户 onUnmount 运行前先取消该出现拥有的任务,防止退役后继续交付旧工作。 if self.unmount { crate::tasks::cancel_owner(cx, self.mount)?; } Ok(NativeResult::Invoke { function: self.function, arguments: vec![Value::Object(self.block), Value::Object(self.mount)], }) } } /// 构造生命周期调用所需的托管函数与参数,使回调遵循同一 VM 的引用和执行约束。 fn lifecycle_call( cx: &mut NativeContext<'_>, mount: ObjectRef, name: &'static str, ) -> Result { // Sync 为每个新增/退役出现建立包装,从 Mount 找到实际业务 receiver。 let block = cx.store().payload::(mount, "Mount")?.block; // 生命周期通过名义类型查找,不能被普通实例同名字段绕开协议。 let definition = cx .store() .type_of(block)? .ok_or(VmError::InvalidArgument("untyped mounted Block"))?; // 按 onMount/onUnmount 查真实方法,缺失类型/方法都表示协议未安装完整。 let function = cx .store() .lookup_method(definition, name)? .ok_or(VmError::InvalidArgument("Block has no lifecycle method"))?; // 生命周期包装显式捕获函数、Block、Mount,使序列执行到回调前这些对象不会被回收。 cx.allocate(ContextNativeFunction::new( name, LifecycleCall { function, block, mount, unmount: name == "onUnmount", }, vec![function, block, mount], )) } impl NativeCallable for Sync { /// 展示同步步骤从捕获的帧与弱节点表读取输入,无需外部实参。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// 把弱业务身份升级为本轮对象映射,再同步帧中出现位置到托管 Mount,建立结构/几何与待通知生命周期。 fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result { // 帧只含 Block 字符串身份,先将弱节点表升级为本次同步可用的实际对象映射。 let mut blocks = HashMap::new(); // 弱注册表不保活隐藏业务树;只升级当前仍存活的 Block 为本次调用临时根。 for weak in &self.nodes { // 失效弱节点直接忽略,帧若仍引用其身份则在下面明确拒绝。 if let Some(block) = cx.upgrade(*weak)? { blocks.insert(block.to_string(), block); } } // 把捕获的活动 Page 转为弱身份写进各 Mount,用于上下文比较和源码 page()。 let page = self .page .map(|page| cx.store().downgrade(page)) .transpose()?; // 新帧 ID→托管 Mount 临时映射,第一遍全部建立后再填关系。 let mut objects = HashMap::new(); // 记录真实新建出现的先序次序,最后仅这些对象安排 onMount。 let mut created = Vec::new(); // 第一遍按帧顺序为每个几何找业务 Block 并复用/分配 Mount。 for geometry in &self.frame.mounts { // 帧 block_id 只能对应仍可升级的业务对象,不能凭字符串新建一个替代身份。 let block = *blocks .get(&geometry.block_id) .ok_or(VmError::InvalidArgument( "frame references an unavailable Block", ))?; // 先按同一 Mount ID 找旧弱对象,失效项不具备复用资格。 let old = self .old .get(&geometry.id) .map(|weak| cx.upgrade(*weak)) .transpose()? .flatten(); // Mount ID 相同仍不足以复用:必须仍挂接、页面相同、真实 Block 相同,防止身份误归属。 let reusable = if let Some(old) = old { // 检查旧 Mount 的真实 payload,页面/业务对象不一致时即使数值 ID 相同也必须新建。 let mount = cx.store().payload::(old, "Mount")?; (mount.mounted && mount.page == page && mount.block == block).then_some(old) } else { None }; // 通过全部资格检查才复用,保留已有任务作用域和用户滚动偏移。 let object = if let Some(old) = reusable { old } else { // 新出现从无关系、零偏移、无任务和无临时 layout 开始,随后安装类型与方法。 let object = cx.allocate(Mount { block, page, parent: None, first_child: None, next_sibling: None, geometry: geometry.clone(), scroll_offset: [0.0, 0.0], version: self.frame.layout_version, mounted: true, tasks: None, layout: None, })?; crate::ui_bridge::attach_desktop_type(cx, object, "Mount", self.desktop)?; // 每个新 Mount 安装捕获自身的原生方法;captures 中只放 Mount,桌面仍保留弱身份。 for (name, query) in [ ("bounds", Query::Bounds), ("scrollTo", Query::ScrollTo), ("scrollBy", Query::ScrollBy), ("scrollPosition", Query::ScrollPosition), ("scrollIntoView", Query::ScrollIntoView), ("fitView", Query::FitView), ("pick3D", Query::Pick3D), ("toView", Query::ToView), ("toLocal", Query::ToLocal), ("parent", Query::Parent), ("firstChild", Query::FirstChild), ("nextSibling", Query::NextSibling), ("page", Query::Page), ("requestFocus", Query::Focus), ("hitTest", Query::HitTest), ] { // 方法函数只捕获本 Mount;弱桌面用于按需获取能力,避免方法反向保活整个窗口。 let function = cx.allocate(ContextNativeFunction::new( name, Method { signature: mount_signature(cx.store(), self.desktop, query)?, owner: object, desktop: cx.store().downgrade(self.desktop)?, query, }, vec![object], ))?; cx.set_attr(object, name, Value::Object(function))?; } cx.set_attr(object, "id", Value::Number((geometry.id as i64).into()))?; cx.set_attr(object, "block", Value::Object(block))?; created.push(object); object }; objects.insert(geometry.id, object); } // 先建立全部 Mount,再第二遍补链接,保证 parent/child/sibling 无论先后都能找到目标身份。 // 第二遍建立新的强拥有字典,桌面切换到此字典后才能释放旧出现集合。 let root = cx.new_dict()?; // 第二遍补齐所有弱层次关系和本轮几何,并将每个活动出现强持有在新字典中。 for geometry in &self.frame.mounts { // 所有 Mount 已先创建,parent/child/sibling 可指向帧中任意先后位置。 let object = objects[&geometry.id]; // 帧链接 ID 在完整临时映射中转弱对象身份;不让父子边额外形成强引用环。 let link = |id: Option| -> Result, VmError> { id.map(|id| cx.store().downgrade(objects[&id])).transpose() }; // 组装最终本轮 payload,任务作用域保留,关系/几何/版本使用本帧数据。 let mount = Mount { tasks: cx.store().payload::(object, "Mount")?.tasks, block: blocks[&geometry.block_id], page, parent: link(geometry.parent)?, first_child: link(geometry.first_child)?, next_sibling: link(geometry.next_sibling)?, geometry: geometry.clone(), // 准备阶段沿用用户滚动状态;提交阶段以帧中有效偏移为准,非滚动容器归零。 scroll_offset: if self.preparing { cx.store().payload::(object, "Mount")?.scroll_offset } else { geometry.scroll.map(|s| [s[0], s[1]]).unwrap_or([0.0, 0.0]) }, version: self.frame.layout_version, mounted: true, // 临时 Context 只有原生派生数据,无托管根;提交时置 None,表示几何可供外部查询。 layout: if self.preparing { Some(Arc::new(Mutex::new(crate::layout::Context::default()))) } else { None }, }; // 与旧 payload 比较避免无变化 replace_payload 造成大量 Contents 日志。 let old = cx.store().payload::(object, "Mount")?; // 只在上下文、几何、版本、链接或公开挂接属性确实需更新时替换 payload,减少无关变更日志。 if self.preparing || old.layout.is_some() || !old.mounted || old.version != mount.version || old.geometry != mount.geometry || old.page != mount.page || old.parent != mount.parent || old.first_child != mount.first_child || old.next_sibling != mount.next_sibling || matches!( cx.store().get_attr(object, "mounted"), Err(VmError::AttributeNotFound(_)) ) { cx.replace_payload(object, mount)?; cx.set_attr(object, "mounted", Value::Bool(true))?; cx.set_attr( object, "layoutVersion", Value::Number((self.frame.layout_version as i64).into()), )?; } // 新字典逐项拥有活动 Mount;弱链接本身不负责让整棵出现树存活。 cx.dict_set( root, Value::Text(geometry.id.to_string()), Value::Object(object), )?; } // 生命周期包装按“全部卸载→全部新挂载”顺序收集,实际源执行留给返回 driver。 let mut hooks = Vec::new(); // Layout records are preorder; reversing guarantees children unmount // before their parents. A Page change creates fresh Mount contexts. // 旧帧按父先子后记录,逆序退役保证孩子 onUnmount 在父之前;复用对象不退役。 for id in self.old_order.iter().rev() { // 从保存的确定顺序取旧身份,不依赖 HashMap 随机迭代安排卸载。 let weak = self.old[id]; // 对象仍存活且没有作为同一 ID 的新对象复用时才退役,避免重复调用 onUnmount。 if let Some(object) = cx.upgrade(weak)? && objects.get(id) != Some(&object) { // 退役副本先撤去挂接及结构链接,随后才安装卸载包装,旧句柄不能继续用于几何操作。 let mut mount = cx.store().payload::(object, "Mount")?.clone(); // 先撤销挂接和全部关系,再安排回调;onUnmount 不能继续使用失效几何或沿旧链操作。 mount.mounted = false; mount.layout = None; mount.parent = None; mount.first_child = None; mount.next_sibling = None; cx.replace_payload(object, mount)?; cx.set_attr(object, "mounted", Value::Bool(false))?; hooks.push(lifecycle_call(cx, object, "onUnmount")?); } } // 新建顺序沿帧先序,onMount 默认父先子后;全部旧退役回调排在新挂载回调之前。 // preparing=true 也会生成这些包装,但 UiBridge 保存驱动到完成时才执行;取消准备时只释放驱动,不触发钩子。 // 按新帧先序安排挂载回调,父先于子;复用出现不重复通知。 for object in created { hooks.push(lifecycle_call(cx, object, "onMount")?); } // 同步只返回生命周期序列,不在此处立即执行用户回调;调用方决定在后续阶段何时运行。 // 无钩子返回 Nil,有钩子构造 vm_sequence;同步本身不直接递归执行源函数。 let driver = if hooks.is_empty() { None } else { Some(crate::vm_sequence::build( cx, hooks, "", self.desktop, )?) }; // 新宿主索引仅保存弱引用,字典和生命周期包装才是实际托管拥有者。 let weak = objects .into_iter() .map(|(id, object)| Ok((id, cx.store().downgrade(object)?))) .collect::, VmError>>()?; // 先让桌面拥有新字典,再公布原生弱索引;返回的驱动保持待退役对象直到回调结束。 cx.set_attr(self.desktop, "mounts", Value::Object(root))?; *self .output .lock() .map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))? = weak; Ok(NativeResult::Return( driver.map(Value::Object).unwrap_or(Value::Nil), )) } } /// Explicitly owned roots protect the old presentation while preparation /// temporarily replaces its desktop dictionary and Mount payloads. /// 布局准备前的挂载快照与临时保留,用于取消恢复;成功提交后必须释放其保留引用。 /// UiBridge 在 source_layout 尚未完成的错误/取消路径调用 restore,进入完成投影/提交路径后调用 release。 /// 只覆盖挂载字典、旧 Mount payload 和两项公开属性,不保存 Page/业务字段、窗口命令或任务内部状态的完整事务快照。 pub struct Checkpoint { // 旧弱表与顺序副本,restore 成功后替换回宿主表。 table: MountTable, // 原 desktop.mounts 字典身份;由 roots 显式保活,None 表示原先没有该属性。 dictionary: Option, // 保存 payload 与 mounted/layoutVersion 两项公开属性;None 表示原先没有属性,回滚要删除。 states: Vec<(ObjectRef, Mount, Option, Option)>, // 检查点实际 retain/upgrade 得到的拥有引用,成功或失败退出都必须 release。 roots: Vec, } impl Checkpoint { /// 释放检查点为回滚临时保留的托管根,成功提交后不再需要这些引用。 pub fn release(self, vm: &mut Vm) -> Result<(), VmError> { // 聚合释放错误,单个根释放失败不阻止清理其他已保留根。 let mut result = Ok(()); // 逆序释放所有临时根,单项失败仍继续尝试余下根,最终返回最后一次释放错误。 for root in self.roots.into_iter().rev() { // 保留最后一次释放错误,调用方仍知道资源清理没有完全成功。 if let Err(error) = vm.release(root) { result = Err(error); } } result } /// 恢复检查点中尚未发布的出现关系与布局上下文,并释放临时保留。 pub fn restore( self, vm: &mut Vm, desktop: ObjectRef, table: &mut MountTable, ) -> Result<(), VmError> { // 先暂存恢复错误,让下方无论成败都尝试释放检查点拥有根。 // 不调用 onUnmount/onMount;准备态尚未执行这些通知,恢复只修复宿主派生关系,避免制造一轮虚假生命周期。 let result = (|| { // 回滚先识别本轮新增的出现并将其脱离,再恢复原有 Mount payload、公开属性和桌面拥有字典。 let old: std::collections::HashSet<_> = self.states.iter().map(|state| state.0).collect(); // 遍历准备阶段当前出现,识别不属于旧检查点的临时新建 Mount。 for weak in table.ordered() { // 弱升级提供临时强根,处理结果暂存后必须释放再传播错误。 if let Some(object) = vm.upgrade(weak)? { // 捕获单个对象退役过程的结果,确保后面的 release 仍执行。 let result = (|| { // 只把准备阶段新出现脱离,旧出现将由保存 payload 原样恢复。 if !old.contains(&object) { // 新对象即使仍被外部源引用,也必须标为未挂接并清除临时几何上下文与弱层次。 let mut mount = vm.payload::(object, "Mount")?.clone(); mount.mounted = false; mount.layout = None; mount.parent = None; mount.first_child = None; mount.next_sibling = None; vm.replace_payload(object, mount)?; vm.set_attr(object, "mounted", Value::Bool(false))?; } Ok::<_, VmError>(()) })(); // 保存处理结果后先释放 upgrade 根,再传播处理错误,避免失败路径遗留临时强引用。 vm.release(object)?; result?; } } // 恢复每个旧 payload 和两项公开属性,不能只改宿主索引而留下 VM 状态不一致。 for (object, mount, mounted, version) in &self.states { vm.replace_payload(*object, mount.clone())?; // 原属性“缺失”和“存在且为 Nil”等值状态不同,逐项按 Option 恢复。 for (name, value) in [("mounted", mounted), ("layoutVersion", version)] { // 原属性存在则恢复旧值,不存在则 del_attr;已缺失可忽略,其余删除错误仍要传播。 if let Some(value) = value { vm.set_attr(*object, name, value.clone())?; } else { // 原本不存在的属性应删除;已经缺失视为已恢复,其他错误仍传播。 match vm.del_attr(*object, name) { Ok(()) | Err(VmError::AttributeNotFound(_)) => {} Err(error) => return Err(error), } } } } // 恢复旧桌面字典的强拥有关系;原来没有字典时必须删去准备阶段新字典。 if let Some(dictionary) = self.dictionary { vm.set_attr(desktop, "mounts", Value::Object(dictionary))?; } else { // 缺失可忽略,其他删除失败意味着恢复未完成,不能直接报告成功。 match vm.del_attr(desktop, "mounts") { Ok(()) | Err(VmError::AttributeNotFound(_)) => {} Err(error) => return Err(error), } } *table = self.table.clone(); Ok(()) })(); // 恢复过程失败也要释放检查点根;先尝试两项操作,再按恢复错误优先的顺序返回。 // 先尝试释放全部检查点根,再优先返回恢复错误;不能因恢复失败跳过根清理。 let released = self.release(vm); result?; released } } impl MountTable { /// 保留布局准备前的 Mount 状态,供失败或取消路径恢复。 pub fn checkpoint(&self, vm: &mut Vm, desktop: ObjectRef) -> Result { // 布局准备入口读取原挂载字典,保留“属性不存在”状态;非对象现值是非法内部状态。 let dictionary = match vm.get_attr(desktop, "mounts") { Ok(Value::Object(dictionary)) => Some(dictionary), Err(VmError::AttributeNotFound(_)) => None, Ok(_) => return Err(VmError::InvalidArgument("invalid mount dictionary")), Err(error) => return Err(error), }; // 记录每次显式 retain/upgrade 的拥有引用,部分构造失败时也可统一清理。 let mut roots = Vec::new(); // 保留旧字典,避免准备阶段 desktop.mounts 换成新字典后旧出现提前回收,导致无法恢复。 if let Some(dictionary) = dictionary { vm.retain(dictionary)?; roots.push(dictionary); } // 先保存表/字典,逐项累积状态和根,不能在全部成功前交给调用方。 let mut checkpoint = Checkpoint { table: self.clone(), dictionary, states: Vec::new(), roots, }; // 把中途读取/保留错误暂存,创建失败时仍释放已经取得的全部拥有根。 let result = (|| { // 按旧展示顺序保存活动对象,便于恢复一致的上下文而不受 HashMap 顺序影响。 for weak in self.ordered() { // 只有仍存活的 Mount 可保存,弱升级本身提供一个检查点拥有根。 if let Some(object) = vm.upgrade(weak)? { // upgrade 已提供一个拥有根,直接记录待释放列表,不重复 retain;随后读取失败也会统一清理。 checkpoint.roots.push(object); // 复制当前出现 payload,恢复时连同滚动请求、任务作用域及临时布局状态一起还原。 let mount = vm.payload::(object, "Mount")?.clone(); // 保存 mounted/layoutVersion 两项独立源字段,不能假设它们与 payload 一定同步或类型固定。 let mut attributes = Vec::new(); // 逐项区分有值/缺失,其他读取错误不应被误当作缺失。 for name in ["mounted", "layoutVersion"] { // 读取返回对象 Value 时还需额外 retain,Rust Value 副本本身不增加 VM 根。 let value = match vm.get_attr(object, name) { Ok(value) => Some(value), Err(VmError::AttributeNotFound(_)) => None, Err(error) => return Err(error), }; // 公开属性理论上可能持有对象,快照保存 Value 本身不会自动增加 VM 根,须额外 retain。 if let Some(Value::Object(object)) = &value { vm.retain(*object)?; checkpoint.roots.push(*object); } attributes.push(value); } checkpoint.states.push(( object, mount, attributes.remove(0), attributes.remove(0), )); } } Ok::<_, VmError>(()) })(); // 检查点创建到一半失败时释放已经取得的根,不返回一个不完整且可能泄漏的检查点。 if let Err(error) = result { checkpoint.release(vm)?; return Err(error); } Ok(checkpoint) } /// A faulted domain cannot run user onUnmount callbacks. Still detach every /// context and release its owning dictionary through the normal heap barrier. /// 退役当前出现表,撤销挂载关系并准备不再展示的生命周期状态。 pub fn retire(&mut self, vm: &mut Vm, desktop: ObjectRef) -> Result<(), VmError> { // 故障域退役按孩子先父后清除出现,但绝不调用任何源 onUnmount。 for weak in self.ordered().into_iter().rev() { // 当前仍存活的旧句柄要置为 detached;每次升级的强根处理后立即释放。 if let Some(object) = vm.upgrade(weak)? { // 暂存 payload/公开属性更新结果,让故障路径也能清理弱升级拥有根。 let result = (|| { // 清挂接、临时 layout 与父子兄弟链接,旧源句柄不得继续操作关闭的展示上下文。 let mut mount = vm.payload::(object, "Mount")?.clone(); mount.mounted = false; mount.layout = None; mount.parent = None; mount.first_child = None; mount.next_sibling = None; vm.replace_payload(object, mount)?; vm.set_attr(object, "mounted", Value::Bool(false)) })(); vm.release(object)?; result?; } } // 故障退役不执行用户回调;将 desktop.mounts 换成空字典,再清除原生索引,撤销桌面拥有关系。 // 通过正常 VM 写屏障将 desktop.mounts 换为空字典,撤销实际托管强拥有边。 let dictionary = vm.new_dict()?; // 先保存写入结果,再释放新字典的局部分配根;桌面属性成功后持有字典。 let result = vm.set_attr(desktop, "mounts", Value::Object(dictionary)); vm.release(dictionary)?; result?; self.active.clear(); self.order.clear(); Ok(()) } /// 按原始展示顺序返回弱 Mount 身份,供事件和协议驱动器保持稳定顺序。 pub fn ordered(&self) -> Vec { // 过滤仍在 active 的 ID 并保留旧帧顺序,事件/生命周期调用方不直接遍历 HashMap。 self.order .iter() .filter_map(|id| self.active.get(id).copied()) .collect() } /// 根据新结构复用或建立每次出现的 Mount,连接父子关系并准备生命周期调用。 pub fn synchronize( &mut self, vm: &mut Vm, desktop: ObjectRef, page: Option, nodes: Vec, frame: &Frame, preparing: bool, ) -> Result, VmError> { // 同步函数与宿主表之间共享一次性输出弱表;Mutex 只传递索引,不承担托管根。 // 本函数不自行创建恢复点;准备调用方须先 checkpoint。调用失败时已有 VM 写入不由 self.active 的保留自动撤销。 let output = Arc::new(Mutex::new(HashMap::new())); // 同步入口显式保活桌面与可选页面;旧表和节点表本身仍是弱索引,不增加隐式根。 let mut captures = vec![desktop]; captures.extend(page); // 构造无参 native 同步入口,显式 captures 保活桌面和可选页面,其他输入为原生值/弱观察。 let function = vm.allocate(ContextNativeFunction::new( "mount-sync", Sync { desktop, page, nodes, old: self.active.clone(), old_order: self.order.clone(), frame: frame.clone(), preparing, output: output.clone(), }, captures, ))?; // 先保存调用结果再释放同步入口,错误时同样清理;只有成功后才替换活动索引与顺序。 // 让所有分配/写入在所属 VM 的 NativeContext 中执行,返回值可能是待执行生命周期驱动。 let result = vm.call(function, &[]); vm.release(function)?; // 先释放入口函数后再传播调用错误;失败不安装半完成的宿主 active/order。 let result = result?; // 从共享输出移动弱表而不是克隆;生命周期驱动若存在则作为拥有结果交给调用方释放。 self.active = std::mem::take( &mut *output .lock() .map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))?, ); self.order = frame.mounts.iter().map(|mount| mount.id).collect(); // 仅 Object 驱动或 Nil 是合法同步返回;其他值先释放再报告内部协议错误。 match result { Value::Object(hooks) => Ok(Some(hooks)), Value::Nil => Ok(None), // 非预期返回值也统一经 release_value 清理,再报告同步返回协议错误。 value => { vm.release_value(value)?; Err(VmError::InvalidArgument("invalid lifecycle batch")) } } } } // 兼容旧 crate::mount::collection 调用路径,实际对象列表拥有/索引实现集中在 object_list 模块。 pub use crate::object_list::collection;