//! Endor 与 Tessera 的展示事务协调层:维护每个窗口的 Page、Mount、布局缓存和发布版本。 //! Session 保存托管对象的展示上下文;跨线程发布的是不可变 UiNode/Frame,而不是可以在窗口线程执行的 VM。 //! 刷新按 prepare → layout → paint → commit 推进。挂起工作必须恢复或显式取消;失败不能把未完成的几何和生命周期状态留作已发布结果。 //! //! 调用来源有两条:runtime::endor_host 通过 UiBridge 安装环境、推进刷新与清理; //! Endor VM 则在源程序调用 ui 工厂、类型构造器或实例方法时,进入本模块的 NativeCallable 实现。 //! Function/TypeMethod/NativeFactory 是语言调用适配,Session 保存跨调用的窗口展示状态; //! 真正的排版/绘制交给 render,业务对象与 Mount 数据交给 model,均不在这里操作 OS 窗口。 //! 本模块会在 worker 中访问 VM;Arc/Mutex 只用于共享原生会话,不授权 GUI 线程直接调用 VM。 mod bootstrap; mod calls; mod developer_pages; mod page_input; mod signatures; mod tree; pub use bootstrap::install; use signatures::{method_signature, operation_signature}; use std::sync::{Arc, Mutex}; use anyhow::{Result, bail}; use endor_core::{ AnnotationType, CallEffect, CallSignature, CallType, ContextNativeFunction, EndorReal, EndorTypeObject, NativeCallable, NativeContext, NativeResult, ObjectRef, TypeConstructor, Value, Vm, VmError, WeakObjectRef, }; use crate::{ block::{self, Child, Data, Kind}, document::parse_color, }; /// new_typed_block 的节点弱表上限,也用于容器工厂的变参数量约束,限制单会话宿主开销。 pub const MAX_UI_NODES: usize = 4096; /// 新建 Page 时按存活对象检查的上限;输入解码也用它拒绝明显越界的页面索引。 pub const MAX_PAGES: usize = 64; /// dimension 将语言数字转换成原生布局长度前检查的上界,避免异常尺寸流入后续绘图。 pub const MAX_DIMENSION: f64 = 1_000_000.0; // 编译器已知宿主全局类型/能力名,同时用于 install_globals;仅列名不代表绕过实际 VM 名义类型查找。 pub const TYPE_NAMES: &[&str] = &[ "Object", "Type", "Function", "Trait", "Visual", "EventTarget", "Children", "Presentable", "Block", "ContainerBlock", "ScrollBlock", "ScrollView", "TextBlock", "RectBlock", "Spacer", "Row", "Column", "Overlay", "Scene2D", "CanvasViewport", "Camera2D", "RayHit3D", "Panel3D", "CameraGesture", "Vec3", "Triangle3D", "Geometry3D", "Mesh3D", "Box3D", "Group3D", "Scene3D", "DirectionalLight", "PerspectiveCamera", "OrthographicCamera", "OrbitController", "Viewport3D", "Button", "Page", "Window", "Mount", "Event", "Subscription", "Task", "ObjectList", "Constraints", "MeasureResult", "Bounds", "Point", "Arrangement", "DisplayList", "PaintCommand", ]; /// install_globals 暴露的类型/能力查询函数名称;与 bootstrap 安装到 ui 的入口对应。 pub const QUERY_NAMES: &[&str] = &[ "isInstance", "isSubtype", "implement", "implements", "supports", ]; pub use tree::{UiKind, UiNode}; #[derive(Debug)] /// 整个 VM 的桥接会话。节点索引跨窗口共享,presentations 按窗口隔离派生状态;current 决定方法当前访问哪份展示上下文。 struct Session { // new_typed_block 登记、reclaim 清理的弱引用表;不因宿主记账让已不可达的 Block 保活。 nodes: Vec, // 每个窗口独立拥有 Mount、布局缓存和待发布画面;切换窗口只改变 current,不移动业务对象。 presentations: std::collections::BTreeMap, // Deref/DerefMut 选择哪个 Presentation;调用前必须保证对应键仍在 presentations 中。 current: u64, // 主 desktop 的托管身份;create_window 用它建立副窗口与主资源域的关系。 master: ObjectRef, // 已分配窗口身份的高水位,关闭窗口后也不降低,避免旧事件误指向新窗口。 next_window: u64, // 跨窗口共用 Mount 高水位,避免某窗口取消/重建后与其他窗口已暴露出现 ID 重合。 next_mount: u64, // 正常关闭副窗口后暂存尚待发布的 Close 等命令;Presentation 已移除也不能丢掉最后的原生退役请求。 retired_commands: Vec, // 以下是供 stats 读取的累计工作量,并非控制下一次刷新的剩余预算。 refresh_yields: usize, refresh_restarts: usize, layout_cancels: usize, paint_cancels: usize, paint_slices: usize, binding_evaluations: usize, // shutdown 设置;create_window 等入口读取,阻止卸载回调在退出期间重新建立窗口。 exiting: bool, } impl Session { /// 由刷新/依赖收集流程调用,返回所有窗口的宿主 desktop,排除宿主记账读取造成的无关失效。 fn host_desktops(&self) -> std::collections::HashSet { // 只取值里的 desktop 而非窗口编号;依赖跟踪按托管 ObjectRef 判断读取归属。 self.presentations .values() .map(|view| view.desktop) .collect() } } impl std::ops::Deref for Session { type Target = crate::presentation::Presentation; /// Rust 在 state.desktop 等字段访问时隐式调用,借用 current 对应窗口的 Presentation。 fn deref(&self) -> &Self::Target { // Session 解引用始终指向 current Presentation;调用源回调前切换窗口,结束后恢复主窗口,不能把它当全局唯一展示。 self.presentations .get(&self.current) .expect("selected presentation") } } impl std::ops::DerefMut for Session { /// Rust 在修改当前展示字段时隐式调用;调用方须先选定仍存在的窗口,不能跨窗口复用缓存。 fn deref_mut(&mut self) -> &mut Self::Target { // 与只读 Deref 使用相同键;缺项表示桥接选择/退役流程破坏了内部不变量,因此直接 expect。 self.presentations .get_mut(&self.current) .expect("selected presentation") } } // 各原生函数 payload 共享同一个会话;Mutex 保护宿主字段,不是 VM 托管堆的并发访问许可。 type Shared = Arc>; #[derive(Debug, Clone, Copy)] /// ui/组件宿主入口的操作枚举;统一由调用适配器分派,不直接代表已经发生的业务变更。 enum Operation { IsInstance, Implement, Implements, Supports, IsSubtype, OpenValue, Mounts, Stats, Viewport, Window, PageWindow, NewWindow, CloseWindow, Detach, MoveWindow, MoveTab, State, StateSet, Increment, TextOf, Bind, Text, Rect, Spacer, Scroll, Row, Column, Overlay, Scene2D, CanvasViewport, Camera2D, SceneObject(crate::scene3d_model::Constructor), Viewport3D, CameraControl, Translate3D, Rotate3D, Button, Page, SetText, SetColor, Append, Insert, Remove, Replace, Move, Children, Render, Unmount, Open, Close, Activate, Navigate, Back, Forward, OnClick, On, Dispatch, OnEvent, Invalidate, OnMount, OnUnmount, Click, Present, } impl Operation { /// 被 signatures、Function/TypeMethod/NativeFactory 的 arity 查询调用,提供统一显式参数范围。 /// 这里不计接收者、Type 首参和源子类字段;各适配器负责给范围加上自己的调用前缀。 fn arity(self) -> endor_core::CallArity { use Operation::*; // 此 arity 只含显式参数;TypeMethod 另加 receiver,NativeFactory 另加 Type 和子类字段外壳。 let (minimum, maximum) = match self { // 类型/能力关系与属性绑定都需要两个显式操作数,不接收可选尾参。 IsInstance | IsSubtype | Bind | Implement | Implements | Supports => (2, 2), // 这些查询所需上下文来自 Session 或绑定 owner,源调用不再传额外实参。 Stats | Viewport | Window | PageWindow | Mounts => (0, 0), // 状态工厂接初值,绑定 set 接新值;接收者已存于 Function.owner。 State | StateSet => (1, 1), // increment 的目标已绑定,递增步长由实现固定,不接用户步长参数。 Increment => (0, 0), // 对象和属性名必填,字号/颜色可选;类型约束另见 operation_signature。 TextOf => (2, 4), // 滚动块、二维与三维视口都要求两个尺寸加一个内容/场景对象。 Scroll => (3, 3), CanvasViewport => (3, 3), Viewport3D => (3, 3), // 3D 族从共用构造元数据读取,避免这里维护另一份容易漂移的参数数量表。 SceneObject(kind) => kind.arity(), // 控制更新接对象与手势,变换接三个分量;这些操作不使用任意长度变参。 CameraControl => (2, 2), Translate3D | Rotate3D => (3, 3), // 相机三个参数都可省略;场景孩子则可为空,仍受全局节点数量预算限制。 Camera2D => (0, 3), Scene2D => (0, MAX_UI_NODES), // 文本内容/矩形宽高必填,其余表现参数可选;不接容器子项尾参。 Text => (1, 3), Rect => (2, 4), // 单值操作复用同一数量规则,具体参数的类型和语义仍由各自签名与调用分支决定。 MoveTab | MoveWindow | OpenValue | Spacer | SetText | SetColor | Append | Remove | Render | Navigate | OnClick | Present => (1, 1), // 容器两个布局选项在前,后面最多 MAX_UI_NODES 个孩子;+2 不应错计入孩子预算。 Row | Column | Overlay => (2, MAX_UI_NODES + 2), // 按钮接初始文本,容器插入/替换/移动接槽位或位置与另一个值。 Button => (1, 1), Insert | Replace | Move => (2, 2), // 页面/窗口状态操作和查询使用绑定 owner 或当前会话,无显式参数。 Page | Unmount | Close | Activate | Back | Forward | Children | NewWindow | CloseWindow | Detach => (0, 0), // open 可指定 Window;click 可指定 Mount;invalidate 可指定类别,其余走缺省语义。 Open => (0, 1), Click | Invalidate => (0, 1), // on 必填事件名和回调,可选捕获阶段;dispatch 必填事件,可选具体出现位置。 On => (2, 3), Dispatch => (1, 2), // 生命周期/事件钩子由宿主传一个协议上下文,用户覆盖必须遵守同一数量约定。 OnEvent | OnMount | OnUnmount => (1, 1), }; // 同时保留必填下界和可选/变参上界,供 VM 在进入实际操作前拒绝参数数量错误。 endor_core::CallArity::range(minimum, maximum) } } #[derive(Debug)] /// 将一个宿主 Operation 及会话环境封装成 VM 可调用对象。 struct Function { // bootstrap 先留空、类型齐全后补入;方法 wrapper 已校验签名时可用 None 转交同一业务分派。 signature: Option, // calls::invoke 的分支选择值;名称只用于查询与诊断,不能替代这个明确的操作标签。 operation: Operation, // 自由工厂为 None,绑定方法保存 receiver;调用参数切片中不再重复包含该 receiver。 owner: Option, // 回调需要访问所属窗口的展示状态;托管强边另外由 ContextNativeFunction 的 captures 声明。 session: Shared, // 原生构造的实际源子类身份;自由 ui 工厂为 None,类型构造器显式传入 Some。 requested_type: Option, } /// One shared implementation on the nominal type. The VM binds receivers; /// individual Blocks no longer allocate owner-capturing method closures. #[derive(Debug)] /// 类型查询/能力方法的原生包装,保存调用所需的声明与会话。 struct TypeMethod { // 包含 receiver 的完整签名,由 VM 在 native 调用边界检查。 signature: CallSignature, // 验证接收者后转交 Function 分派的操作,不为每个 Block 复制一个函数实现。 operation: Operation, // 声明此方法的名义类型;is_instance 允许真正的源子类,但拒绝无关对象。 expected: ObjectRef, // 方法安装时捕获的桥接会话,决定 owner 操作使用的展示环境。 session: Shared, } /// 由 snapshot 的事件能力提取调用,判断 onEvent 是否仍为默认原生实现,区分源覆盖。 pub(crate) fn is_intrinsic_event( store: &endor_core::ObjectStore, function: ObjectRef, ) -> Result { // 先检查 payload 是否为 ContextNativeFunction;源函数或其他 callable 不可能是默认 TypeMethod。 let Some(function) = store .object(function)? .payload() .as_any() .downcast_ref::() else { // 类型不同即非内建实现;对象读取失败则由前面的 ? 返回错误,不能当作正常 false。 return Ok(false); }; // 原生函数也可能是其他 UI 操作;只有 TypeMethod 包装的 OnEvent 才符合默认事件入口。 Ok(function .implementation::() .is_some_and(|method| matches!(method.operation, Operation::OnEvent))) } impl NativeCallable for TypeMethod { /// VM 的 callable 签名查询入口,返回 bootstrap 按声明类型建立的完整 receiver/参数/返回契约。 fn signature(&self) -> Option { Some(self.signature.clone()) } /// 在 Operation 的显式参数范围两端各加一,计入 VM 传入的隐式接收者。 fn arity(&self) -> Option { // operation 的范围不含 receiver;这里构造的是 VM 实际传入 args 的范围。 let arity = self.operation.arity(); Some(endor_core::CallArity::range( arity.required + 1, arity.maximum + 1, )) } /// VM 分派实例方法后调用;先校验 args[0],再把 receiver 交给 Function.owner 执行业务操作。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { // 名义方法统一存在类型上,VM 把 receiver 放首参;校验后剥离 receiver 再交 Function 的显式参数分派。 // 不假设源语法一定来自 obj.method:显式调用同样必须提供有效的对象首参。 let owner = object(args.first())?; if !cx.store().is_instance(owner, self.expected)? { // 拒绝错误接收者,不进入分派器,防止按某种 Block 布局读取无关 payload。 return Err(invalid("incorrect receiver type for visual method")); } // 使用临时适配器共享分派;原 wrapper 已提供签名,转交时无需重复携带完整 receiver 签名。 Function { signature: None, operation: self.operation, owner: Some(owner), session: self.session.clone(), requested_type: None, } .invoke(cx, &args[1..]) } } /// The Type dispatcher passes the requested subclass explicitly. Native /// constructor options precede the subclass's inherited/own field arguments. #[derive(Debug)] /// 为源代码调用原生类型构造器提供受检查的分配入口。 struct NativeFactory { // 对 VM 可见的宽外壳:Type 首参 + 原生参数 + 源子类字段。 signature: Option, // 仅描述原生选项;invoke 切分字段后再检查,不能用外壳的 Any 变参绕过类型契约。 options: Option, // 决定实际原生分配器,Function 将据此创建 Block、相机或三维对象。 operation: Operation, // 与该类型注册域一致的展示会话,跨 VM 的同名 Type 不能混用。 session: Shared, } impl NativeCallable for NativeFactory { /// VM 在类型构造的原生调用边界查询外壳签名;具体原生参数的窄签名由 invoke 进一步检查。 fn signature(&self) -> Option { self.signature.clone() } /// 要求原生必填参数再加一个请求 Type;上界暂开放,子类字段与原生选项在 invoke 内分别校验。 fn arity(&self) -> Option { // 原生参数有下界,但源子类可增加字段;不能把 Operation 的上界直接当总实参数量上界。 Some(endor_core::CallArity::range( self.operation.arity().required + 1, usize::MAX, )) } /// VM 调用 NativeTyped 构造器时进入;切开原生选项和子类字段,先验证,再分配并初始化结果。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { // VM 类型分派把实际请求的子类放在首位,不能退回工厂所属的基类而丢失源子类身份。 let requested = object(args.first())?; // 复制字段声明以结束对 store 的借用,后续调用 Function 和写属性需要可变访问 cx。 let fields = cx .store() .payload::(requested, "Type")? .fields() .to_vec(); // 切掉 Type 首参后的实参由两段组成:原生构造选项在前,按声明顺序的子类字段在后。 let values = &args[1..]; // 构造实参末尾是类型声明的继承/自有字段,前缀才是原生构造选项;checked_sub 防止缺字段造成下溢。 let native_count = values .len() .checked_sub(fields.len()) .ok_or(invalid("missing subclass field arguments"))?; cx.store() .check_class_field_arguments(requested, &values[native_count..])?; // 外壳允许宽变参,但原生前缀仍按具体 Operation 完整签名检查,不能因子类存在绕过类型或 arity。 self.options .as_ref() .ok_or(invalid("native factory is not initialized"))? .check_arguments( cx.store(), "native constructor options", &values[..native_count], )?; // requested_type 通知原生分配器附上真实子类 Type;owner 为空,因为这是构造而非实例方法。 let result = Function { signature: None, operation: self.operation, owner: None, session: self.session.clone(), requested_type: Some(requested), } .invoke(cx, &values[..native_count])?; // 原生工厂必须同步返回对象;不能把挂起调用或普通标量误当已分配实例继续写字段。 let NativeResult::Return(Value::Object(instance)) = result else { // 契约错误直接交回 VM,避免把不完整结果作为源程序的构造返回值。 return Err(invalid("native constructor did not return an instance")); }; // 分配器必须返回精确 requested 类型,不能只返回基类实例;之后才初始化子类字段。 if cx.store().type_of(instance)? != Some(requested) { // 精确身份不符说明分配器漏传/忽略了 requested_type,接受基类会破坏源子类语义。 return Err(invalid( "native constructor returned an incorrect nominal type", )); } for (field, value) in fields.iter().zip(&values[native_count..]) { // 前面已验证数量与类型,此处按同一声明顺序写入;set_attr 负责托管引用与变更记录。 cx.set_attr(instance, &field.name, value.clone())?; } // 将完成字段初始化的原生子类交回 VM,后续源代码才可使用它。 Ok(NativeResult::Return(Value::Object(instance))) } } /// 各解码器和 NativeCallable 分支调用的参数错误构造器,交由 VM 保留调用诊断上下文。 fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } /// 供桥接实参/属性解码调用:只接受对象值,不把 nil、标量或缺失实参转换成虚构的对象身份。 fn object(value: Option<&Value>) -> Result { // Option 同时覆盖缺失参数和实际值;返回 ObjectRef 仅复制句柄,不增加托管拥有根。 match value { Some(Value::Object(value)) => Ok(*value), _ => Err(invalid("expected an object")), } } /// 被工厂、方法及 color 参数解码调用,只接收语言字符串值,不隐式调用对象的 repr。 fn text(value: Option<&Value>) -> Result { // 拷贝字符串是为了结束源 Value 的借用,让调用者可以把文本移入新的 Block payload。 match value { Some(Value::Text(value)) => Ok(value.clone()), _ => Err(invalid("expected text")), } } /// 被 calls 中的容器索引/槽位等参数解析调用,要求可表示为 i64 的非负整数,再转为 u64。 fn integer(value: Option<&Value>) -> Result { // 先走数字自身的精确转换,再做有符号到无符号检查;不能用浮点截断接受带小数的索引。 match value { Some(Value::Number(value)) => value .as_i64() .and_then(|v| u64::try_from(v).ok()) .ok_or(invalid("expected a nonnegative integer")), _ => Err(invalid("expected a nonnegative integer")), } } /// 被文本/矩形工厂和颜色修改操作调用;缺省时使用调用方指定颜色,显式传入错误值则报错。 fn color(value: Option<&Value>, default: &str) -> Result { // transpose 将 Some(Err) 传播出去,同时保留 None;只有未传参数才使用 default。 let value = value .map(|v| text(Some(v))) .transpose()? .unwrap_or_else(|| default.into()); // 此处只校验并保留源字符串,render/document 在使用颜色时按同一格式转换成像素颜色。 parse_color(&value).map_err(|_| invalid("color must use #RRGGBB or #RRGGBBAA"))?; Ok(value) } /// 被原生工厂的尺寸/字号等参数解析调用,把 Endor 数字限制在可用的有限非负布局范围。 fn dimension(value: Option<&Value>) -> Result { // 缺失和非 Number 值都不是合法必填尺寸;默认尺寸由调用方选择,解码器不自行猜测。 let Some(Value::Number(number)) = value else { // 在访问 number 表示之前退出,防止把 nil 或文本当成零尺寸。 return Err(invalid("expected a finite nonnegative dimension")); }; // 分别支持大整数、十进制和二进制实数表示,不接受非实值;转 f64 后检查有限与范围,再降 f32。 let value = match number.as_real() { Some(EndorReal::Integer(v)) => v.to_string().parse::().ok(), Some(EndorReal::Decimal(v)) => v.literal().parse::().ok(), Some(EndorReal::BinaryFloat(v)) => Some(f64::from_bits(*v)), _ => None, } .filter(|v| v.is_finite() && (0.0..=MAX_DIMENSION).contains(v)) .ok_or(invalid("dimension exceeds limits"))?; // MAX_DIMENSION 远小于 f32 的有限上界;通过上面的检查后再按原生几何格式降低精度。 Ok(value as f32) } /// 由 calls 的 state 工厂调用,为状态对象安装 set/increment 等捕获 owner 的原生方法。 /// 普通 Block 实例方法走 TypeMethod 共享实现;这里处理的是需要固定 owner 的对象属性函数。 fn bind( cx: &mut NativeContext<'_>, owner: ObjectRef, name: &'static str, operation: Operation, session: &Shared, ) -> Result<(), VmError> { // 只在读 desktop 身份时持锁,随后分配/设属性不持有 Session 的锁,避免不必要的嵌套借用。 let desktop = session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; // 签名描述显式参数,owner 存入 payload;调用者不需要把状态对象再当首参传入。 let function = cx.allocate(ContextNativeFunction::new( name, Function { signature: Some(operation_signature(cx.store(), desktop, operation)?), operation, owner: Some(owner), session: session.clone(), requested_type: None, }, // 绑定方法显式捕获 owner 与 desktop,让托管 GC 识别函数的真实强边,Rust Shared 会话本身不隐式保活 VM 对象。 vec![owner, desktop], ))?; // 名义函数检查与普通源函数一致;把 callable 写成 owner 属性后由该托管强边持有。 attach_desktop_type(cx, function, "Function", desktop)?; cx.set_attr(owner, name, Value::Object(function)) } /// PresentResult/present_value 等内部包装路径调用,创建没有源子类请求的默认原生 Block。 fn new_block( cx: &mut NativeContext<'_>, data: Data, session: &Shared, ) -> Result { // 复用类型校验和节点/Page 预算,不允许内部包装绕开普通工厂的资源限制。 new_typed_block(cx, data, session, None) } /// calls 的 Camera2D/CanvasViewport 分配路径调用,为视口建立有名义身份的独立相机状态。 fn new_camera2d( cx: &mut NativeContext<'_>, session: &Shared, requested: Option, args: &[Value], ) -> Result { // 相机和使用它的视口必须在同一个类型域;查询当前窗口 desktop 取得注册表。 let desktop = session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; // 源子类构造保留 requested;自由工厂和视口默认相机使用注册的 Camera2D。 let definition = requested.unwrap_or(desktop_type(cx.store(), "Camera2D", desktop)?); // payload 标识相机种类,具体 x/y/zoom 则是下面写入的托管属性,供源代码和依赖追踪读取。 let camera = cx.allocate(crate::viewport::Camera2D)?; cx.attach_type(camera, definition)?; for (index, name, fallback) in [(0, "x", 0.0), (1, "y", 0.0), (2, "zoom", 1.0)] { // 位置缺省为原点、缩放缺省为 1;显式错误值由 coordinate 返回错误,不能静默套用缺省。 let value = args .get(index) .map(crate::scroll::coordinate) .transpose()? .unwrap_or(fallback); cx.set_attr(camera, name, crate::native_visual::value(value))?; } // 初始化完所有相机属性后走完整相机校验,尤其 zoom 必须处于 0.01..100,不能只通过一般坐标范围。 crate::viewport::camera(cx.store(), camera)?; Ok(camera) } /// calls 的视觉工厂与 new_block 调用:按 Data.kind 分配 payload,保留请求子类并登记节点弱索引。 fn new_typed_block( cx: &mut NativeContext<'_>, data: Data, session: &Shared, requested: Option, ) -> Result { // payload 种类确定合法名义基类;同为 Block 也不能把矩形 payload 伪装成文本类型。 let type_name = match data.kind { Kind::Text { .. } => "TextBlock", Kind::Rect { .. } => "RectBlock", Kind::Spacer(_) => "Spacer", Kind::Scroll { .. } => "ScrollBlock", Kind::Row => "Row", Kind::Column => "Column", Kind::Overlay => "Overlay", Kind::Scene2D => "Scene2D", Kind::CanvasViewport { .. } => "CanvasViewport", Kind::Viewport3D { .. } => "Viewport3D", Kind::Button => "Button", Kind::Page => "Page", }; // 在 data 移入分配器之前记住是否 Page;除通用节点预算外,Page 还受单独数量限制。 let page = matches!(data.kind, Kind::Page); // 先读取注册域,再释放锁进行托管分配;登记弱表时会重新短暂持锁。 let desktop = session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; // expected 是当前 VM 中合法基类的精确身份,type_object 才是实例最终要附加的真实类型。 let expected = desktop_type(cx.store(), type_name, desktop)?; // 没有源子类请求时使用基类;有请求时仍必须经过下面的继承关系验证。 let type_object = requested.unwrap_or(expected); // 叶/容器原生种类对应不同类型基类,禁止用 TextBlock 分配器创建并非其子类的名义对象。 if !cx.store().is_subtype(type_object, expected)? { return Err(invalid("native constructor cannot allocate this subclass")); } // 业务 payload 先建立,再附上已验证的名义类型;此处没有布局几何,几何属于后续 Mount。 let block = block::allocate(cx, data)?; cx.attach_type(block, type_object)?; { // 这个作用域把锁限制在预算检查和弱索引写入期间,返回对象时不再持锁。 let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?; // 节点弱表受宿主预算限制;实际死亡弱项通过后续 reclaim 压缩,不能把弱索引当永不增长的免费结构。 if state.nodes.len() >= MAX_UI_NODES { return Err(VmError::ExecutionLimit("4096 node budget")); } if page { // 弱表可能含已死亡节点,所以 Page 预算数存活页面,而非弱记录总数。 let mut pages = 0; for weak in &state.nodes { // 只有成功升级且 payload 的 page 标志为真的节点才计入;普通 Block 不占页面预算。 if let Some(node) = cx.upgrade(*weak)? && cx.store().payload::(node, "Block")?.page { pages += 1; } } if pages >= MAX_PAGES { // 在登记新节点前失败,运行时仍负责释放此次调用中已经分配但未发布的对象。 return Err(VmError::ExecutionLimit("64 page budget")); } } // 只登记弱身份,真实拥有关系由返回值、容器子项或页面注册表建立。 state.nodes.push(cx.store().downgrade(block)?); } Ok(block) } /// calls 中 Window/Detach 操作调用,建立托管窗口和 Presentation;OS 窗口由 GUI 收到发布命令后创建。 fn create_window( cx: &mut NativeContext<'_>, state: &mut Session, definition: ObjectRef, ) -> Result { if cx.store().is_read_only() { // 布局/绘制只读调用不能借“分配新值”偷偷增加已有窗口注册表和外部资源。 return Err(invalid( "window creation is forbidden during read-only rendering", )); } if state.exiting { // shutdown 已关闭资源域,卸载回调即使仍能执行也不能重新创建窗口延长退出。 return Err(invalid("runtime is exiting; window creation is forbidden")); } // 同一 VM 最多16窗口(含主壳);ID 单调分配并限制到 i64 正范围,关闭后不复用旧资源身份。 if state.presentations.len() >= 16 { // 将主窗口一并计入,避免每个副窗口都独立持有缓存和托管根而无界增长。 return Err(VmError::ExecutionLimit("16 window budget")); } // checked_add 防溢出,i64 上界保证身份能放入语言整数使用的原生窗口命令路径。 let id = state .next_window .checked_add(1) .filter(|id| *id <= i64::MAX as u64) .ok_or(invalid("window identities exhausted"))?; // model/window 建立托管资源关系,返回窗口对象、私有桌面及页面字典,三者用于不同层次。 let (window, desktop, tabs) = crate::window::create(cx, state.master, id, definition)?; // 缓存和 Mount 按窗口独立;共享同一业务对象也不应共享它在两个窗口中的几何。 let mut presentation = crate::presentation::Presentation::new(desktop, tabs); // 新窗口即使暂无内容也需发布有限空帧,让 GUI 根据投影创建 OS 窗口,而不是在 VM 中直接创建。 presentation.force_frame = true; state.presentations.insert(id, presentation); state.next_window = id; Ok(window) } /// show_in 与页面/窗口方法调用,遍历真实注册表确定 Page 当前归属;未打开的 Page 返回 None。 fn page_window( store: &endor_core::ObjectStore, state: &Session, page: ObjectRef, ) -> Result, VmError> { for (id, view) in &state.presentations { // 从每个私有 desktop 取得托管 Window,不能只依据当前活动页判断窗口的全部页面。 let window = object(Some(&store.get_attr(view.desktop, "window")?))?; if crate::window::pages(store, window)?.contains(&page) { // 返回原生资源 ID 而非 Window ObjectRef,供 Session 的 presentations 索引使用。 return Ok(Some(*id)); } } // 无归属是新 Page 的正常状态,show_in 会再选 current;注册表读取失败已由 ? 传播。 Ok(None) } /// show_in 跨窗口迁移及 calls 关闭页面时调用,撤销登记并为源窗口安排替代活动页。 fn remove_page( cx: &mut NativeContext<'_>, view: &mut crate::presentation::Presentation, page: ObjectRef, ) -> Result<(), VmError> { // view 决定移除的是哪个窗口的关系;同一个 Page 的业务内容并不在这里销毁。 let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?; crate::window::unregister_page(cx, window, page)?; // 双层 pending 区分无请求与请求清空;移除当前/待选页时改选剩余第一页,或显式 pending=None页面。 if view.pending.unwrap_or(view.active) == Some(page) { // 必须在 unregister_page 之后选取,才不会重新选中刚移走的 Page。 let next = crate::window::pages(cx.store(), window)?.first().copied(); cx.set_attr( view.desktop, "pending", next.map(Value::Object).unwrap_or(Value::Nil), )?; view.pending = Some(next); } Ok(()) } /// 页面 open/move 等调用与 show 的共同入口:验证目标、迁移注册关系,再安排一次待提交展示。 fn show_in( cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared, target: Option, ) -> Result<(), VmError> { // 一次持锁覆盖“查旧归属—移除—登记新归属”,避免 Session 内出现中途被其他桥接调用观察的关系。 let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?; // old 是迁移前归属;后面仅在新旧 ID 不同时撤源登记,重复 activate 不应搬动页面。 let old = page_window(cx.store(), &state, page)?; // target 是托管 Window,id 是对应宿主窗口编号;验证后才能用它索引 Presentation。 let id = match target { // 显式目标窗口既要未关闭,也要对应本会话 Presentation 的同一 desktop,拒绝其他 VM 的窗口身份。 Some(window) => { // 先检查 payload 种类,再验证资源状态及 desktop 身份,不能只信任传入对象的数字 ID。 let data = cx .store() .payload::(window, "Window")?; if data.closed { // 已退役窗口仍可能被源代码持有,但不能再作为展示目标。 return Err(invalid("native window is closed")); } // ID 用于查表,desktop 的相等检查补上所属桥接会话验证。 let id = data.id; if state .presentations .get(&id) .is_none_or(|view| view.desktop != data.desktop) { // 缺少 Presentation 或 desktop 不匹配都说明不是本会话可操作的窗口。 return Err(invalid("window belongs to another runtime")); } id } // 不指定窗口时优先保留 Page 现有归属;未打开过的新页才进入 current,而非每次 activate 都迁移到主窗口。 None => old.unwrap_or(state.current), }; if let Some(old) = old && old != id { // 跨窗口移动先撤源窗口登记与必要活动页选择,再在目标登记同一 Page,业务内容对象不复制。 remove_page( cx, state .presentations .get_mut(&old) .ok_or(invalid("missing source window"))?, page, )?; } // 目标经过检查后取得可变展示上下文,用于写 pending;不会在这里改 active 或发布帧。 let view = state .presentations .get_mut(&id) .ok_or(invalid("missing target window"))?; // model 注册表保存 Page 的强拥有关系,也决定标签顺序;只写 pending 不足以完成登记。 let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?; crate::window::register_page(cx, window, page)?; cx.set_attr(view.desktop, "pending", Value::Object(page))?; // 仅记录展示请求并由 desktop.pending 强持有 Page;实际 active、Mount、布局和 published_version 留待刷新提交。 view.pending = Some(Some(page)); Ok(()) } /// calls 的激活/导航及 PresentResult 调用,不指定新目标窗口,委托 show_in 保留既有归属或选择 current。 fn show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result<(), VmError> { show_in(cx, page, session, None) } #[derive(Debug)] /// 接收源 present 结果并将其转换成宿主可展示内容的原生续接步骤。 struct PresentResult { // ui.open 的续接需确保有 Page 并安排展示;ui.present 的续接只返回视觉对象。 open: bool, // 源 present 返回后仍要回到原桥接域分配包装页/修改 pending,因此续接保存同一会话。 session: Shared, } impl NativeCallable for PresentResult { /// VM 的原生续接调用只提供被委托 present 的一个返回值;参数数量在进入 invoke 前检查。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } /// VM 完成源 Presentable.present 后调用;验证返回 payload,并按 open 标志选择包装和展示行为。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { // 类型声明不能替代返回值防御检查,错误返回立即中止,不能把它包装成看似成功的文本。 let [Value::Object(root)] = args else { return Err(invalid("Presentable.present must return a Block")); }; // payload 检查保证返回的是原生视觉 Block;page 标志决定是否还需外包 Page。 let is_page = cx.store().payload::(*root, "Block")?.page; // present 委托返回值必须为 Block;open=true 必要时包 Page 并展示,open=false 只返回转换结果。 let result = if self.open { // open 对已是 Page 的值保留身份,对普通 Block 创建单子项 Page,均走同一 show 流程。 let page = if is_page { *root } else { // 包装只新增页面,不复制返回对象;同一个 Block 可随后产生自己的 Mount。 new_block( cx, Data::new(Kind::Page, vec![*root], 0.0, 0.0), &self.session, )? }; show(cx, page, &self.session)?; page } else { // present 是值转换,不能附带把页面激活/打开的外部副作用。 *root }; Ok(NativeResult::Return(Value::Object(result))) } } /// calls 的 open/present 普通值路径调用:已有 Block 保持身份,其余转成默认 TextBlock。 /// 具有 Presentable 协议的源方法调用在 calls 的外层处理,返回后才进入 PresentResult。 fn present_value( cx: &mut NativeContext<'_>, value: &Value, session: &Shared, ) -> Result { // 快速路径只接受真实 Block payload,避免对已有视觉对象调用 repr 后丢失其行为与内容。 if let Value::Object(block) = value && cx .store() .object(*block)? .payload() .as_any() .is::() { // 复用对象身份,不在此新增根或克隆业务 payload;调用边界管理返回值的拥有关系。 return Ok(*block); } // 非 Block 的标量/对象转可读文字,Presentable 方法委托在 calls.rs 的更外层处理,本函数不自己调用 present。 let value = match value { Value::Nil => "nil".into(), Value::Bool(v) => v.to_string(), Value::Number(v) => v.to_string(), Value::Text(v) => v.clone(), Value::Object(v) => cx.store().repr(*v)?, }; if value.len() > 16_384 { // 按 UTF-8 字节限制文本开销;这是展示限制,不是“最多 16384 个 Unicode 字符”。 return Err(invalid("presentation exceeds 16 KiB")); } // 回退视觉使用默认颜色和字号种类;几何留给后续测量,不在转换阶段猜测像素尺寸。 new_block( cx, Data::new( Kind::Text { value, size: 1, color: "#242422".into(), }, vec![], 0.0, 0.0, ), session, ) } #[must_use = "pending layout must be resumed on its owning VM"] /// 桥接布局的挂起/完成结果;PendingLayout 必须与其窗口上下文一起继续处理。 pub enum LayoutProgress { // 交给 endor_host 保存;恢复时必须携带同一 VM、窗口和检查点,不能换域重跑。 Pending(Box), // 几何已就绪的帧草稿;运行时仍需完成绘制和发布阶段,不等于此刻已显示到屏幕。 Complete(Box), } /// 一次未提交布局的上下文:目标窗口、根、布局/缓存、延后钩子以及用于失败恢复的检查点。 struct LayoutContext { // 本次布局使用的页面模式/相机参数;窗口 resize 重定向时需要和新约束比较。 surface: crate::page::Surface, // 不可变视觉树草稿,布局和后续绘制必须使用与这一轮匹配的对象快照。 root: Arc, // 与本轮页面注册表一致的纯数据标签投影,不能在提交时混入下一轮的顺序。 tabs: Vec, // 原生输入给出的整窗可用尺寸;不同于页面局部约束,完成布局时据此构造完整帧。 full_viewport: Option, // 私有宿主对象和资源 ID 一起记录所属窗口,续体恢复不能误用 Session.current 的新值。 desktop: ObjectRef, window: u64, // 布局同步返回的生命周期序列拥有根,暂留到安全提交边界执行/释放,取消时也必须清理。 hooks: Option, // 布局准备可能暂改 Mount payload 与 desktop.mounts,检查点保证失败或尺寸重定向能恢复未发布关系。 checkpoint: crate::mount::Checkpoint, } /// 挂起布局的拥有者。续体与上下文必须一起保留并最终恢复或取消。 pub struct PendingLayout { // source_layout 拥有尚未完成的 VM 调用和测量状态,必须继续推进或显式取消。 work: crate::source_layout::Work, // 与 work 配套的树、窗口和检查点;单独丢弃上下文会使取消后无法恢复未提交 Mount。 context: LayoutContext, } /// 运行时持有的桥接能力句柄,通过共享 Session 协调托管对象与每窗口展示。 pub struct UiBridge { // runtime 安装全局名字和调用 ui.open 时使用;真正的 VM 拥有根由 Candidate 管理。 pub ui: ObjectRef, // 工厂/方法与运行时共用展示会话,窗口切换、版本和缓存均从这里协调。 session: Shared, // 本桥接域的任务调度器,运行时通过 poll_tasks 推进,退出通过 shutdown/cancel_tasks 清理。 tasks: crate::tasks::Scheduler, } /// dispatch_tab/页面事件等桥接控制路径调用,通过 VM 正常查找绑定方法,保留源覆盖与继承语义。 fn invoke_member( vm: &mut Vm, owner: ObjectRef, method: &str, arguments: &[Value], instruction_budget: usize, ) -> Result { // 使用 VM 正常绑定规则创建临时方法值;无论 call 成功/失败,先保存 result 再释放 bound 根。 let bound = vm.bind_attr(owner, method)?; // 暂存调用结果而非直接 ? 返回,确保即使用户方法报错也会执行后面的 release_value(bound)。 let result = match &bound { // 方法值为对象才可能调用;VM 再检查对象是否 callable,并应用帧数/指令预算。 Value::Object(function) => vm .call_with_limits( *function, arguments, endor_core::ExecutionLimits { max_frames: 128, max_instructions: instruction_budget, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error)), // 普通属性没有函数身份,不能把它当无参 getter 或忽略调用。 _ => Err(anyhow::anyhow!("object method is not callable")), }; vm.release_value(bound)?; result } /// prepare/刷新流程调用,把页面注册表投影成 GUI 可直接读取的标签数据;不向 GUI 传 ObjectRef。 fn project_tabs( vm: &Vm, state: &mut crate::presentation::Presentation, ) -> Result> { // 从窗口模型取得完整页面顺序,不能只用当前 active 页面构造标签栏。 let window = object(Some(&vm.get_attr(state.desktop, "window")?))?; // pages 是本次读取的托管身份列表;下方逐项转为不依赖 VM 的标题、数字 ID 和选中状态。 let pages = crate::window::pages(vm.object_store(), window)?; // 标签选中反映待提交页面(若有请求),否则沿用 active;页面标题和顺序仍从真实窗口注册表提取。 let active = state.pending.unwrap_or(state.active); // live 重建仍有效的弱身份到标签 ID 映射,过期/移出页面的旧记录不再保留。 let mut live = Vec::new(); // tabs 是对外返回的纯数据;和 live 分开,避免渲染线程接触托管弱引用。 let mut tabs = Vec::new(); for (index, page) in pages.iter().copied().enumerate() { // 弱身份识别同一个 Page,即使它改标题或位置也不改变该窗口内的标签 ID。 let weak = vm.object_store().downgrade(page)?; // 同一 Page 弱身份复用该窗口标签 ID,新增页用高水位;移出后 live 更新会移除旧映射,不靠标题匹配。 let id = if let Some((_, id)) = state.tab_ids.iter().find(|(w, _)| *w == weak) { *id } else { // 新页面才分配单调增长的 ID;checked_add 避免溢出后把旧拖动事件指向另一标签。 state.next_tab = state .next_tab .checked_add(1) .ok_or_else(|| anyhow::anyhow!("tab identities exhausted"))?; state.next_tab }; live.push((weak, id)); // 标题显示过滤控制字符并限制120个 Unicode 字符,缺失/空标题用默认序号;不修改原 Page.title。 let title = match vm.get_attr(page, "title") { Ok(Value::Text(title)) if !title.is_empty() => title .chars() .filter(|c| !c.is_control()) .take(120) .collect(), _ => format!("Page {}", index + 1), }; // 对外标签只保存可显示数据;工具标记用于 GUI 识别开发工具入口,不泄露 Page 对象。 tabs.push(crate::tabs::Tab { id, title, developer_tool: match vm.get_attr(page, "_tesseraTool") { // 仅接受源属性中的字符串工具名,缺失或其他值都投影为普通页面。 Ok(Value::Text(name)) => Some(name), _ => None, }, active: active == Some(page), }); } // 覆盖映射而不是追加,后续 dispatch_tab 只允许解析这次仍在注册表里的页面身份。 state.tab_ids = live; Ok(tabs) } impl UiBridge { /// endor_host 清理运行时调用,取消任务并退役所有窗口展示;callbacks 决定能否继续执行用户卸载钩子。 /// 返回窗口、Mount 和取消任务数量供诊断使用;不为退出再启动布局或绘制。 pub fn shutdown(&self, vm: &mut Vm, callbacks: bool) -> Result<(usize, usize, usize)> { // 退出先关闭新任务入口并取消旧任务,再标 session.exiting,避免卸载回调重新创建任务/窗口。 let tasks = self.tasks.quiesce(vm)?; // 保存各窗口卸载驱动的拥有根,必须等所有窗口先关闭后才逐个调用并释放。 let mut drivers = Vec::new(); // 统计退出前仍活动的出现位置,不按共享业务 Block 数量计数。 let mut mounts = 0; // 闭包收集准备阶段错误但不立刻退出外层:此前已生成的驱动仍需释放。 let prepared = (|| -> Result { // 只在修改窗口表时持锁;用户钩子执行移到锁作用域之外,避免回调重入死锁。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.exiting = true; // 复制键后再逐窗可变借用,保证本轮遍历稳定且不持有 map 迭代借用。 let ids: Vec<_> = state.presentations.keys().copied().collect(); for id in &ids { // ids 来自同一锁内的表,未删除过键,因此缺项表示内部不变量被破坏。 let view = state.presentations.get_mut(id).expect("known window"); // 每个窗口自己的 desktop 用于切断挂载关系,不能用主窗口 desktop 代替。 let desktop = view.desktop; mounts += view.mounts.active.len(); // 正常域用空结构 synchronize 安排孩子先卸载的 hooks;故障域直接 retire,不能再次执行用户代码。 if callbacks { if let Some(hooks) = view.mounts.synchronize( vm, desktop, None, Vec::new(), &crate::document::Frame::default(), false, )? { // synchronize 只准备卸载顺序;现在不运行回调,以便先关闭其他窗口。 drivers.push((*id, hooks)); } } else { // 故障 VM 不再执行源代码;直接断开挂载关系,把清理限于宿主操作。 view.mounts.retire(vm, desktop)?; } // 取得 model/window 的资源对象,以便退役页面拥有关系与窗口状态。 let window = object(Some(&vm.get_attr(desktop, "window")?))?; // 所有窗口先标 closed 并断 Page 拥有关系,然后才在下面执行用户卸载 hooks,阻止回调重新打开已关闭窗口。 crate::window::retire(vm, window)?; vm.set_attr(desktop, "pending", Value::Nil)?; vm.set_attr(desktop, "focus", Value::Nil)?; view.pending = Some(None); view.active = None; view.closing = true; } Ok(ids.len()) })(); // 保留准备失败或数量结果;后续钩子/释放错误仍可把结果转成 Err,但不跳过驱动释放。 let mut result = prepared.map(|windows| (windows, mounts, tasks)); // 若之前清理/回调失败,余下 hooks 不再执行,但所有驱动根仍逐项 release,避免失败路径泄漏。 for (id, hooks) in drivers { if result.is_ok() { // 只有此前所有退出操作正常时才调用源代码;每个驱动有独立有界 VM 预算。 let call = (|| -> Result<()> { self.select_window(id)?; // 钩子调用的返回值也是拥有值,即使无业务用途也必须释放。 let value = vm .call_with_limits( hooks, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error))?; vm.release_value(value)?; Ok(()) })(); if let Err(error) = call { // 一次失败后停止后续用户钩子,继续外层释放所有已准备的驱动根。 result = Err(error); } } if let Err(error) = vm.release(hooks) { // 无论钩子是否实际调用都释放;释放失败也报告,不能把错误吞作正常退出。 result = Err(error.into()); } } // 退出方法返回前恢复桥接默认上下文;Presentation 表尚未在本方法内删除主窗口。 self.select_window(crate::window::MAIN_WINDOW)?; result } /// endor_host/worker 查询是否仍有协作任务,以决定继续推进还是等待外部事件;不执行任务本身。 pub fn has_cooperative_tasks(&self) -> bool { self.tasks.has_cooperative_work() } /// endor_host 的 VM 轮询调用,在所属 VM 中交付至多 limit 项任务,保持回调串行。 pub fn poll_tasks(&self, vm: &mut Vm, limit: usize) -> Result { self.tasks.poll(vm, limit) } /// endor_host 取消/退役运行时调用,取消本域全部任务;已在后台进行的 I/O 也不得再交付旧回调。 pub fn cancel_tasks(&self, vm: &mut Vm) -> Result { self.tasks.cancel_all(vm) } /// endor_host 启动全窗口刷新时调用,取得经过迁移退旧优先排序的窗口身份快照。 pub fn window_ids(&self) -> Result> { // 持锁期间只读/排序原生资源表,不执行 VM 回调;调用者取得独立列表后再开始逐窗工作。 let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 复制身份,不把 Session 的 map 借用暴露给运行时;后续每窗刷新仍显式 select_window。 let mut ids: Vec<_> = state.presentations.keys().copied().collect(); // 优先处理切离旧 active 页的窗口,使跨窗口迁移先退旧出现再挂新出现;同优先级按窗口 ID 稳定排序。 ids.sort_by_key(|id| { // 第一个排序键区分“旧 active 即将退出”和其他窗口,第二个键确保同组顺序可重复。 let view = &state.presentations[id]; ( if view.active.is_some() && view.pending.is_some() && view.pending.flatten() != view.active { // pending = Some(None) 也属于切离旧页,需先安排卸载旧出现位置。 0 } else { // 无旧页或没有切换请求,无需抢在迁移源窗口之前刷新。 1 }, *id, ) }); Ok(ids) } /// runtime 在接受窗口输入前查询;锁异常、缺失或 closing 都视为不再可接受,不执行 VM 操作。 pub fn accepts_window(&self, id: u64) -> bool { self.session.lock().is_ok_and(|state| { // 控制线程只查询原生会话状态;页面对象身份与版本还有单独的后续校验。 state .presentations .get(&id) .is_some_and(|view| !view.closing) }) } /// endor_host 的逐窗刷新、事件及退出钩子调用,选择后续 Session 字段访问的目标窗口。 pub fn select_window(&self, id: u64) -> Result<()> { // current 的检查与写入放在同一短锁中,Deref 的“选中窗口存在”前提由本方法维护。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; if !state.presentations.contains_key(&id) { // 先检查再改 current,失败时保留原选择,避免 Deref 命中不存在的 Presentation。 bail!("window is unavailable"); } state.current = id; Ok(()) } /// endor_host 故障/关闭路径调用,直接退役副窗口并丢弃未发布命令,不触发源卸载回调。 pub fn retire_window(&self, vm: &mut Vm, id: u64) -> Result<()> { if id == crate::window::MAIN_WINDOW { // 主壳由原生生命周期管理,不能通过删除 Presentation 假装已关闭 OS 主窗口。 bail!("the primary shell is closed by the OS window control"); } // 锁覆盖副窗口所有关系的撤销,后续不在此执行源回调,因此无需放锁再进入 VM。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let Some(view) = state.presentations.get_mut(&id) else { // 已退役的窗口再次清理是正常幂等操作,不能让重复关闭导致整个运行时失败。 return Ok(()); }; // 把要清理窗口的身份保存下来,随后依次断 Mount、窗口页面和焦点属性的托管引用。 let desktop = view.desktop; // model/window 负责拥有关系;Session 中删项本身不足以关闭托管资源。 let window = object(Some(&vm.get_attr(desktop, "window")?))?; view.mounts.retire(vm, desktop)?; crate::window::retire(vm, window)?; vm.set_attr(desktop, "pending", Value::Nil)?; vm.set_attr(desktop, "focus", Value::Nil)?; vm.set_attr(desktop, "focusRequested", Value::Bool(false))?; // Draining is not publication: failed-domain effects are discarded. // 故障宿主退役的命令只丢弃不发布,防止失败域遗留 Resize/Focus 等影响当前原生状态。 let _ = crate::window::take_commands(vm, window)?; // 从主资源字典删除副窗口强边,允许仍无其他引用的私有窗口对象被回收。 let windows = object(Some(&vm.get_attr(state.master, "windows")?))?; vm.dict_delete(windows, &Value::Text(id.to_string()))?; state.presentations.remove(&id); state.current = crate::window::MAIN_WINDOW; Ok(()) } /// endor_host 在正常全窗口刷新收尾调用,移除已 closing 的副窗口并保留其待发布原生命令。 pub fn finish_closed(&self, vm: &mut Vm) -> Result<()> { // 正常收尾与故障 retire_window 不同:本锁内先保存命令,再删除托管/原生资源表项。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 正常 finish_closed 只移除已标 closing 的副窗口,主壳保持到 OS 退出;其最后命令转存 retired_commands 待发布。 let ids: Vec<_> = state .presentations .iter() .filter(|(id, view)| **id != crate::window::MAIN_WINDOW && view.closing) .map(|(id, _)| *id) .collect(); // 该字典是托管拥有关系,需和原生 presentations 的删项保持一致。 let windows = object(Some(&vm.get_attr(state.master, "windows")?))?; for id in ids { // 键来自上面已筛选列表,持同一把锁时不会被其他路径删掉。 let desktop = state.presentations[&id].desktop; // 在移除资源表前取出 outbox,避免正常关闭命令失去最后的读取机会。 let window = object(Some(&vm.get_attr(desktop, "window")?))?; state .retired_commands .extend(crate::window::take_commands(vm, window)?); vm.dict_delete(windows, &Value::Text(id.to_string()))?; state.presentations.remove(&id); } state.current = crate::window::MAIN_WINDOW; Ok(()) } /// endor_host/worker 形成发布结果时调用,收集并清空各窗口 outbox;GUI 执行仍要校验 session。 pub fn take_window_commands(&self, vm: &Vm) -> Result> { // 需要可变锁消费 retired_commands;VM 侧 take_commands 也清空各自 outbox,不在此调用源方法。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 先取已移除窗口的最后命令,再合并仍活动窗口 outbox;按窗口/窗口内序号排序,不丢失正常 Close 请求。 let mut commands = std::mem::take(&mut state.retired_commands); for view in state.presentations.values() { // 活动和正在关闭但尚未移除的窗口都可能有命令,不能只收集当前窗口。 let window = object(Some(&vm.get_attr(view.desktop, "window")?))?; commands.extend(crate::window::take_commands(vm, window)?); } // 保证同窗命令按递增序号执行;不同窗口之间只提供稳定顺序,不伪造跨窗口事务顺序。 commands.sort_by_key(|command| (command.window, command.sequence)); Ok(commands) } /// endor_host 创建会话/接收 resize 时调用,更新选中窗口的逻辑尺寸,返回是否真的发生变化。 pub fn set_viewport(&self, vm: &mut Vm, viewport: Option) -> Result { if let Some(size) = viewport && (!size.width.is_finite() || !size.height.is_finite() || !(0.0..=MAX_DIMENSION as f32).contains(&size.width) || !(0.0..=MAX_DIMENSION as f32).contains(&size.height)) { // 在分配托管输入前拒绝 NaN/无穷/负值/过大值,保留原来合法尺寸和发布画面。 bail!("invalid window viewport"); } // 校验成功后再持锁更新当前窗口的尺寸,其他 Presentation 保留各自 viewport。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 尺寸比较在分配新输入对象前完成,避免相同 resize 通知改变 VM 版本而触发自我失效。 if state.layout.viewport == viewport { // 相同输入返回 false,worker 不必因此强制再排版或修改依赖版本。 return Ok(false); } if let Some(size) = viewport { // 只在有尺寸时创建托管协议对象;该局部对象先有根,设置 desktop 属性成功后再释放局部根。 let input = vm.new_object()?; // 闭包暂存写属性错误,确保任意一个字段写入失败也能经过下面的 release(input)。 let result = (|| { vm.set_attr(input, "width", Value::Number((size.width as f64).into()))?; vm.set_attr(input, "height", Value::Number((size.height as f64).into()))?; vm.set_attr(state.desktop, "viewport", Value::Object(input)) })(); // desktop 已建立 viewport 强边后释放局部根;字段/安装失败也先释放 input,再传播保存的错误。 vm.release(input)?; result?; } else { // 未知尺寸用 nil 明确表示,不把旧尺寸残留在源 ui.viewport 可见输入里。 vm.set_attr(state.desktop, "viewport", Value::Nil)?; } state.layout.viewport = viewport; // A fresh Console domain can own window inputs without replacing the // preserved displayed page until it actually opens/renders something. // 空的新 Console 域可以记录逻辑尺寸,但未展示任何 Page 时不应覆盖用户仍看到的旧域画面。 state.force_frame |= state.active.is_some() || state.pending.flatten().is_some(); Ok(true) } /// endor_host 完成 bootstrap 后调用,把 UI 命名空间、兼容名字和类型查询入口写入程序全局字典。 pub fn install_globals(&self, vm: &mut Vm, globals: ObjectRef) -> Result<()> { vm.dict_set(globals, Value::Text("ui".into()), Value::Object(self.ui))?; // 旧模块示例把 Block 写成 View;别名引用同一 Type,不建立另一个互不相容的同名类型。 vm.dict_set( globals, Value::Text("View".into()), vm.get_attr(self.ui, "Block")?, )?; // 全局名引用 ui 中同一批名义类型/函数对象,模块和 Console 不会各复制一份类型体系。 for name in TYPE_NAMES.iter().chain(QUERY_NAMES) { // bootstrap 必须已安装相应名字;取值失败就返回,不能悄悄让程序缺少某个声明的全局。 let value = vm.get_attr(self.ui, name)?; vm.dict_set(globals, Value::Text((*name).into()), value)?; } Ok(()) } /// endor_host 处理主程序结果时查询,判断源程序是否已自行打开页面,从而避免重复包装/展示返回值。 pub fn has_page(&self) -> Result { // 检查所有窗口;pending 的 Some(None) 是清空请求,不等于已经有一个新页面。 Ok(self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .presentations .values() .any(|view| view.pending.flatten().is_some() || view.active.is_some())) } /// runtime 的输入接纳路径调用,验证拖放两端的版本和注册顺序仍与发布画面一致,不修改页面。 pub fn accepts_tab_drop(&self, vm: &Vm, input: &crate::events::Input) -> bool { let Some(target) = &input.tab_drop else { // 普通事件没有拖放目标,本方法不额外限制;它们由其他输入身份检查处理。 return true; }; let Ok(state) = self.session.lock() else { // 无法可靠验证时拒绝,而不是让不可用的会话继续重排页面。 return false; }; // 拖放源与目标都必须匹配各自发布版本,且真实 Page 注册顺序仍对应标签弱身份表,防止用陈旧列表下标重排。 for (id, version) in [ (input.window, input.layout_version), (target.window, target.layout_version), ] { let Some(view) = state.presentations.get(&id) else { // 任一端窗口已经退役都使整次拖动失效。 return false; }; if view.closing || view.published_version != version { // 不仅源端,目标端布局变化也会让落点索引不再可靠。 return false; } let Ok(Value::Object(window)) = vm.get_attr(view.desktop, "window") else { // 内部关系读取异常在这个布尔验证接口统一映射为拒绝,不产生半次迁移。 return false; }; let Ok(pages) = crate::window::pages(vm.object_store(), window) else { // 页面注册表不可读时同样不能相信 GUI 发来的标签顺序。 return false; }; if view.tab_ids.len() != pages.len() || !pages .iter() .zip(&view.tab_ids) .all(|(page, (weak, _))| vm.object_store().downgrade(*page).ok() == Some(*weak)) { // 数量相同也可能已换页/重排,所以逐个比较弱身份而非只比较长度。 return false; } if id == target.window && target.index > pages.len() { // 允许插到末尾(等于 len),拒绝超出末尾的过期/无效索引。 return false; } } true } /// endor_host 的程序结果展示路径调用,经 ui.open 复用 Presentable 协议及普通值包装规则。 pub fn open_value(&self, vm: &mut Vm, value: &Value) -> Result<()> { // 使用已安装 callable 而不直接调用 new_block,源 Presentable 的覆盖因此仍会执行。 let function = object(Some(&vm.get_attr(self.ui, "open")?))?; // 结果 Page 已由页面注册表持有,本地返回根用完即释放,避免每次 open 泄漏一个根。 let page = vm.call(function, std::slice::from_ref(value))?; vm.release_value(page)?; Ok(()) } /// dispatch 的标签事件分支调用,按发布时 Tab ID 找回 Page,再调用可被源代码覆盖的页面方法。 fn dispatch_tab(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> { // 锁内只解析来源和身份,返回 Page 后释放锁,真正用户方法执行不能持有 Session 锁。 let page = { // 只借用当前窗口已发布的标签映射,不升级成新的页面根;页面注册表仍持有 Page。 let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; if state.published_version != input.layout_version { // 标签画面已经更新,旧 ID/顺序可能不再有效;拒绝后由 GUI 使用新发布数据。 bail!("event targets an obsolete presentation"); } // input.mount 在标签路径表示 Tab ID,先映回弱 Page,再确认仍在当前窗口 pages 集合,不是 Mount 几何下标。 let weak = state .tab_ids .iter() .find(|(_, id)| *id == input.mount) .map(|(weak, _)| *weak) .ok_or_else(|| anyhow::anyhow!("tab has expired"))?; // 再在真实窗口注册表中验证弱身份,不能仅凭旧 tab_ids 让已移出的页面继续响应操作。 let window = object(Some(&vm.get_attr(state.desktop, "window")?))?; crate::window::pages(vm.object_store(), window)? .iter() .copied() .find(|page| vm.object_store().downgrade(*page).ok() == Some(weak)) .ok_or_else(|| anyhow::anyhow!("Page is no longer open"))? }; if let Some(target) = &input.tab_drop { // 拖放可能跨窗口;第二段锁只验证目标,避免下方 moveToWindow 源覆盖重入时死锁。 let window = { // 回调前复制目标窗口身份;释放锁后再执行可覆盖的 moveToWindow。 let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 输入的窗口 ID 必须仍有展示上下文;不能为过期拖动自动重建一个窗口。 let view = state .presentations .get(&target.window) .ok_or_else(|| anyhow::anyhow!("drop window has closed"))?; if view.published_version != target.layout_version { // 目标标签布局改变后插入位置不可沿用,即使源页面本身仍存活也要拒绝。 bail!("drop targets an obsolete presentation"); } object(Some(&vm.get_attr(view.desktop, "window")?))? }; if target.window != input.window { // 跨窗口迁移走名义方法分派,给源子类决定是否接受/重定向,而非直接改宿主字典。 let value = invoke_member(vm, page, "moveToWindow", &[Value::Object(window)], 300_000)?; vm.release_value(value)?; } // An override may deliberately decline or redirect the move. Do // not reorder an unrelated window after that source decision. // 回调可能改写实际归属,必须在调用结束后重新读取,不能沿用最初 target 当作已成功。 let destination = { // 重新加锁读取回调后的真实归属,不沿用迁移前的 Session 借用。 let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; page_window(vm.object_store(), &state, page)? }; // 源 moveToWindow 可覆盖并拒绝/改投其他窗口;只有确认实际归属仍为预期目标才继续重排与激活。 if destination == Some(target.window) { // 用回调之后的真实顺序定位页面,避免回调中增删标签使旧下标错指其他页面。 let pages = crate::window::pages(vm.object_store(), window)?; // 找不到页面说明迁移过程中它已被关闭/移走;不能继续以默认 0 号位置重排。 let from = pages .iter() .position(|candidate| *candidate == page) .ok_or_else(|| anyhow::anyhow!("Page is no longer open"))?; // 同窗口拖动的 index 是移除前插入缝位置,向后拖时先减一;跨窗口新加入则不用这一偏移修正。 let position = if input.window == target.window && from < target.index { target.index - 1 } else { // 跨窗口插入或向前拖动保留原缝位置,不做同窗移除产生的偏移修正。 target.index }; if position >= pages.len() { // 用户方法改变了目标列表,当前计划位置已不可用,不把它静默夹到末尾。 bail!("drop destination changed during Page method"); } // 重排同样保留源 moveTab 覆盖;结果是临时拥有值,随后显式释放。 let value = invoke_member( vm, page, "moveTab", &[Value::Number((position as i64).into())], 300_000, )?; vm.release_value(value)?; // 最后激活被拖页面;若源 activate 另有决定,下一轮刷新以真实 pending 为准。 let value = invoke_member(vm, page, "activate", &[], 300_000)?; vm.release_value(value)?; } return Ok(()); } // 无拖放目标的标签事件只需一次页面方法调用;普通标签选择统一映射到 activate。 let method = match input.kind.as_str() { "tab-close" => "close", "tab-detach" => "detachToWindow", _ => "activate", }; // detach 的返回 Window 还可能用于屏幕定位,因此先保存返回根,不立即释放。 let value = invoke_member(vm, page, method, &[], 1_000_000)?; // detach 返回值可能被源覆盖成其他值,仅确为 Window 且带屏幕位置才调用 moveTo;最后统一释放原返回值。 let placed = (|| -> Result<()> { // 只有带坐标且真实返回 ManagedWindow 的拆离操作才移动 OS 资源;其他覆盖返回值正常保留语义。 if input.kind == "tab-detach" && let Some([x, y]) = input.screen_position && let Value::Object(window) = &value && vm .payload::(*window, "Window") .is_ok() { // moveTo 也经绑定方法调用,坐标转为语言数字;返回值和外层 detach 返回值分别释放。 let result = invoke_member( vm, *window, "moveTo", &[ Value::Number((x as i64).into()), Value::Number((y as i64).into()), ], 100_000, )?; vm.release_value(result)?; } Ok(()) })(); // 即使 moveTo 出错也先释放 detach 返回根,再传播 placed,防止错误路径泄漏。 vm.release_value(value)?; placed?; Ok(()) } /// endor_host 已选定输入窗口后调用,校验信封并分派宿主交互或托管事件;源代码仍在同一 VM 串行执行。 pub fn dispatch(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> { // 信封层校验保证坐标/增量等字段满足事件格式,后面仍要验证窗口、版本、Mount 与可见性。 input .validate() .map_err(|message| anyhow::anyhow!(message))?; if self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .current != input.window { // current 决定 Session 的隐式字段访问;不匹配就拒绝,避免把另一窗口输入应用到当前页。 bail!("event belongs to another window"); } // 窗口关闭通过名义 close 方法,保留源覆盖语义;直接调用宿主原生退役只用于故障清理路径。 if input.kind == "window-close" { // 复制 desktop 身份后立即释放锁,下面的 close 源覆盖可以安全重入其他 UI 方法。 let desktop = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .desktop; // Window 是源代码可覆盖的资源对象;不是 winit 的 OS Window。 let window = object(Some(&vm.get_attr(desktop, "window")?))?; // 绑定产生临时拥有值;先保存调用结果,再清理该方法值,保证报错也不跳过释放。 let bound = vm.bind_attr(window, "close")?; // 保存 Result 以便绑定值始终释放;result? 放在释放之后,避免错误路径留下根。 let result = match &bound { Value::Object(function) => vm.call_with_limits( *function, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ), _ => Err(invalid("Window.close is not callable")), }; vm.release_value(bound)?; vm.release_value(result?)?; return Ok(()); } // 内置工具打开走同一 VM 的 developerTools 能力,返回值只作临时根使用,不跨到 GUI。 if let Some(name) = input.kind.strip_prefix("developer-open:") { // 只传工具名字,页面工厂和工具状态仍在所属 VM 中创建,GUI 不接收托管返回对象。 let function = object(Some(&vm.get_attr(self.ui, "developerTools")?))?; // 工具工厂返回的页面已由托管展示关系持有,调用结束释放这里只为接收返回值而持有的根。 let page = vm.call(function, &[Value::Text(name.into())])?; vm.release_value(page)?; return Ok(()); } if matches!(input.kind.as_str(), "page-pan" | "page-zoom") { // 整页相机操作使用 page_input 中的模式/坐标检查,不走普通 Block.click。 return self.dispatch_page_view(vm, input); } if matches!(input.kind.as_str(), "viewport-pan" | "viewport-zoom") { // 局部 CanvasViewport 的相机不同于整页相机,需要按命中出现位置转换增量。 return self.dispatch_viewport_view(vm, input); } if matches!( input.kind.as_str(), "scene-pan" | "scene-orbit" | "scene-zoom" ) { // 三维交互交给场景控制器,保留源更新方法及投影坐标语义。 return self.dispatch_scene_view(vm, input); } if matches!( input.kind.as_str(), "tab-activate" | "tab-close" | "tab-detach" | "tab-drop" ) { // 标签路径的 input.mount 是 Tab ID,不是 Mount;必须在普通目标解析之前分流。 return self.dispatch_tab(vm, input); } // 普通指针输入取同一锁内的版本/目标/完整出现候选快照;随后不持 session 锁执行源 hitTest 或事件回调。 let (weak, desktop, candidates, viewport, surface) = { // 一次锁内拍下同版本信息,随后 hitTest/回调在锁外运行,避免读到窗口选择的半次变化。 let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; if state.published_version != input.layout_version { // 弱对象尚存活不代表几何仍适合旧坐标;版本不符时整次普通输入失效。 bail!("event targets an obsolete presentation"); } ( *state .mounts .active .get(&input.mount) .ok_or_else(|| anyhow::anyhow!("Mount is no longer active"))?, state.desktop, state.mounts.ordered(), state.layout.viewport, state.surface.clone(), ) }; if input.kind == "scroll" { // 升级弱 Mount 获得临时拥有根,保证检查/回调期间不会因源代码删关系而悬空。 let mount = vm .upgrade(weak)? .ok_or_else(|| anyhow::anyhow!("scroll Mount expired"))?; // 暂存滚动处理错误,以便退出前无条件释放升级得到的 Mount 根。 let result = (|| { // 读取这一具体出现位置的几何,不使用共享 Block 上的全局坐标。 let data = vm.payload::(mount, "Mount")?; // scroll 必须有指针位置,才能验证是否仍处于裁剪与页面视口内。 let point = input .point .ok_or_else(|| anyhow::anyhow!("scroll has no pointer"))?; // scroll 不只校验 Mount 身份,还要指针仍在页面视口和出现裁剪中;相机/面板映射后的增量才可交给 scrollBy。 if !data.mounted || data.version != input.layout_version || !surface.contains(point, viewport) || !data .geometry .contains(crate::geometry::Point::new(point[0], point[1])) { bail!("scroll targets an obsolete or clipped Mount"); } // 屏幕增量还需转换成 Mount 所属空间,不能直接当作内容滚动的逻辑距离。 let delta = input .delta .ok_or_else(|| anyhow::anyhow!("scroll has no delta"))?; let Some(delta) = data.geometry.logical_delta(input.point, delta) else { // 投影不可逆/无法形成有效局部增量时忽略此轮,不伪造一个零或屏幕空间增量。 return Ok(()); }; // 通过托管方法保留滚动协议,调用预算单独有界;方法根与结果根随后各自释放。 let method = vm.bind_attr(mount, "scrollBy")?; // 数量/类型之外的滚动规则交给该方法;临时绑定值在 result 成功/失败两种情况下都释放。 let result = match method { Value::Object(function) => vm.call_with_limits( function, &[ Value::Number((delta[0] as f64).into()), Value::Number((delta[1] as f64).into()), ], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 100_000, }, ), _ => Err(invalid("scrollBy is not callable")), }; vm.release_value(method)?; vm.release_value(result?)?; Ok(()) })(); // 滚动失败也清理临时根;只有释放后才把保存的 result 交回运行时。 vm.release(mount)?; return result; } // 坐标点击重新执行源协议选择按下和释放目标,二者同出现才分发;无坐标输入使用已捕获的弱目标。 let mount = if let (Some(press), Some(release)) = (input.press_point, input.point) { if [press, release] .iter() .any(|point| !surface.contains(*point, viewport)) { // 按下或释放任一点已离开可见页,不能仅凭旧 Mount 身份触发点击。 return Ok(()); } let Some(mount) = crate::source_hit::resolve(vm, desktop, candidates, press, release)? else { // resolve 会运行真实源 hitTest;没有共同有效命中目标是正常无事件结果。 return Ok(()); }; mount } else { // 无坐标输入已有捕获目标,升级弱身份后仍要在下面检查 mounted/interactive。 vm.upgrade(weak)? .ok_or_else(|| anyhow::anyhow!("Mount has expired"))? }; // 同一所有权模式覆盖三维拾取、指针和普通事件:保存任何错误,最后统一 release(mount)。 let result = (|| { // 复制 payload 结束 VM 只读借用,后续分配事件或执行用户方法需要可变借用 VM。 let data = vm.payload::(mount, "Mount")?.clone(); // 3D 视口点击先生成 RayHit3D 并附到 Event.hit,再沿视口出现传播,不能只按二维外框假定击中某网格。 if data.geometry.scene3d_viewport && input.kind == "click" && let Some(point) = input.point { // 用原生 callable 承接 3D 拾取与事件传播;captures 显式保活 Mount 与 desktop。 let function = vm.allocate(ContextNativeFunction::new( "3D pointer click", crate::scene3d_model::PointerPick { mount, desktop, point: crate::geometry::Point::new(point[0], point[1]), }, vec![mount, desktop], ))?; // 先暂存结果,释放临时 callable 根后再传播错误,保持失败路径也无额外根残留。 let result = vm.call_with_limits( function, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ); vm.release(function)?; vm.release_value(result?)?; return Ok(()); } if !data.mounted { // 对象可达但已经卸载,不能把旧事件传给仍被其他地方引用的业务 Block。 bail!("event targets an obsolete presentation"); } if !data.geometry.interactive { // 几何可能是装饰或明确禁交互;不能因为外框命中就默认接受所有输入。 bail!("Mount does not accept input"); } if input.kind.starts_with("pointer") { // 原始指针事件用于图形交互,保留具体 kind 与坐标,不转换为高层 click。 let p = input .point .ok_or_else(|| anyhow::anyhow!("pointer needs point"))?; // PointerDispatch 在 VM 中执行并显式捕获所有托管依赖,不从 GUI 线程运行源处理器。 let f = vm.allocate(ContextNativeFunction::new( "native pointer event", crate::bridge::graphics::PointerDispatch { owner: data.block, mount, desktop, kind: input.kind.clone(), point: crate::geometry::Point::new(p[0], p[1]), }, vec![data.block, mount, desktop], ))?; // 一次有界的 VM 调用完成指针分派;先释放 callable 根,再传播执行错误。 let result = vm.call_with_limits( f, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ); vm.release(f)?; vm.release_value(result?)?; return Ok(()); } // 普通点击/焦点先登记展示焦点请求,再按 kind 调 Block.click 或 Mount.requestFocus;生效仍需后续完整刷新。 vm.set_attr( desktop, "focus", Value::Number((data.geometry.id as i64).into()), )?; vm.set_attr(desktop, "focusRequested", Value::Bool(true))?; // click 的 receiver 是 Block,Mount 作为出现上下文;focus 的 receiver 则直接是 Mount。 let (owner, name, args) = match input.kind.as_str() { "click" => (data.block, "click", vec![Value::Object(mount)]), "focus" => (mount, "requestFocus", vec![]), _ => bail!("unsupported native event"), }; // 保留源方法覆盖;临时绑定值需要在调用成功与失败两种情况下都释放。 let bound = vm.bind_attr(owner, name)?; // 临时绑定保持 receiver;错误同样先保存,释放绑定值后才传播。 let result = match &bound { Value::Object(function) => vm.call_with_limits( *function, &args, endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ), _ => Err(invalid("event method is not callable")), }; vm.release_value(bound)?; vm.release_value(result?)?; Ok(()) })(); vm.release(mount)?; result } /// endor_host 在执行/清理边界调用,压缩节点弱表和每窗快照缓存;不改写业务对象拥有关系。 pub fn reclaim(&self, vm: &mut Vm, _roots: &mut Vec) -> Result<()> { // 先回收托管环,再扫描弱节点表,只保留能升级的身份;render_cache.prune 同步清除死亡业务索引。 vm.collect_cycles(); // VM 环回收结束后再压缩原生弱表,避免用回收前仍能升级的状态保留多余缓存索引。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 重建表只保留当前能升级的弱身份;不能把升级得到的临时强根永久留在宿主索引中。 let mut live = Vec::new(); for weak in &state.nodes { if let Some(object) = vm.upgrade(*weak)? { // 升级是有拥有权的 VM 操作,探测结束立即释放,再保存原弱引用。 vm.release(object)?; live.push(*weak); } } state.nodes = live; for presentation in state.presentations.values_mut() { // 同一业务节点可能缓存于多个窗口,逐窗剪枝,避免只清理 current 的派生状态。 presentation.render_cache.prune(vm.object_store()); } Ok(()) } /// endor_host 的输入/刷新失败路径调用,修复未发布 pending;不会回滚 REPL 中已成功完成的业务赋值。 pub fn discard_pending(&self, vm: &mut Vm) -> Result<()> { // 修复宿主 pending/注册关系,不运行用户方法;因此持锁逐窗处理不会进入回调重入路径。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; for presentation in state.presentations.values_mut() { // 真实 Window 注册表是归属依据,不能单靠失败前的 Presentation.active 恢复页面。 let window = object(Some(&vm.get_attr(presentation.desktop, "window")?))?; // A failed first render must not become visible on the next input // merely because show() already registered its Page with the Window. // 第一次 render 失败时撤销尚未发布的 Page 登记,避免下一次无关输入把失败页意外显示出来。 if presentation.active.is_none() && presentation.last_tabs.is_empty() { // 从未发布过的窗口不保留失败 render 偶然注册的 Page;拷贝后逐项移除,避免迭代时改集合。 let unpublished = crate::window::pages(vm.object_store(), window)?.to_vec(); for page in unpublished { // 使用宿主撤销入口,不再执行可能再次失败或产生副作用的源页面方法。 crate::window::unregister_page_host(vm, window, page)?; } } // 撤销首次失败登记后重新读表;先前成功的 close/move 仍可能已改变现有窗口的拥有关系。 let pages = crate::window::pages(vm.object_store(), window)?; // 失败输入已成功执行的关闭/迁移语句不回滚;重新选择仍在真实窗口注册表中的页,修复 pending 与拥有关系。 let next = if presentation.closing { // 正在关闭的资源不能为了恢复画面而重新挑一页继续展示。 None } else { // 原 active 仍登记就保留,否则选择当前真实列表首项;空表自然得到 None。 presentation .active .filter(|page| pages.contains(page)) .or_else(|| pages.first().copied()) }; // Earlier successful statements in a failed REPL input remain live. // Repair ownership changes before the next publication so a closed or // moved Page cannot survive in an old context's mount dictionary. presentation.pending = if next != presentation.active || presentation.closing { // Some(None) 是明确卸载旧页,不等同于无刷新请求的 None。 Some(next) } else { // 无需改变活动页时清掉失败请求,避免后续输入重复展示失败候选。 None }; vm.set_attr( presentation.desktop, "pending", presentation .pending .flatten() .map(Value::Object) .unwrap_or(Value::Nil), )?; if presentation .active .is_some_and(|page| vm.object(page).is_err()) { // active 的托管对象已不存在时去除 Rust 借用句柄,不能让下一轮继续拿它提取快照。 presentation.active = None; } } Ok(()) } /// endor_host 在刷新稳定循环中调用,重算失效绑定;回调可继续改变对象,随后 prepare_frame 才排空变化。 pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> { // 只在锁内快照弱节点与宿主桌面集合,实际绑定回调在锁外执行,避免回调访问 UI 时死锁。 let (nodes, desktops) = { // 锁内只复制弱索引与排除集合,锁外 binding::refresh 才真正执行源计算。 let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; (state.nodes.clone(), state.host_desktops()) }; // binding::refresh 在所属 VM 中运行受控计算并返回实际评估次数,不把节点总数当作工作量。 let evaluated = crate::binding::refresh(vm, &nodes, &desktops)?; // 计算回调已返回,重新短暂加锁累计次数,不把会话锁贯穿用户代码。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 诊断计数饱和增长,不让长期会话的统计溢出反过来打断有效程序执行。 state.binding_evaluations = state.binding_evaluations.saturating_add(evaluated); Ok(()) } /// endor_host 每轮刷新调用,广播 VM 变化后为 current 决定无工作、空帧或带视觉根的新一轮布局。 pub fn prepare_frame(&self, vm: &mut Vm) -> Result>>> { // 一次排空变更必须广播所有 Presentation,不能让先刷新的窗口吃掉其他窗口共享业务数据的失效通知。 let changes = vm.take_changes(); // 本锁内广播变化与提取只读快照,保证 current 根与各窗口的失效观察来自同一批更新。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 本轮目标优先 pending,包括显式清空;快照提取返回 None 代表目标无页面,不等于无需刷新。 let active = state.pending.unwrap_or(state.active); // One VM change drain feeds every presentation before selecting a root. // Otherwise a window refreshed first could consume another window's work. for presentation in state.presentations.values_mut() { // 每个窗口都必须先观察同一批变化,即使本轮最后只为 current 提取一个根。 presentation.observe_changes(vm, &changes); } // 没页面得到 Ok(None);有页面但快照验证失败则传播 Err,不能伪装成正常空帧。 let root = active .map(|v| state.render_cache.snapshot(vm.object_store(), v)) .transpose()?; // 视觉缓存用 Arc 身份快速判断根未变;即使根相同,焦点、滚动、标签或源依赖仍可能要求重绘。 let same = match (&root, &state.last_snapshot) { (Some(a), Some(b)) => Arc::ptr_eq(a, b), (None, None) => true, _ => false, }; // 焦点请求是宿主控制状态,视觉树即使复用同一 Arc 也需要一次完整帧处理。 let focus_changed = matches!( vm.get_attr(state.desktop, "focusRequested"), Ok(Value::Bool(true)) ); // 标签标题/顺序可独立于内容根变化,所以投影后与上次提交结果单独比较。 let tabs = project_tabs(vm, &mut state)?; // 汇总显式展示请求、视觉树、焦点/滚动及强制失效;任一变化都不能用 Arc 相同快速跳过。 let changed = state.pending.is_some() || !same || focus_changed || matches!( vm.get_attr(state.desktop, "scrollRequested"), Ok(Value::Bool(true)) ) || state.force_frame || tabs != state.last_tabs; if changed { // 只在确有新帧工作时记录快照诊断,避免空闲轮询覆盖最后一次有效失效原因。 if !same { // 根身份变化才归因于 snapshot;仅标签/焦点变更不伪造快照失效记录。 state.last_invalid_dependency = format!("snapshot {}", state.render_cache.last_invalidation); } state.snapshot_visited = state.render_cache.visited; state.snapshot_reused = state.render_cache.reused; } // 双层 Option 三态:外 None 无工作;Some(None) 需提交空帧;Some(Some(root)) 需布局/绘制根。 Ok(changed.then_some(root)) } /// 布局完成路径调用,提交桥接内部几何和生命周期,供后续 paint 使用;外部发布由 endor_host 稳定循环完成。 pub fn commit_frame( &self, vm: &mut Vm, snapshot: Option>, frame: &crate::document::Frame, ) -> Result<()> { // 同步 Mount 和更新桥接状态时需要排他访问;调用用户 hooks 前显式 drop(state)。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // pending 的 Some(None) 要清空展示,不能简单 or(active) 又把旧页取回来。 let page = state.pending.unwrap_or(state.active); // synchronize 必须使用本轮选中窗口的私有资源域,而不是 master 的全局注册表。 let desktop = state.desktop; // 同步过程可能升级节点,用弱表快照避免同时借用 Session 的其他可变字段。 let nodes = state.nodes.clone(); // 提交几何同步清除临时 layout Context,并产生挂载/卸载序列;提交桥接状态不等于已写 GUI 共享文档。 let hooks = state .mounts .synchronize(vm, desktop, page, nodes, frame, false)?; // 暂存提交/钩子错误,无论中途哪一步失败都经过下方 hooks 根释放。 let result = (|| { if snapshot.is_none() { // 空帧不应残留旧出现几何,否则下一次输入可能命中已经退场的内容。 state.layout.clear_occurrences(); } if let Some(page) = state.pending.take() { // 消费本轮请求;没有请求就沿用 active,不凭快照的 Option 擅自改业务页面身份。 state.active = page; } vm.set_attr(state.desktop, "pending", Value::Nil)?; vm.set_attr(state.desktop, "focusRequested", Value::Bool(false))?; vm.set_attr(state.desktop, "scrollRequested", Value::Bool(false))?; state.force_frame = false; state.last_snapshot = snapshot; state.last_tabs = frame.tabs.clone(); state.surface = frame.surface.clone(); // 让随后源回调看到本轮几何与版本,但真正对外原子发布要等整个刷新循环稳定,回调可能继续触发下一轮。 state.published_version = frame.layout_version; // Hooks may call Page/container APIs; never hold the session mutex // across user code. Publish only after the refresh loop settles. drop(state); if let Some(hooks) = hooks { // 用户钩子可能再次修改页面,执行后 endor_host 仍会检查是否需要下一轮刷新。 let value = vm .call_with_limits( hooks, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error))?; vm.release_value(value)?; } Ok(()) })(); if let Some(hooks) = hooks { // 调用成功、VM 报错或提交字段写入失败都释放驱动拥有根,不能依赖 Rust Option 自动清理。 vm.release(hooks)?; } result } /// endor_host 在 LayoutProgress::Complete 后调用,按帧的出现顺序启动第一片源 paint 工作。 pub fn begin_paint_frame( &self, vm: &mut Vm, root: Option>, frame: crate::document::Frame, ) -> Result { let (desktop, mounts) = { // 取绘制顺序与弱身份只读 Session,source_paint::start 在锁外执行 VM 协议。 let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 按最终帧绘制顺序查活动 Mount 弱身份,每条都必须存在;不按 HashMap 顺序调用源 paint。 let mounts = frame .mounts .iter() .map(|mount| { // 以 frame.mounts 的顺序查活动弱身份;缺项代表布局/挂载不一致,必须报错而非漏画。 state .mounts .active .get(&mount.id) .copied() .map(|weak| (mount.id, weak)) .ok_or_else(|| anyhow::anyhow!("paint Mount is unavailable")) }) .collect::>>()?; (state.desktop, mounts) }; // 每片预算 256,由 source_paint 保留未完成续体;此处既可能返回 Draft,也可能返回 Work。 let progress = crate::source_paint::start(vm, desktop, mounts, root, frame, 256)?; self.accept_paint_progress(progress) } /// endor_host 保存 PendingPaint 后调用,在同一 VM 上继续最多一片工作,不从头重放 paint 源方法。 pub fn resume_paint_frame( &self, vm: &mut Vm, work: crate::source_paint::Work, ) -> Result { // Work 拥有前片续体和草稿,消费后返回新的挂起 Work 或最终完整 Draft。 let progress = work.resume(vm, 256)?; self.accept_paint_progress(progress) } /// begin_paint_frame/resume_paint_frame 共用收尾,只有 Complete 才更新展示域的完整 paint 依赖。 fn accept_paint_progress( &self, progress: crate::source_paint::Progress, ) -> Result { if let crate::source_paint::Progress::Complete(draft) = &progress { // 借用 Draft 而非消费,调用方仍需把它交回运行时进行验证和文档发布。 // 只有完整 Draft 写入依赖,Pending 不改变上一轮已提交读取集合。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 完成后过滤所有窗口私有 desktop 读取,再拆对象版本与名称依赖;挂起切片不覆盖上一份完成的 paint 依赖。 let desktops = state.host_desktops(); // 私有 desktop 是宿主记账读,排除后才得到源程序真正依赖的属性/名字集合。 let reads = draft .reads .iter() .filter(|read| !desktops.contains(&read.object())) .cloned() .collect(); // 按对象版本读取与命名查找拆分,下一轮 observe_changes 使用各自的失效判断。 let (reads, names) = crate::presentation::split_reads(reads); state.paint_reads = reads; state.paint_names = names; state.paint_slices = draft.slices; } Ok(progress) } /// endor_host 的 resize/输入替换/退役路径调用,取消所属 VM 的 paint 续体并强制该窗口重新刷新。 pub fn cancel_paint_frame( &self, vm: &mut Vm, window: u64, work: crate::source_paint::Work, ) -> Result<()> { // 先尝试取消 VM 续体,再尝试选原窗口并强制下一帧;前一操作失败也执行必要的宿主失效准备。 let cancelled = work.cancel(vm); // 这一步即使 cancelled 是 Err 也先尝试;否则失败的取消会留下“旧帧看似仍有效”的缓存状态。 let invalidated = self.select_window(window).and_then(|()| { self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .force_frame = true; Ok(()) }); // 所有必要清理尝试完成后再按顺序传播错误,不让前一个 ? 跳过后一个失效操作。 cancelled?; invalidated } /// endor_host 从 prepare_frame 得到新根后调用,建立出现结构和恢复检查点,再开始有预算的源布局。 pub fn begin_layout_frame( &self, vm: &mut Vm, root: Option>, ) -> Result { // 一个 VM 同时只能推进原来的续体;不能在仍只读/挂起时再开另一帧破坏调用与恢复上下文。 anyhow::ensure!( !vm.has_suspended_call() && !vm.object_store().is_read_only(), "cannot prepare another frame during suspended layout" ); // 显式空页面无需源布局,清测量/布局读取并推进空帧版本,再同步退役旧 Mount;不是无工作返回。 let Some(root) = root else { // 空帧已没有可复用的测量结果和源布局依赖,分三项清理,不能继续引用旧页读取集合。 self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .measure_cache .clear(); self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .layout_reads .clear(); self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .layout_names .clear(); // 空帧仍有窗口、标签和新版本:GUI 必须能识别旧出现位置已全部退场。 let frame = { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 不复用旧版本,否则旧输入可能被当成对应这个空页面的有效事件。 state.layout.version = state .layout .version .checked_add(1) .ok_or_else(|| anyhow::anyhow!("layout version exhausted"))?; crate::document::Frame { window: state.current, layout_version: state.layout.version, tabs: project_tabs(vm, &mut state)?, ..crate::document::Frame::default() } }; // 走共同提交路径退役 Mount/运行卸载钩子,不能仅返回一个默认空 Frame 留下旧生命周期。 self.commit_frame(vm, None, &frame)?; return Ok(LayoutProgress::Complete(Box::new(frame))); }; // 准备私有布局草稿与缓存副本,之后的源调用在锁外运行;正式状态等完成时再写回。 let (mut layout, cache, desktop, page, nodes) = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; ( state.layout.clone(), state.measure_cache.clone(), state.desktop, state.pending.unwrap_or(state.active), state.nodes.clone(), ) }; // 标签投影会更新稳定 ID 映射,因此短暂可变借用 Session,而非沿用上面的只读锁。 let tabs = { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; project_tabs(vm, &mut state)? }; // 记录真实原生页面视口供页面相机 clamp;兼容 Page.mode 只改变根约束,不永久篡改保存的窗口尺寸。 let full_viewport = layout.viewport; // 根未声明特殊表面时采用默认页面模式;保存副本用于完成时添加范围和相机 clamp。 let surface = root.surface.clone().unwrap_or_default(); layout.page_mode = root.surface.as_ref().map(|surface| surface.mode); { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 按全会话高水位分配结构出现 ID,取消后也不回收已暴露过的数值身份。 layout.reserve_identity_floor(state.next_mount); } // 先建立出现结构与 ID,尚未执行源 measure/arrange;结构供 Mount 准备与协议调用查上下文。 let mut frame = layout.structure(&root)?; { // 本轮已暴露的 ID 即使后续取消也不能回收,所以马上推进全会话高水位,而非等最终提交。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.next_mount = state.next_mount.max(layout.identity_high_watermark()); frame.window = state.current; } // 同步准备阶段可能暂改 Mount;把可恢复检查点、延后钩子及有序弱身份一起交给后续工作。 let (hooks, mounts, checkpoint) = { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 在准备态 synchronize 改写 Mount 前保留旧字典与 payload;准备失败立即恢复,不能留下半套父子关系。 let checkpoint = state.mounts.checkpoint(vm, desktop)?; // true 表示准备模式:Mount 关系可供布局读取,但生命周期钩子延后,不在此执行用户代码。 let hooks = match state .mounts .synchronize(vm, desktop, page, nodes, &frame, true) { Ok(hooks) => hooks, Err(error) => { // 即使结构准备失败也先恢复旧关系;恢复失败本身也必须作为错误报告。 checkpoint.restore(vm, desktop, &mut state.mounts)?; return Err(error.into()); } }; (hooks, state.mounts.ordered(), checkpoint) }; // 检查点、暂存 hooks、所属窗口与根随源布局 Work 一起保留,跨量子不能只留下 VM 续体。 let context = LayoutContext { surface, root: root.clone(), tabs, full_viewport, desktop, window: frame.window, hooks, checkpoint, }; // Draft 携带布局求解用的私有 runtime/cache;reads 从空开始收集本轮实际依赖。 let draft = crate::source_layout::Draft { frame, runtime: layout, cache, reads: Vec::new(), }; // 暂存 Result 而不立即 ?,让 accept_layout_progress 在出错时仍能恢复 context 的检查点。 let progress = crate::source_layout::start(vm, desktop, mounts, root, draft, 256); self.accept_layout_progress(vm, context, progress) } /// endor_host 在布局/绘制挂起让出 worker 时调用,累计诊断次数,不改变下一片执行预算。 pub fn note_refresh_yield(&self) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 统计饱和而非溢出报错,避免长期运行时仅因诊断计数达到上限而中断。 state.refresh_yields = state.refresh_yields.saturating_add(1); Ok(()) } /// endor_host 重定向刷新时调用,分别记录重启以及本轮是否确实取消 layout/paint,供 stats 诊断。 pub fn note_refresh_restart( &self, cancelled_layout: bool, cancelled_paint: bool, ) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 一次重启不一定同时取消两个阶段;布尔转 usize 让各计数只增加实际发生的事件。 state.refresh_restarts = state.refresh_restarts.saturating_add(1); state.layout_cancels = state .layout_cancels .saturating_add(usize::from(cancelled_layout)); state.paint_cancels = state .paint_cancels .saturating_add(usize::from(cancelled_paint)); Ok(()) } /// cancel_layout_frame/布局出错路径调用,恢复所属窗口的检查点并释放尚未执行的生命周期驱动。 fn discard_layout_context(&self, vm: &mut Vm, context: LayoutContext) -> Result<()> { // 消费 context 后只提取恢复所需的资源,树/标签等纯 Rust 值自然释放,不执行用户回调。 let LayoutContext { surface: _, desktop, window, hooks, checkpoint, .. } = context; // 恢复必须在原窗口;若窗口或 session 锁已不可用,至少释放检查点拥有根,不能尝试恢复到另一个窗口。 let selected = self.select_window(window); let restored = if selected.is_ok() { // 选窗成功也可能锁损坏,分别处理“可恢复”和“只能释放检查点根”两种情况。 match self.session.lock() { Ok(mut state) => { // restore 还原准备阶段暂改的 Mount 字典/payload;随后强制下一帧重新建立几何。 let result = checkpoint.restore(vm, desktop, &mut state.mounts); state.force_frame = true; result.map_err(anyhow::Error::from) } // 锁不可用时仍先释放 VM 拥有根,再报告会话损坏,不能泄漏也不能假装恢复成功。 Err(_) => checkpoint .release(vm) .map_err(anyhow::Error::from) .and(Err(anyhow::anyhow!("UI session poisoned"))), } } else { // 原窗口已不可选择,不拿 current 的其他窗口去恢复旧检查点,仅清理拥有权。 checkpoint.release(vm).map_err(anyhow::Error::from) }; // 无论选择/恢复是否失败都先尝试释放延迟 hooks;保存结果后再按优先级传播错误。 // 不运行待取消的钩子;Option 转置后保存错误,确保上面的恢复尝试已完成。 let released = hooks.map(|hooks| vm.release(hooks)).transpose(); selected?; restored?; released?; Ok(()) } /// endor_host 取消/重定向刷新时调用,分别取消 owning VM 续体与恢复准备态 Mount,保留外部已发布帧。 pub fn cancel_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> { // Work 和 Context 绑定为一对,不能只取消 VM 调用而丢下临时挂载关系。 let PendingLayout { work, context } = pending; // 取消只读执行与恢复 Mount 是两件事,均先尝试;没有继续执行原续体时也不能漏掉检查点恢复。 let cancelled = work.cancel(vm); let restored = self.discard_layout_context(vm, context); cancelled?; restored } /// endor_host 撤销失败候选布局时调用,与正常取消共用同一清理实现,不另起一轮布局回放源代码。 pub fn abort_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> { self.cancel_layout_frame(vm, pending) } /// endor_host 持有 PendingLayout 后调用,在同一 VM、原窗口和原检查点上恢复最多一片布局工作。 pub fn resume_layout_frame( &self, vm: &mut Vm, pending: PendingLayout, ) -> Result { // 解包只转移拥有权,仍将这一份 context 原样传到完成/错误收尾,不能重建新的检查点。 let PendingLayout { work, context } = pending; // 恢复时原窗口不存在则取消 owning VM 调用并清上下文,不重新建窗口或重放源布局。 if let Err(error) = self.select_window(context.window) { // 先保存原选择错误,取消与恢复均尝试执行,最后按清理结果和原错误顺序返回。 let cancelled = work.cancel(vm); let restored = self.discard_layout_context(vm, context); cancelled?; restored?; return Err(error); } // 不在这里用 ? 丢掉错误,否则失败续体配套的 context 没有机会恢复/释放。 let progress = work.resume(vm, 256); self.accept_layout_progress(vm, context, progress) } /// begin_layout_frame/resume_layout_frame 共用入口,把源布局结果转成桥接进度,集中处理恢复和清理。 fn accept_layout_progress( &self, vm: &mut Vm, context: LayoutContext, progress: Result, ) -> Result { match progress { // Pending 原样捆绑 Work 与 Context;Complete 才投影、提交,Error 则撤销准备态上下文。 Ok(crate::source_layout::Progress::Pending(work)) => { Ok(LayoutProgress::Pending(Box::new(PendingLayout { work, context, }))) } Ok(crate::source_layout::Progress::Complete(draft)) => self .finish_layout_frame(vm, context, draft) .map(|frame| LayoutProgress::Complete(Box::new(frame))), Err(error) => { // 源布局失败不能发布 draft;先撤销准备态关系并释放检查点/钩子,再传播执行错误。 self.discard_layout_context(vm, context)?; Err(error) } } } /// accept_layout_progress 收到 Complete 时调用,投影最终几何、保存依赖和提交 Mount,交回待绘制帧。 fn finish_layout_frame( &self, vm: &mut Vm, context: LayoutContext, draft: crate::source_layout::Draft, ) -> Result { // 消费开始阶段保存的上下文,所有元数据都来自这一轮,不能临时从另一窗口重取。 let LayoutContext { mut surface, root, tabs, full_viewport, desktop, window, hooks, checkpoint, } = context; // 消费完整求解结果;reads/cache 在生命周期回调前保存,才能发现钩子对布局依赖的新修改。 let crate::source_layout::Draft { mut frame, runtime: mut layout, mut cache, mut reads, } = draft; // 把全部投影/提交错误保存在闭包结果中,外层始终尝试释放 hooks 和 checkpoint 的拥有根。 let result: Result = (|| { self.select_window(window)?; { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 先在布局坐标中处理滚动,再叠二维视口/面板投影;绘制与输入共享最终 Frame 几何。 crate::scroll::project(vm, &state.mounts, &root, &mut frame)?; crate::viewport::project(&root, &mut frame)?; } // Native layout may replace the structural Frame; attach chrome only // after either layout path has produced the final page geometry. // 小原生快速路径可能返回新的 Frame,标签/窗口与页面相机必须在最终布局之后重新附加。 frame.tabs = tabs; frame.window = window; if let Some(root) = frame.mounts.first() { // 首个出现是根几何,作为页面内容范围;没有出现时保留已有表面范围。 surface.extent = crate::layout::Size { width: root.width, height: root.height, }; } if let Some(viewport) = full_viewport { // 用真实整页视口约束相机,不能用为特殊 Page.mode 改写过的局部布局约束。 surface.clamp(viewport); } frame.surface = surface; // Capture versions before lifecycle hooks can change a dependency. // 排除所有窗口的宿主私有记账读取;共享原生方法可能捕获主 desktop,不能只排除 current。 let desktops = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .host_desktops(); // Shared intrinsic functions capture the primary desktop even when // invoked from another window. Desktop bookkeeping is private host // state; Window, viewport input and business dependencies remain. // 源布局实际业务读取版本在生命周期 hooks 前保存,hooks 改写依赖后下一轮才能识别失效并重算。 reads.retain(|read| !desktops.contains(&read.object())); cache.exclude_host_desktops(&desktops); // 对象版本读取和命名查找分开存储,供 Presentation.observe_changes 判断不同类别失效。 let (reads, names) = crate::presentation::split_reads(reads); // 焦点输入只接受能表示为非负 u64 的数字,nil/错误类型自然表示没有焦点。 frame.focus = match vm.get_attr(desktop, "focus") { Ok(Value::Number(id)) => id.as_i64().and_then(|n| u64::try_from(n).ok()), _ => None, }; // 焦点 ID 仅保留仍存在且可交互的出现,切页/树编辑移除目标后自动清掉失效焦点。 frame.focus = frame .focus .filter(|id| frame.mounts.iter().any(|m| m.id == *id && m.interactive)); { // 生命周期回调前先写回本轮计算结果;回调产生的新变化随后由运行时刷新循环再观察。 let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; layout.viewport = full_viewport; state.layout = layout; state.measure_cache = cache; state.layout_reads = reads; state.layout_names = names; } // 几何先提交到桥接状态,再执行准备阶段延后的挂载 hooks;其变化由全窗口刷新循环再收敛。 self.commit_frame(vm, Some(root), &frame)?; if let Some(hooks) = hooks { // 准备阶段暂存的驱动现在才执行;无论返回值业务上是否使用都需释放其 VM 根。 let value = vm .call_with_limits( hooks, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error))?; vm.release_value(value)?; } Ok(frame) })(); // 收尾计算无论成功/失败,都先释放 hooks 与检查点的拥有根;只有所有必要清理通过才返回帧。 // 两项释放都先求值,不能在 hooks 释放处直接 ? 而跳过 checkpoint 的清理。 let released_hooks = hooks.map(|hooks| vm.release(hooks)).transpose(); // 此阶段完成/报错均消费检查点根;这里是释放,不是把已提交几何回滚到准备前。 let released_checkpoint = checkpoint.release(vm); // 所有清理尝试结束后才传播主结果与释放错误,不把失败帧交给绘制/发布调用者。 let frame = result?; released_hooks?; released_checkpoint?; Ok(frame) } /// endor_host::compile 的兼容一次性执行路径调用,读取源程序安排展示的 Page 并提取不可变视觉树。 pub fn rendered(&self, vm: &Vm) -> Result>> { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; // 此兼容提取接口只读取待展示 Page,不回退 active;持久增量路径使用 prepare_frame 的 active/pending 选择。 state .pending .flatten() .map(|v| block::snapshot(vm.object_store(), v)) .transpose() .map_err(Into::into) } } /// calls 创建状态/协议结果时调用,从当前会话取 desktop,再给对象附上指定的名义类型。 fn attach_named_type( cx: &mut NativeContext<'_>, object: ObjectRef, name: &str, session: &Shared, ) -> Result<(), VmError> { // 仅复制资源域身份,释放锁后才操作 VM;不会把 Session 锁延伸到类型附加路径。 let desktop = session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; attach_desktop_type(cx, object, name, desktop) } /// bridge/model/runtime 的原生分配器调用,给已分配 payload 附上同一 desktop 注册的语言类型。 pub fn attach_desktop_type( cx: &mut NativeContext<'_>, instance: ObjectRef, name: &str, desktop: ObjectRef, ) -> Result<(), VmError> { // 缺少类型条目直接返回错误,不能自动创建同名 Type 让实例与已有契约不兼容。 let definition = desktop_type(cx.store(), name, desktop)?; cx.attach_type(instance, definition) } /// 签名构造、能力检查和各分配器调用,查询 bootstrap 注册的精确 Type 身份,不按名字新建类型。 pub(crate) fn desktop_type( store: &endor_core::ObjectStore, name: &str, desktop: ObjectRef, ) -> Result { // 查当前 desktop 的实际字典对象和精确类型条目;不是按名字创建新类型,缺条目沿 object 解码报错。 let types = object(Some(&store.get_attr(desktop, "types")?))?; object(store.dict_get(types, &Value::Text(name.into()))?.as_ref()) } /// endor_host::compile 在 main 未通过页面 API 自行展示时调用,把 main 返回的 Block 提取成不可变根。 pub fn snapshot(vm: &Vm, value: &Value) -> Result> { // 兼容 main 必须返回真实对象;标量值的 ui.open 回退规则不在这个直接快照接口中执行。 let Value::Object(root) = value else { bail!("main must return a Block"); }; // block::snapshot 继续检查 payload/树语义;仅是 Value::Object 还不足以证明它可展示。 block::snapshot(vm.object_store(), *root).map_err(Into::into) }