From fbc5bed19e11d764a0b081b1a32f66ff9e7bd1b7 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 16:36:11 +0800 Subject: [PATCH] docs: explain UI bridge callers, control flow and VM ownership --- src/bridge/ui_bridge.rs | 501 ++++++++++++++++++++---- src/bridge/ui_bridge/bootstrap.rs | 65 ++- src/bridge/ui_bridge/calls.rs | 176 ++++++++- src/bridge/ui_bridge/developer_pages.rs | 32 +- src/bridge/ui_bridge/page_input.rs | 51 ++- src/bridge/ui_bridge/signatures.rs | 64 ++- src/bridge/ui_bridge/start_page.endor | 38 ++ src/bridge/ui_bridge/tree.rs | 54 ++- 8 files changed, 878 insertions(+), 103 deletions(-) diff --git a/src/bridge/ui_bridge.rs b/src/bridge/ui_bridge.rs index f849c4e..8b89459 100644 --- a/src/bridge/ui_bridge.rs +++ b/src/bridge/ui_bridge.rs @@ -2,7 +2,11 @@ //! Session 保存托管对象的展示上下文;跨线程发布的是不可变 UiNode/Frame,而不是可以在窗口线程执行的 VM。 //! 刷新按 prepare → layout → paint → commit 推进。挂起工作必须恢复或显式取消;失败不能把未完成的几何和生命周期状态留作已发布结果。 //! -//! Tessera's native visual protocols over Aster's managed object graph. +//! 调用来源有两条: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; @@ -26,8 +30,11 @@ use crate::{ 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 名义类型查找。 @@ -84,6 +91,7 @@ pub const TYPE_NAMES: &[&str] = &[ "DisplayList", "PaintCommand", ]; +/// install_globals 暴露的类型/能力查询函数名称;与 bootstrap 安装到 ui 的入口对应。 pub const QUERY_NAMES: &[&str] = &[ "isInstance", "isSubtype", @@ -97,28 +105,35 @@ pub use tree::{UiKind, UiNode}; #[derive(Debug)] /// 整个 VM 的桥接会话。节点索引跨窗口共享,presentations 按窗口隔离派生状态;current 决定方法当前访问哪份展示上下文。 struct Session { - // Weak visual-object registry belongs to the VM, not a particular window. + // 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 身份,用于排除宿主记账读取造成的无关布局失效。 + /// 由刷新/依赖收集流程调用,返回所有窗口的宿主 desktop,排除宿主记账读取造成的无关失效。 fn host_desktops(&self) -> std::collections::HashSet { + // 只取值里的 desktop 而非窗口编号;依赖跟踪按托管 ObjectRef 判断读取归属。 self.presentations .values() .map(|view| view.desktop) @@ -128,7 +143,7 @@ impl Session { 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 @@ -137,13 +152,15 @@ impl std::ops::Deref for Session { } } 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)] @@ -215,39 +232,58 @@ enum Operation { } impl Operation { - /// 返回该操作显式参数数量范围;这里不计接收者、Type 首参和源子类字段。 + /// 被 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) } } @@ -255,9 +291,13 @@ impl Operation { #[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, @@ -268,37 +308,45 @@ struct Function { #[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 { - /// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。 + /// 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, @@ -306,13 +354,16 @@ impl NativeCallable for TypeMethod { )) } - /// 校验首参是预期名义类型实例,再剥离接收者,复用 Function 分派执行同一 Operation。 + /// 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, @@ -329,33 +380,41 @@ impl NativeCallable for TypeMethod { #[derive(Debug)] /// 为源代码调用原生类型构造器提供受检查的分配入口。 struct NativeFactory { + // 对 VM 可见的宽外壳:Type 首参 + 原生参数 + 源子类字段。 signature: Option, + // 仅描述原生选项;invoke 切分字段后再检查,不能用外壳的 Any 变参绕过类型契约。 options: Option, + // 决定实际原生分配器,Function 将据此创建 Block、相机或三维对象。 operation: Operation, + // 与该类型注册域一致的展示会话,跨 VM 的同名 Type 不能混用。 session: Shared, } impl NativeCallable for NativeFactory { - /// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。 + /// 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 @@ -373,6 +432,7 @@ impl NativeCallable for NativeFactory { "native constructor options", &values[..native_count], )?; + // requested_type 通知原生分配器附上真实子类 Type;owner 为空,因为这是构造而非实例方法。 let result = Function { signature: None, operation: self.operation, @@ -381,42 +441,50 @@ impl NativeCallable for NativeFactory { 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) } -/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。 +/// 供桥接实参/属性解码调用:只接受对象值,不把 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() @@ -425,18 +493,22 @@ fn integer(value: Option<&Value>) -> Result { _ => 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。 @@ -448,10 +520,12 @@ fn dimension(value: Option<&Value>) -> Result { } .filter(|v| v.is_finite() && (0.0..=MAX_DIMENSION).contains(v)) .ok_or(invalid("dimension exceeds limits"))?; + // MAX_DIMENSION 远小于 f32 的有限上界;通过上面的检查后再按原生几何格式降低精度。 Ok(value as f32) } -/// 为一个托管对象安装绑定到当前会话的原生方法,声明调用名与具体 Operation。 +/// 由 calls 的 state 工厂调用,为状态对象安装 set/increment 等捕获 owner 的原生方法。 +/// 普通 Block 实例方法走 TypeMethod 共享实现;这里处理的是需要固定 owner 的对象属性函数。 fn bind( cx: &mut NativeContext<'_>, owner: ObjectRef, @@ -459,10 +533,12 @@ fn bind( 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 { @@ -475,34 +551,40 @@ fn bind( // 绑定方法显式捕获 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)) } -/// 使用默认原生类型创建 Block;需要源子类身份的构造走 new_typed_block。 +/// 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) @@ -515,13 +597,14 @@ fn new_camera2d( Ok(camera) } -/// 创建原生 Block 并附加请求的 Endor 类型;先验证继承关系,再登记到会话节点索引。 +/// 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", @@ -536,28 +619,36 @@ fn new_typed_block( 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 { @@ -565,38 +656,46 @@ fn new_typed_block( } } if pages >= MAX_PAGES { + // 在登记新节点前失败,运行时仍负责释放此次调用中已经分配但未发布的对象。 return Err(VmError::ExecutionLimit("64 page budget")); } } + // 只登记弱身份,真实拥有关系由返回值、容器子项或页面注册表建立。 state.nodes.push(cx.store().downgrade(block)?); } Ok(block) } -/// 在当前 VM 中建立窗口、desktop 和独立 Presentation,实际 OS 窗口等命令发布后再创建。 +/// 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; @@ -605,30 +704,35 @@ fn create_window( Ok(window) } -/// 从页面登记信息取得所属窗口,保持 Page 的归属与原生资源身份一致。 +/// 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, @@ -639,30 +743,37 @@ fn remove_page( } Ok(()) } -/// 在指定窗口安排页面展示,并维护目标 Presentation 的活动/待发布状态。 +/// 页面 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 @@ -683,10 +794,12 @@ fn show_in( 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))?; @@ -694,7 +807,7 @@ fn show_in( 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) } @@ -702,26 +815,32 @@ fn show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result #[derive(Debug)] /// 接收源 present 结果并将其转换成宿主可展示内容的原生续接步骤。 struct PresentResult { + // ui.open 的续接需确保有 Page 并安排展示;ui.present 的续接只返回视觉对象。 open: bool, + // 源 present 返回后仍要回到原桥接域分配包装页/修改 pending,因此续接保存同一会话。 session: Shared, } impl NativeCallable for PresentResult { - /// 接收 present 委托返回的唯一视觉对象,作为后续展示处理的输入。 + /// VM 的原生续接调用只提供被委托 present 的一个返回值;参数数量在进入 invoke 前检查。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } - /// 验证 present 结果为 Block;open 模式必要时包装 Page 并展示,否则只返回转换得到的视觉对象。 + /// 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), @@ -731,18 +850,21 @@ impl NativeCallable for PresentResult { show(cx, page, &self.session)?; page } else { + // present 是值转换,不能附带把页面激活/打开的外部副作用。 *root }; Ok(NativeResult::Return(Value::Object(result))) } } -/// 返回已有 Block,或把其他值的可读表示包装成 TextBlock;Presentable 委托由外层调用分派负责。 +/// 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() @@ -751,6 +873,7 @@ fn present_value( .as_any() .is::() { + // 复用对象身份,不在此新增根或克隆业务 payload;调用边界管理返回值的拥有关系。 return Ok(*block); } // 非 Block 的标量/对象转可读文字,Presentable 方法委托在 calls.rs 的更外层处理,本函数不自己调用 present。 @@ -762,8 +885,10 @@ fn present_value( 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( @@ -783,16 +908,23 @@ fn present_value( #[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, // 布局同步返回的生命周期序列拥有根,暂留到安全提交边界执行/释放,取消时也必须清理。 @@ -803,18 +935,23 @@ struct LayoutContext { /// 挂起布局的拥有者。续体与上下文必须一起保留并最终恢复或取消。 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, } -/// 解析对象的方法并在同一 VM 中调用,使源覆盖与原生成员共用方法查找规则。 +/// dispatch_tab/页面事件等桥接控制路径调用,通过 VM 正常查找绑定方法,保留源覆盖与继承语义。 fn invoke_member( vm: &mut Vm, owner: ObjectRef, @@ -824,7 +961,9 @@ fn invoke_member( ) -> 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, @@ -835,29 +974,36 @@ fn invoke_member( }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error)), + // 普通属性没有函数身份,不能把它当无参 getter 或忽略调用。 _ => Err(anyhow::anyhow!("object method is not callable")), }; vm.release_value(bound)?; result } -/// 将托管页面注册表投影成稳定标签 ID、标题与活动状态,不把托管引用交给原生侧栏。 +/// 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) @@ -874,38 +1020,47 @@ fn project_tabs( .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 { - /// Detach all occurrences without running layout/paint. All windows are - /// already closed before source unmount hooks run, so they cannot remount. - /// 按窗口和展示生命周期执行有界退出清理,允许调用方根据故障状态决定是否执行回调。 + /// 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,不能再次执行用户代码。 @@ -918,11 +1073,14 @@ impl UiBridge { &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)?; @@ -934,12 +1092,15 @@ impl UiBridge { } 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, @@ -954,47 +1115,55 @@ impl UiBridge { 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 } - /// 查询任务系统是否仍有可推进的协作工作,供 worker 选择等待策略。 + /// endor_host/worker 查询是否仍有协作任务,以决定继续推进还是等待外部事件;不执行任务本身。 pub fn has_cooperative_tasks(&self) -> bool { self.tasks.has_cooperative_work() } - /// 把当前 VM 交给任务调度器推进,限制一轮交付数量。 + /// 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, @@ -1002,9 +1171,10 @@ impl UiBridge { }); Ok(ids) } - /// 判断窗口是否仍登记且没有进入关闭状态。 + /// runtime 在接受窗口输入前查询;锁异常、缺失或 closing 都视为不再可接受,不执行 VM 操作。 pub fn accepts_window(&self, id: u64) -> bool { self.session.lock().is_ok_and(|state| { + // 控制线程只查询原生会话状态;页面对象身份与版本还有单独的后续校验。 state .presentations .get(&id) @@ -1012,31 +1182,38 @@ impl UiBridge { }) } - /// 切换当前 Presentation 上下文;不会创建新窗口或复制 VM。 + /// 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(()) } - /// 从托管展示域撤销窗口,退役 Mount 并清理该窗口的私有对象关系。 + /// 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)?; @@ -1046,6 +1223,7 @@ impl UiBridge { // 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); @@ -1053,8 +1231,9 @@ impl UiBridge { Ok(()) } - /// 收尾已经关闭的展示资源,避免它们参与下一次窗口遍历。 + /// endor_host 在正常全窗口刷新收尾调用,移除已 closing 的副窗口并保留其待发布原生命令。 pub fn finish_closed(&self, vm: &mut Vm) -> Result<()> { + // 正常收尾与故障 retire_window 不同:本锁内先保存命令,再删除托管/原生资源表项。 let mut state = self .session .lock() @@ -1066,9 +1245,12 @@ impl UiBridge { .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 @@ -1079,8 +1261,9 @@ impl UiBridge { state.current = crate::window::MAIN_WINDOW; Ok(()) } - /// 从各托管窗口收集待发布命令;原生执行时还需校验 session。 + /// 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() @@ -1088,14 +1271,16 @@ impl UiBridge { // 先取已移除窗口的最后命令,再合并仍活动窗口 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() @@ -1103,18 +1288,23 @@ impl UiBridge { || !(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()))?; @@ -1124,6 +1314,7 @@ impl UiBridge { vm.release(input)?; result?; } else { + // 未知尺寸用 nil 明确表示,不把旧尺寸残留在源 ui.viewport 可见输入里。 vm.set_attr(state.desktop, "viewport", Value::Nil)?; } state.layout.viewport = viewport; @@ -1134,10 +1325,10 @@ impl UiBridge { Ok(true) } - /// 将 ui 入口与兼容名称安装到本次程序的全局字典,保持所有调用处在同一个 VM 域。 + /// 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))?; - // Compatibility spelling used by the original module examples. + // 旧模块示例把 Block 写成 View;别名引用同一 Type,不建立另一个互不相容的同名类型。 vm.dict_set( globals, Value::Text("View".into()), @@ -1145,13 +1336,15 @@ impl UiBridge { )?; // 全局名引用 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() @@ -1160,12 +1353,14 @@ impl UiBridge { .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 注册顺序仍对应标签弱身份表,防止用陈旧列表下标重排。 @@ -1174,15 +1369,19 @@ impl UiBridge { (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() @@ -1191,29 +1390,36 @@ impl UiBridge { .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 几何下标。 @@ -1223,6 +1429,7 @@ impl UiBridge { .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() @@ -1231,28 +1438,35 @@ impl UiBridge { .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() @@ -1261,7 +1475,9 @@ impl UiBridge { }; // 源 moveToWindow 可覆盖并拒绝/改投其他窗口;只有确认实际归属仍为预期目标才继续重排与激活。 if destination == Some(target.window) { + // 用回调之后的真实顺序定位页面,避免回调中增删标签使旧下标错指其他页面。 let pages = crate::window::pages(vm.object_store(), window)?; + // 找不到页面说明迁移过程中它已被关闭/移走;不能继续以默认 0 号位置重排。 let from = pages .iter() .position(|candidate| *candidate == page) @@ -1270,11 +1486,14 @@ impl UiBridge { 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, @@ -1283,19 +1502,23 @@ impl UiBridge { 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 @@ -1303,6 +1526,7 @@ impl UiBridge { .payload::(*window, "Window") .is_ok() { + // moveTo 也经绑定方法调用,坐标转为语言数字;返回值和外层 detach 返回值分别释放。 let result = invoke_member( vm, *window, @@ -1317,13 +1541,15 @@ impl UiBridge { } 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))?; @@ -1334,17 +1560,22 @@ impl UiBridge { .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, @@ -1362,36 +1593,44 @@ impl UiBridge { } // 内置工具打开走同一 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"); } ( @@ -1407,11 +1646,15 @@ impl UiBridge { ) }; 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"))?; @@ -1425,13 +1668,17 @@ impl UiBridge { { 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, @@ -1450,6 +1697,7 @@ impl UiBridge { vm.release_value(result?)?; Ok(()) })(); + // 滚动失败也清理临时根;只有释放后才把保存的 result 交回运行时。 vm.release(mount)?; return result; } @@ -1459,24 +1707,30 @@ impl UiBridge { .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 { @@ -1486,6 +1740,7 @@ impl UiBridge { }, vec![mount, desktop], ))?; + // 先暂存结果,释放临时 callable 根后再传播错误,保持失败路径也无额外根残留。 let result = vm.call_with_limits( function, &[], @@ -1499,15 +1754,19 @@ impl UiBridge { 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 { @@ -1519,6 +1778,7 @@ impl UiBridge { }, vec![data.block, mount, desktop], ))?; + // 一次有界的 VM 调用完成指针分派;先释放 callable 根,再传播执行错误。 let result = vm.call_with_limits( f, &[], @@ -1538,12 +1798,15 @@ impl UiBridge { 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, @@ -1562,49 +1825,60 @@ impl UiBridge { vm.release(mount)?; result } - /// 整理不再有效的弱索引与展示根,并配合 VM 回收,避免派生缓存延长业务对象生命周期。 + /// 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)) @@ -1614,8 +1888,10 @@ impl UiBridge { // 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( @@ -1631,33 +1907,39 @@ impl UiBridge { .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(()) } - /// 排空一次 VM 变化记录并通知各窗口,提取活动页面快照,判断本次是否需要布局与发布。 + /// 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() @@ -1667,8 +1949,10 @@ impl UiBridge { // 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()?; @@ -1678,11 +1962,14 @@ impl UiBridge { (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 @@ -1693,7 +1980,9 @@ impl UiBridge { || state.force_frame || tabs != state.last_tabs; if changed { + // 只在确有新帧工作时记录快照诊断,避免空闲轮询覆盖最后一次有效失效原因。 if !same { + // 根身份变化才归因于 snapshot;仅标签/焦点变更不伪造快照失效记录。 state.last_invalid_dependency = format!("snapshot {}", state.render_cache.last_invalidation); } @@ -1703,29 +1992,36 @@ impl UiBridge { // 双层 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)?; @@ -1741,6 +2037,7 @@ impl UiBridge { // 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, @@ -1756,12 +2053,13 @@ impl UiBridge { 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, @@ -1769,6 +2067,7 @@ impl UiBridge { frame: crate::document::Frame, ) -> Result { let (desktop, mounts) = { + // 取绘制顺序与弱身份只读 Session,source_paint::start 在锁外执行 VM 协议。 let state = self .session .lock() @@ -1778,6 +2077,7 @@ impl UiBridge { .mounts .iter() .map(|mount| { + // 以 frame.mounts 的顺序查活动弱身份;缺项代表布局/挂载不一致,必须报错而非漏画。 state .mounts .active @@ -1789,38 +2089,44 @@ impl UiBridge { .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) } - /// 恢复同一次源绘制调用,把完成的 Draft 交回发布流程。 + /// 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; @@ -1829,7 +2135,7 @@ impl UiBridge { Ok(progress) } - /// 取消尚未完成的源绘制,释放其续体和未发布上下文。 + /// endor_host 的 resize/输入替换/退役路径调用,取消所属 VM 的 paint 续体并强制该窗口重新刷新。 pub fn cancel_paint_frame( &self, vm: &mut Vm, @@ -1838,6 +2144,7 @@ impl UiBridge { ) -> Result<()> { // 先尝试取消 VM 续体,再尝试选原窗口并强制下一帧;前一操作失败也执行必要的宿主失效准备。 let cancelled = work.cancel(vm); + // 这一步即使 cancelled 是 Err 也先尝试;否则失败的取消会留下“旧帧看似仍有效”的缓存状态。 let invalidated = self.select_window(window).and_then(|()| { self.session .lock() @@ -1845,22 +2152,25 @@ impl UiBridge { .force_frame = true; Ok(()) }); + // 所有必要清理尝试完成后再按顺序传播错误,不让前一个 ? 跳过后一个失效操作。 cancelled?; invalidated } - /// 为目标快照建立 Mount、检查点和布局上下文,再启动可挂起的只读布局求解。 + /// 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"))? @@ -1876,11 +2186,13 @@ impl UiBridge { .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 @@ -1893,9 +2205,11 @@ impl UiBridge { ..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 @@ -1909,6 +2223,7 @@ impl UiBridge { state.nodes.clone(), ) }; + // 标签投影会更新稳定 ID 映射,因此短暂可变借用 Session,而非沿用上面的只读锁。 let tabs = { let mut state = self .session @@ -1918,6 +2233,7 @@ impl UiBridge { }; // 记录真实原生页面视口供页面相机 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); { @@ -1928,8 +2244,10 @@ impl UiBridge { // 按全会话高水位分配结构出现 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() @@ -1937,6 +2255,7 @@ impl UiBridge { 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 @@ -1944,12 +2263,14 @@ impl UiBridge { .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()); } @@ -1967,27 +2288,30 @@ impl UiBridge { 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) } - /// 记录本次刷新主动让出 worker 的次数,用于诊断分片执行。 + /// 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, @@ -1997,6 +2321,7 @@ impl UiBridge { .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 @@ -2007,8 +2332,9 @@ impl UiBridge { Ok(()) } - /// 撤销未完成布局所保留的窗口、挂载与生命周期上下文。 + /// cancel_layout_frame/布局出错路径调用,恢复所属窗口的检查点并释放尚未执行的生命周期驱动。 fn discard_layout_context(&self, vm: &mut Vm, context: LayoutContext) -> Result<()> { + // 消费 context 后只提取恢复所需的资源,树/标签等纯 Rust 值自然释放,不执行用户回调。 let LayoutContext { surface: _, desktop, @@ -2020,21 +2346,26 @@ impl UiBridge { // 恢复必须在原窗口;若窗口或 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?; @@ -2042,8 +2373,9 @@ impl UiBridge { 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); @@ -2052,31 +2384,34 @@ impl UiBridge { 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) } - /// 接收布局的 Pending/Complete 结果;保留上下文或执行最终投影和提交。 + /// begin_layout_frame/resume_layout_frame 共用入口,把源布局结果转成桥接进度,集中处理恢复和清理。 fn accept_layout_progress( &self, vm: &mut Vm, @@ -2095,19 +2430,21 @@ impl UiBridge { .finish_layout_frame(vm, context, draft) .map(|frame| LayoutProgress::Complete(Box::new(frame))), Err(error) => { + // 源布局失败不能发布 draft;先撤销准备态关系并释放检查点/钩子,再传播执行错误。 self.discard_layout_context(vm, context)?; Err(error) } } } - /// 依次投影滚动/视口、补页面元数据、保存读取版本、提交 Mount,并处理延后的生命周期调用。 + /// 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, @@ -2118,12 +2455,14 @@ impl UiBridge { 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)?; { @@ -2141,16 +2480,19 @@ impl UiBridge { 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() @@ -2162,7 +2504,9 @@ impl UiBridge { // 源布局实际业务读取版本在生命周期 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, @@ -2172,6 +2516,7 @@ impl UiBridge { .focus .filter(|id| frame.mounts.iter().any(|m| m.id == *id && m.interactive)); { + // 生命周期回调前先写回本轮计算结果;回调产生的新变化随后由运行时刷新循环再观察。 let mut state = self .session .lock() @@ -2185,6 +2530,7 @@ impl UiBridge { // 几何先提交到桥接状态,再执行准备阶段延后的挂载 hooks;其变化由全窗口刷新循环再收敛。 self.commit_frame(vm, Some(root), &frame)?; if let Some(hooks) = hooks { + // 准备阶段暂存的驱动现在才执行;无论返回值业务上是否使用都需释放其 VM 根。 let value = vm .call_with_limits( hooks, @@ -2200,15 +2546,18 @@ impl UiBridge { 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 @@ -2224,13 +2573,14 @@ impl UiBridge { } } -/// 从会话的类型注册表取得指定类型,再给原生分配的对象附加托管类型身份。 +/// 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"))? @@ -2238,18 +2588,19 @@ fn attach_named_type( attach_desktop_type(cx, object, name, desktop) } -/// 按给定 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) } -/// 读取 desktop 的托管类型注册表;缺失或类型错误时返回 VM 错误。 +/// 签名构造、能力检查和各分配器调用,查询 bootstrap 注册的精确 Type 身份,不按名字新建类型。 pub(crate) fn desktop_type( store: &endor_core::ObjectStore, name: &str, @@ -2260,10 +2611,12 @@ pub(crate) fn desktop_type( 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) } diff --git a/src/bridge/ui_bridge/bootstrap.rs b/src/bridge/ui_bridge/bootstrap.rs index bd4d557..7b11576 100644 --- a/src/bridge/ui_bridge/bootstrap.rs +++ b/src/bridge/ui_bridge/bootstrap.rs @@ -1,42 +1,63 @@ //! 安装一次 VM 域需要的宿主类型、对象和方法。类型注册表是源代码调用原生能力的入口。 //! 先建立类型与桌面基础设施,再绑定构造器、视觉协议和开发工具工厂;返回的根负责保持这些对象存活。 //! -//! Allocate and connect the managed desktop, type tables and host globals. +//! 调用链:`endor_host::compile` / `ReplSession::new` 创建候选 VM, +//! 随后调用本模块的 `install`;运行时再通过 install_globals 注入源程序使用的 UI 值。 +//! 本文件只建立宿主环境。源程序稍后调用构造器或实例方法时,才会进入 `calls::Function::invoke`。 +//! 安装返回的 UiBridge 由运行时保存;传入的 roots 由候选运行时持有,并在失败或退役时统一释放。 use super::*; -/// 把本模块的类型、方法或能力注册到所属运行时上下文。 +/// 为一次候选程序/REPL 会话安装 Tessera 宿主环境,供运行时后续注入全局值和执行源程序。 +/// +/// 类型要先创建,签名才能引用它们;因此先分配没有签名的函数,再原地补齐 payload。 +/// 所有 `?` 都把安装失败交还候选运行时,不会在这里把半成品替换成活跃程序。 +/// `roots` 保存明确的 VM 拥有根;Rust 的 ObjectRef 数值和 Arc> 均不能替代它们。 pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { // 先建立 desktop/ui/tabs 并加入显式 roots,后面初始化失败时由上层运行时统一释放,避免依赖 Rust 句柄保活。 + // desktop 是此桥接域的类型注册表和窗口资源入口;它不是操作系统桌面或原生窗口。 let desktop = vm.new_object()?; roots.push(desktop); + // 安装时尚无窗口尺寸;运行时收到 viewport 后再填充,避免在 VM 引导期间依赖 GUI 状态。 vm.set_attr(desktop, "viewport", Value::Nil)?; + // tabs 是主窗口的托管页面注册表,Presentation 和 window::install 会使用同一字典身份。 let tabs = vm.new_dict()?; roots.push(tabs); vm.set_attr(desktop, "tabs", Value::Object(tabs))?; + // ui 是对源代码公开的命名空间;desktop 保存内部记账,二者用途不能混同。 let ui = vm.new_object()?; roots.push(ui); // 原生 Session 借用托管身份,主窗口先拥有独立 Presentation;业务对象仍全部位于传入的单一 VM。 let session = Arc::new(Mutex::new(Session { + // 节点弱索引用来检查数量与回收失效记录,不应让已关闭的业务对象继续存活。 nodes: Vec::new(), + // 先登记主窗口上下文;后续窗口通过 create_window 建立各自的 Presentation。 presentations: std::collections::BTreeMap::from([( crate::window::MAIN_WINDOW, crate::presentation::Presentation::new(desktop, tabs), )]), + // Session 的 Deref 按 current 选择布局/挂载状态;初始所有操作都落在主窗口。 current: crate::window::MAIN_WINDOW, + // master 始终指向主 desktop,副窗口仍借用同一 VM 的类型和资源域。 master: desktop, + // 分配器从现有最高窗口 ID 继续增长;Mount 则尚无任何已分配出现位置。 next_window: crate::window::MAIN_WINDOW, next_mount: 0, + // 关闭后尚未交给 GUI 的原生命令暂存在这里,不能随 Presentation 删除而丢失。 retired_commands: Vec::new(), + // 累计诊断计数由刷新流程更新,供 ui.stats/开发工具观察本会话的实际工作量。 refresh_yields: 0, refresh_restarts: 0, layout_cancels: 0, paint_cancels: 0, paint_slices: 0, binding_evaluations: 0, + // 清理阶段会置位,create_window 等操作据此拒绝在退出期间重新创建资源。 exiting: false, })); // 先分配函数占位再补签名,因为签名会引用尚未建立的名义类型;保存列表用于第二阶段替换 payload。 + // 保存 ui.name 的函数身份及 Operation,等全部类型注册后再为这些函数补齐精确签名。 let mut public_functions = Vec::new(); + // 另外记录 TypeConstructor 所用的工厂;它们多接收一个 Type 首参,不能套用自由函数签名。 let mut factories = Vec::new(); for (name, operation) in [ ("implement", Operation::Implement), @@ -63,6 +84,8 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { ("viewport", Operation::Viewport), ("window", Operation::Window), ] { + // 每个名字得到自己的 VM callable,实际操作由 Function 根据 operation 分派。 + // desktop 放在 captures 中,确保函数仍可达时它所依赖的类型注册表也能被 GC 追踪。 let function = vm.allocate(ContextNativeFunction::new( name, Function { @@ -75,6 +98,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { vec![desktop], ))?; roots.push(function); + // 属性供源程序的 ui.text/ui.present 等查询;列表供本函数第二阶段补签名,职责不同。 vm.set_attr(ui, name, Value::Object(function))?; public_functions.push((name, function, operation)); } @@ -83,6 +107,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { roots.extend([root_types.object, root_types.type_type]); vm.set_attr(ui, "Object", Value::Object(root_types.object))?; vm.set_attr(ui, "Type", Value::Object(root_types.type_type))?; + // definitions 汇集全部类型,稍后写入 desktop.types,供 desktop_type 按名称查精确身份。 let mut definitions = vec![ ("Object", root_types.object), ("Type", root_types.type_type), @@ -127,10 +152,13 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { ("Task", "Object", None), ("ObjectList", "Object", Some("object-list")), ] { + // 依照表中基类名取得当前 VM 的真实类型,不能用另一个 VM 中的同名 Type 代替。 let base = object(Some(&vm.get_attr(ui, base_name)?))?; + // 根据构造规则选择专用工厂、通用工厂或抽象类型;这里只注册,不分配业务实例。 let constructor = match constructor_name { // ObjectList 与 Event 有特殊的“类型首参+子类字段”构造规则,单独原生适配;其他类型共用 NativeFactory。 Some("object-list") => { + // ObjectList 构造要保持托管元素身份与字段规则,由 object_list::Constructor 处理。 let factory = vm.allocate(ContextNativeFunction::new( "object-list", crate::object_list::Constructor { @@ -140,17 +168,20 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { vec![desktop], ))?; roots.push(factory); + // 工厂本身也是语言中的 Function 实例,挂类型后才能通过名义函数检查。 let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; vm.attach_type(factory, function_type)?; TypeConstructor::NativeTyped(factory) } Some("event") => { + // Event 另有必填事件名;其专用 Constructor 在真正调用时解析事件参数和子类字段。 let factory = vm.allocate(ContextNativeFunction::new( "event", crate::event_dispatch::Constructor { signature: None }, vec![], ))?; roots.push(factory); + // 与列表工厂保持相同函数类型体系;无 desktop 捕获的分配器不额外引入桌面强边。 let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; vm.attach_type(factory, function_type)?; TypeConstructor::NativeTyped(factory) @@ -176,6 +207,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { .map(Operation::SceneObject) .ok_or(invalid("unknown native type allocator"))?, }; + // 通用 NativeFactory 在调用时区分原生参数与源子类字段,并保留 requested Type。 let factory = vm.allocate(ContextNativeFunction::new( constructor_name, NativeFactory { @@ -187,10 +219,12 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { vec![desktop], ))?; roots.push(factory); + // 这个函数属于构造器元数据,不直接放在 ui.name;但仍需标为 Function 实例。 let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; vm.attach_type(factory, function_type)?; TypeConstructor::NativeTyped(factory) } + // 没有公开分配规则的类型仍可用于类型检查/协议;直接把它当构造器调用会由 VM 拒绝。 None => TypeConstructor::Abstract, }; // 记住实际工厂身份,类型构建完成后原地补签名,避免替换类型内指向的构造函数对象。 @@ -198,6 +232,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { { factories.push((tag, factory)); } + // 生成新 Type 后同时公开到 ui,并加入待提交注册表,两个入口必须指向同一对象。 let definition = vm.new_type(name, base, constructor)?; roots.push(definition); vm.set_attr(ui, name, Value::Object(definition))?; @@ -217,6 +252,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { ("PaintCommand", vec![]), ("CameraGesture", vec![]), ] { + // 表里的字段名转成 VM 可检查的字段声明;map 的每次调用只描述一个字段,不执行布局。 let fields = fields .into_iter() .map(|name| endor_core::ClassField { @@ -225,11 +261,13 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { type_name: if matches!(name, "children" | "commands") { "Object" } else { + // 余下字段都是测量/几何数值,让 VM 在实例化时先拒绝非数字实参。 "Number" } .into(), }) .collect(); + // 这些协议值只携带源字段,使用普通 class 分配;不需要 Block 的原生 payload。 let definition = vm.new_class( name, root_types.object, @@ -243,14 +281,17 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { // The desktop owns the registry through ordinary traced attributes. The // Rust session only borrows handles and adds no hidden lifetime roots. // 桌面通过普通属性拥有类型字典;后续 nominal 查询必须使用同一 desktop 中注册的精确类型身份。 + // 类型字典由 desktop 属性托管持有;后续桥接调用可从其查找 Block、Event 等精确类型。 let types = vm.new_dict()?; roots.push(types); for (name, definition) in definitions { + // 字符串键是宿主查询约定,值才是参与 is_instance/is_subtype 的真实名义身份。 vm.dict_set(types, Value::Text(name.into()), Value::Object(definition))?; } vm.set_attr(desktop, "types", Value::Object(types))?; vm.attach_type(desktop, root_types.object)?; vm.attach_type(ui, root_types.object)?; + // 安装后续编译函数的默认类型,也用于下方所有方法 wrapper 的 attach_type。 let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; // 让后续 VM 创建的函数使用宿主 Function 名义类型,并打开源类型检查,再安装完整本机签名。 vm.set_function_type(function_type)?; @@ -273,6 +314,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { )?; } for (name, function) in factories { + // 恢复第一阶段记录的分配器类别;Event/ObjectList 不走 NativeFactory,需要单独补外壳。 let operation = match name { "text" => Some(Operation::Text), "rect" => Some(Operation::Rect), @@ -288,6 +330,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { "button" => Some(Operation::Button), "page" => Some(Operation::Page), "window-create" => Some(Operation::NewWindow), + // None 在本轮只表示专用构造器,不表示前一阶段的 Abstract 类型。 "event" | "object-list" => None, name => Some( crate::scene3d_model::Constructor::from_function(name) @@ -296,6 +339,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { ), }; if let Some(operation) = operation { + // options 只描述原生构造选项,调用时由 NativeFactory 在切掉 Type 和子类字段后校验。 let options = operation_signature(vm.object_store(), desktop, operation)?; // 外层 NativeTyped 工厂接收 Type 首参和任意候选尾参,允许子类字段;NativeFactory 用 options 再核对基构造参数。 let envelope = CallSignature { @@ -305,6 +349,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { returns: options.returns.clone(), effect: options.effect, }; + // 初始强边是注册表;随后追加签名引用的类型,供 ContextNativeFunction::trace 使用。 let mut captures = vec![desktop]; // 参数和返回注解中的名义类型也是托管引用,必须纳入 captures,不能只把它们存进 Rust 签名结构。 captures.extend(options.references()); @@ -323,16 +368,20 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { )?; } else { // 特殊构造器保留宽变参外壳,再由对应 Constructor 按类型字段数拆实参,返回准确 Event/ObjectList 名义类型。 + // 为返回值选取专用名义类型,避免宽变参外壳同时放宽了返回值检查。 let kind = if name == "event" { "Event" } else { "ObjectList" }; + // Event 的 Type 之后还有必填字符串事件名;ObjectList 只有 Type 为必填,其余由实现解释。 let signature = CallSignature { arity: endor_core::CallArity::range( + // 这里包含 Type 首参;源子类字段由构造器验证,所以最大个数保留开放。 if name == "event" { 2 } else { 1 }, usize::MAX, ), + // 固定前缀与 required 数量保持一致,额外实参由 variadic 和字段验证共同约束。 parameters: if name == "event" { vec![AnnotationType::Type.into(), AnnotationType::String.into()] } else { @@ -344,6 +393,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { effect: CallEffect::ReadOnly, }; if name == "event" { + // 原地更新 Event 工厂,TypeConstructor 中已保存的对象引用继续有效。 vm.replace_payload( function, ContextNativeFunction::new( @@ -355,6 +405,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { ), )?; } else { + // 这里只有 ObjectList;保留 desktop 捕获,以便专用分配器给结果附加列表类型。 vm.replace_payload( function, ContextNativeFunction::new( @@ -369,6 +420,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { } } } + // 对这批公开入口补 Function 类型身份;不重新分配 callable,ui 属性仍指向原对象。 for name in [ "open", "text", @@ -390,6 +442,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { "implements", "supports", ] { + // 从 ui 取回前面已补完签名的函数;名称表与安装表不一致会在这里报错而非静默漏装。 let function = object(Some(&vm.get_attr(ui, name)?))?; vm.attach_type(function, function_type)?; } @@ -437,7 +490,9 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { ("Page", "back", Operation::Back), ("Page", "forward", Operation::Forward), ] { + // expected 同时用于方法所属类型、receiver 校验和签名中的名义参数身份。 let expected = object(Some(&vm.get_attr(ui, type_name)?))?; + // VM 的方法分派负责绑定 receiver;TypeMethod 再验证它并转交 Function 执行业务操作。 let method = vm.allocate(ContextNativeFunction::new( name, TypeMethod { @@ -451,20 +506,26 @@ pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { ))?; roots.push(method); vm.attach_type(method, function_type)?; + // 写到类型方法表而非每个实例的属性,实例与源子类可共享同一个原生实现。 vm.define_method(expected, name, method)?; } // 类型与基础方法就绪后再装窗口、默认视觉、事件方法和能力契约,顺序满足各模块查类型/方法的前提。 crate::window::install(vm, roots, desktop, tabs)?; + // 默认 measure/arrange/paint 安装到 Block 体系;源代码之后可按协议覆盖它们。 let block_type = object(Some(&vm.get_attr(ui, "Block")?))?; crate::native_visual::install(vm, roots, desktop, block_type, function_type)?; + // 事件方法需要识别 Event 实例;订阅返回值则使用 Subscription 身份管理取消连接。 let event_type = object(Some(&vm.get_attr(ui, "Event")?))?; + // token_type 传给事件安装器,不能拿 Event 类型代替,否则取消令牌会违反返回契约。 let token_type = object(Some(&vm.get_attr(ui, "Subscription")?))?; crate::event_dispatch::install(vm, roots, event_type, token_type, function_type)?; crate::protocols::install(vm, roots, desktop, ui)?; // 每个桥接域配套自己的任务 Scheduler,再接工具入口及托管起始页工厂,最后返回可使用的 UiBridge。 + // Scheduler 随 UiBridge 交还运行时;它还未开始执行用户任务,只完成 ui 异步 API 注册。 let tasks = crate::tasks::install(vm, roots, ui, desktop)?; crate::bridge::graphics::install(vm, roots, ui, desktop)?; developer_pages::install(vm, ui, &session)?; + // 将语言命名空间、展示会话与任务调度打包,运行时随后通过同一 bridge 推进刷新与回调。 let bridge = UiBridge { ui, session, tasks }; developer_pages::install_start_page(vm, &bridge)?; Ok(bridge) diff --git a/src/bridge/ui_bridge/calls.rs b/src/bridge/ui_bridge/calls.rs index 59ad18c..15c80a1 100644 --- a/src/bridge/ui_bridge/calls.rs +++ b/src/bridge/ui_bridge/calls.rs @@ -1,46 +1,57 @@ //! 把 Endor 方法调用分派到对应宿主操作。这里处理构造、树修改、绑定、状态与页面能力。 //! 先检查参数和只读阶段限制,再修改托管状态;对象引用的生命周期仍交给 NativeContext 和 Aster 管理。 //! -//! Native Block operations executed on the owning VM worker. +//! 调用来源:VM 直接执行 bootstrap 注册的 ui 自由函数,或由 TypeMethod/NativeFactory 剥离接收者/Type 后转交。 +//! Function.owner 保存绑定对象,requested_type 保存原生子类构造请求;本文件 args 只包含显式原生参数。 +//! 简单分支同步返回 NativeResult::Return;需要执行源协议/事件处理器的分支返回 Invoke,让 VM 接着推进调用栈。 +//! NativeContext 追踪本次分配和 upgrade 的临时根,返回/委托时提升输出引用,其余在域退出时释放。 use super::*; impl NativeCallable for Function { - /// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。 + /// VM 在调用边界查询 bootstrap/bind 写入的签名;由外层已校验的临时转交 Function 可返回 None。 fn signature(&self) -> Option { self.signature.clone() } - /// 接收已经剥离 receiver 的显式参数,数量沿用 Operation 的范围。 + /// VM 查询自由函数的实参数量,或供转交适配器使用;这里不再给 receiver/Type 首参加一。 fn arity(&self) -> Option { Some(self.operation.arity()) } - /// 统一校验数量与只读限制,再按 Operation 分派构造、树修改、页面、绑定、状态与能力调用,返回托管结果或 VM 续调用。 + /// VM 或外层原生调用适配器进入的实际业务分派,返回借用结果或委托;不负责 GUI 帧的原子发布。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { use Operation::*; + // 外层 TypeMethod/NativeFactory 会直接调用本方法,因此这里仍需防御性检查参数数量。 let arity = self.operation.arity(); + // 显式区间不包含已剥离的 receiver/Type,后面 args[0] 等索引依赖这次验证。 let (minimum, maximum) = (arity.required, arity.maximum); // Function 接收已剥离 receiver 的显式参数;先校验数量,后面索引 args 才安全。 if !(minimum..=maximum).contains(&args.len()) { + // 在任何分配或模型修改之前拒绝数量不符,避免越界或带半次副作用的失败。 return Err(invalid("incorrect UI method argument count")); } if matches!(self.operation, Camera2D) { + // 独立相机不是 Block,不进入下方的视觉 Data.kind 构造;requested_type 保留源子类。 let camera = new_camera2d(cx, &self.session, self.requested_type, args)?; return Ok(NativeResult::Return(Value::Object(camera))); } // 3D 构造与普通 Block 分开,requested_type 保留源子类名义身份,否则从本 desktop 取默认类型。 if let SceneObject(kind) = self.operation { + // 只复制当前类型域身份,放锁后进入三维分配器;该分配器校验 kind 的实参与子类约束。 let desktop = self .session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; + // 类型构造保留请求身份,自由工厂选择当前 desktop 中 kind.name 对应的默认类型。 let definition = self.requested_type .unwrap_or(desktop_type(cx.store(), kind.name(), desktop)?); + // model::scene3d 建立托管几何/场景对象,此刻不投影、不光栅化也不建立 OS 资源。 let object = crate::scene3d_model::allocate(cx, desktop, definition, kind, args)?; return Ok(NativeResult::Return(Value::Object(object))); } if matches!(self.operation, CameraControl) { + // receiver 是控制器,两个显式参数是相机与手势;实际状态更新由 scene3d_model::control 解释。 crate::scene3d_model::control( cx, self.owner @@ -51,6 +62,7 @@ impl NativeCallable for Function { return Ok(NativeResult::Return(Value::Nil)); } if matches!(self.operation, Translate3D | Rotate3D) { + // 三分量实参共用转换入口,末尾布尔值区分旋转/位移;对象缺少 owner 时直接报错。 crate::scene3d_model::transform( cx, self.owner @@ -62,9 +74,12 @@ impl NativeCallable for Function { } // implement 修改显式能力关系;implements 查类型,supports 查对象实际能力,不靠同名字段鸭子类型推断。 if matches!(self.operation, Implement | Implements | Supports) { + // 第二实参必须是托管 Trait,注册和查询共用身份,但标量 supports 仍需先验证 Trait 本身。 let capability = object(args.get(1))?; + // 此分支计算语言返回值,随后统一包 Return;只有 Implement 真正修改类型关系。 let value = match self.operation { Implement => { + // 第一参数是 Type,VM 的 implement_trait 检查能力契约并建立托管引用关系。 cx.implement_trait(object(args.first())?, capability)?; Value::Nil } @@ -73,24 +88,30 @@ impl NativeCallable for Function { .implements_trait(object(args.first())?, capability)?, ), Supports => { - // Validate the contract even for immediate scalar values. + // 即使第一参数是标量也先验证第二参数真为 Trait;不能用 false 掩盖错误能力对象。 cx.store() .payload::(capability, "Trait")?; Value::Bool(match args[0] { + // 对象按实际能力契约查询,标量没有这里的宿主对象能力而返回 false。 Value::Object(value) => cx.store().supports(value, capability)?, _ => false, }) } + // 外层 matches 已限定三种操作,其他枚举值到这里表示分派实现内部不一致。 _ => unreachable!(), }; return Ok(NativeResult::Return(value)); } if matches!(self.operation, IsInstance | IsSubtype) { + // 当前实现要求两者都解码成对象:签名中的 Any 不等于可在这里传 nil 后获得 false。 let candidate = object(args.first())?; + // expected 必须属于此 VM 的真实类型体系;关系查询不按类型名字字符串比较。 let expected = object(args.get(1))?; + // 实例关系检查对象的类型,子类型关系检查两个 Type 的继承关系,不能混为同一查询。 let value = if matches!(self.operation, IsInstance) { cx.store().is_instance(candidate, expected)? } else { + // isSubtype 用于类型间关系,不把 candidate 当实例读取其 type_of。 cx.store().is_subtype(candidate, expected)? }; return Ok(NativeResult::Return(Value::Bool(value))); @@ -98,48 +119,61 @@ impl NativeCallable for Function { // 显式失效属于模型变更,只读阶段拒绝;paint/layout/content 精度不同,后续分别清理对应缓存。 if matches!(self.operation, Invalidate) { if cx.store().is_read_only() { + // measure/paint 等只读域不能靠显式失效反复制造下一轮更新反馈。 return Err(invalid( "invalidation is forbidden during read-only rendering", )); } + // 仅省略参数才默认 layout,错误类型不回退;transpose 保留显式解析失败。 let reason = args .first() .map(|value| text(Some(value))) .transpose()? .unwrap_or_else(|| "layout".into()); if !matches!(reason.as_str(), "paint" | "layout" | "content") { + // 精度标签必须已知,否则不能可靠决定清哪些缓存和依赖集合。 return Err(invalid( "invalidation reason must be paint, layout or content", )); } + // TypeMethod 已把 receiver 移入 owner,没有绑定对象的自由调用不能执行此方法。 let owner = self.owner.ok_or(invalid("invalidation requires a Block"))?; + // 以解析后的共享 Data 身份归并别名,不能只让调用者这一个包装对象失效。 let (data, _) = block::resolve(cx.store(), owner)?; + // 锁内只遍历弱节点并更新派生失效状态,不在这里调用用户布局/绘制方法。 let mut session = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // 去重后的 Block 身份集合供每个窗口判断是否包含同一份受影响的共享业务状态。 let mut affected = std::collections::HashSet::new(); // 找出解析到同一底层 Data 的所有别名,失效共享数据时不能只刷新调用者包装身份。 for weak in &session.nodes { if let Some(block) = cx.upgrade(*weak)? && block::resolve(cx.store(), block)?.0 == data { + // NativeContext 持有升级临时根直到本次调用结束,不为弱索引永久添加拥有关系。 affected.insert(block); } } for presentation in session.presentations.values_mut() { + // 同一 Data 可以出现在多个窗口,失效通知必须广播,不能仅清 current 的缓存。 presentation.invalidate(cx, &affected, &reason)?; } return Ok(NativeResult::Return(Value::Nil)); } if matches!(self.operation, Mounts) { + // 返回的是这个 Block 的具体展示出现,不能把底层 Data 相同的其他别名也算到 owner 上。 let owner = self.owner.ok_or(invalid("mount query requires a Block"))?; + // 查全部窗口,锁内只读取原生 Mount 表与托管 payload,不执行源回调。 let session = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // 暂留窗口/出现 ID 作为排序键,最后列表只暴露 Mount 对象,不暴露宿主元组。 let mut positions = Vec::new(); for (window, presentation) in &session.presentations { + // 每窗活动表只有当前仍展示的出现,死亡弱项和其他 Block 的出现都不加入结果。 for (id, weak) in &presentation.mounts.active { if let Some(object) = cx.upgrade(*weak)? && cx @@ -154,6 +188,7 @@ impl NativeCallable for Function { } // 跨窗口 Mount 查询按窗口/出现 ID 稳定排序,不依赖 HashMap 遍历顺序;返回每个具体出现而非几何副本。 positions.sort_unstable_by_key(|(window, id, _)| (*window, *id)); + // 用专用托管集合保留结果引用和只读访问契约,不把 Rust Vec 直接当源值返回。 let set = crate::mount::collection( cx, positions.into_iter().map(|(_, _, object)| object).collect(), @@ -162,25 +197,31 @@ impl NativeCallable for Function { return Ok(NativeResult::Return(Value::Object(set))); } if matches!(self.operation, NewWindow) { + // 持锁分配新的窗口 ID 并建立 Presentation,create_window 还会检查只读/退出/数量限制。 let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // Window 只能通过 Type 构造入口创建,缺少 requested_type 说明不是合法的类型调用。 let definition = self .requested_type .ok_or(invalid("Window constructor requires a type"))?; + // 返回托管资源对象,原生窗口由随后发布的帧/命令驱动 GUI 创建。 let window = create_window(cx, &mut state, definition)?; return Ok(NativeResult::Return(Value::Object(window))); } if matches!(self.operation, CloseWindow) { + // 资源对象来自 bound receiver,不用 current 猜要关闭哪个窗口。 let window = self .owner .ok_or(invalid("window close requires a receiver"))?; + // 用 payload 的资源 ID 定位 Presentation;关闭状态由 window::close 在模型中维护。 let id = cx .store() .payload::(window, "Window")? .id; crate::window::close(cx, window)?; + // model 关闭成功后才改宿主展示状态,关闭失败不能提前标记为已退场。 let mut state = self .session .lock() @@ -195,6 +236,7 @@ impl NativeCallable for Function { return Ok(NativeResult::Return(Value::Nil)); } if matches!(self.operation, Window) { + // ui.window() 查询当前选择窗口的资源对象;和 Window() 分配新窗口的路径不同。 let desktop = self .session .lock() @@ -205,15 +247,19 @@ impl NativeCallable for Function { )); } if matches!(self.operation, Viewport) { + // ui.viewport() 从当前窗口的输入对象生成结果,不直接读取 winit/OS 尺寸。 let desktop = self .session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; + // 运行时还没提供尺寸时属性为 nil,明确报不可用,不把它伪装成零宽高。 let Value::Object(input) = cx.store().get_attr(desktop, "viewport")? else { return Err(invalid("window viewport is unavailable")); }; + // 两个普通属性读取参与 VM 依赖跟踪,尺寸变化可让调用方的测量失效。 let width = cx.store().get_attr(input, "width")?; + // 返回 MeasureResult 协议值而非把内部输入对象本身暴露给源程序修改。 let height = cx.store().get_attr(input, "height")?; return Ok(NativeResult::Return(Value::Object( crate::native_visual::record( @@ -227,15 +273,19 @@ impl NativeCallable for Function { // 诊断会读对象总数与宿主变化统计,禁止在只读渲染中观察并形成反馈依赖。 if matches!(self.operation, Stats) { if cx.store().is_read_only() { + // 即使签名标 External,这里仍防御直接适配器调用,防止布局自观察计数形成反馈。 return Err(invalid( "runtime statistics are unavailable during read-only rendering", )); } + // 各窗口共享的计数与当前展示派生计数一起投影,锁内不推进刷新。 let session = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // blocks/pages 数的是成功升级的存活业务对象,而不是弱表历史长度或 Mount 出现数。 let mut blocks = 0_i64; + // 页面是 Block 的子集,同一页跨窗口迁移不会增加业务页面计数。 let mut pages = 0_i64; for weak in &session.nodes { if let Some(object) = cx.upgrade(*weak)? { @@ -245,8 +295,11 @@ impl NativeCallable for Function { } } } + // 统计对象总数在分配 stats 结果之前采样,避免把本次诊断结果计入自己。 let objects = cx.store().object_count() as i64; + // session 的 Deref 指向 current,tabs/mounts 与布局计数属于当前窗口,而非跨窗求和。 let tabs = cx.store().dict_items(session.tabs)?.len() as i64; + // 新普通对象只装原生数值/文本,不把 VM store 或 Session 句柄暴露给程序。 let stats = cx.new_object()?; for (name, value) in [ ("objects", objects), @@ -273,8 +326,10 @@ impl NativeCallable for Function { session.dependency_invalidations as i64, ), ] { + // 统一转成语言整数,字段名字是 ui.stats 的观察接口,不影响调度逻辑。 cx.set_attr(stats, name, Value::Number(value.into()))?; } + // 单独保留文本失效原因,数值计数不能解释究竟哪条源属性触发了重算。 cx.set_attr( stats, "lastInvalidDependency", @@ -297,6 +352,7 @@ impl NativeCallable for Function { } // 状态是普通名义 Object,value 属性经 VM 追踪,set/increment 方法捕获同一对象,不额外建立原生状态副本。 if matches!(self.operation, State) { + // value 及绑定方法都是普通托管属性,GC 能追踪回调和状态之间的真实边。 let state = cx.new_object()?; attach_named_type(cx, state, "Object", &self.session)?; cx.set_attr(state, "value", args[0].clone())?; @@ -305,10 +361,13 @@ impl NativeCallable for Function { return Ok(NativeResult::Return(Value::Object(state))); } if matches!(self.operation, StateSet | Increment) { + // 状态对象由 bind 捕获,不能把 args[0] 当 receiver 再切一次参数。 let owner = self.owner.ok_or(invalid("state method has no owner"))?; + // set 接受任意新值;increment 则对当前 value 做精确整数读取与溢出检查。 let value = if matches!(self.operation, StateSet) { args[0].clone() } else { + // 先限制 Number 表示,再从中取可表示 i64 的整数;不截断十进制或二进制小数。 let Value::Number(value) = cx.store().get_attr(owner, "value")? else { return Err(invalid("increment requires an integer state")); }; @@ -319,22 +378,28 @@ impl NativeCallable for Function { .ok_or(invalid("integer state increment exceeds its range"))?; Value::Number(next.into()) }; + // 通过统一属性写入口更新版本,textOf/绑定的依赖失效因此能被下一轮刷新观察。 cx.set_attr(owner, "value", value)?; return Ok(NativeResult::Return(Value::Nil)); } if matches!(self.operation, TextOf) { + // 先保存被观察对象和属性名;这里不是把当前值立即格式化成一个永不更新的字符串。 let source = object(args.first())?; + // 属性名必须是真字符串,不能从任意对象 repr 推导不稳定的名字。 let field = text(args.get(1))?; // textOf 建立前确认字段可读取,具体字符串化与依赖记录发生在快照提取时。 cx.store().get_attr(source, &field)?; + // 字号可选但必须是等级整数,缺省为 1;颜色在后面按独立可选参数解析。 let size = args .get(2) .map(|v| integer(Some(v))) .transpose()? .unwrap_or(1); if !(1..=8).contains(&size) { + // 数字能转为整数不代表是受支持等级,在分配 TextBlock 之前拒绝越界。 return Err(invalid("text size must be 1 through 8")); } + // 初始文字为空,真正内容留给快照读取 source.field;构造不冻结当前属性值。 let mut data = Data::new( Kind::Text { value: String::new(), @@ -355,6 +420,7 @@ impl NativeCallable for Function { } // 普通组件先组装 Data,再统一由 new_typed_block 安装 Cell、属性和名义类型;不直接写原生窗口。 let constructor = match self.operation { + // 滚动视口有限宽高和唯一内容 Block;滚动轴/偏移由属性和 Mount 后续处理。 Scroll => Some(Data::new( Kind::Scroll { width: dimension(args.first())?, @@ -364,6 +430,7 @@ impl NativeCallable for Function { 0.0, 0.0, )), + // 文本是叶节点,size 的 u64→u32 转换和下面统一 1..=8 检查分别限制表示与语义。 Text => Some(Data::new( Kind::Text { value: text(args.first())?, @@ -380,6 +447,7 @@ impl NativeCallable for Function { 0.0, 0.0, )), + // 宽高必填,圆角缺省零,颜色缺省浅蓝;每个尺寸都先经有限非负检查。 Rect => Some(Data::new( Kind::Rect { width: dimension(args.first())?, @@ -395,18 +463,23 @@ impl NativeCallable for Function { 0.0, 0.0, )), + // 空白只提供主轴占位,不带孩子或绘图颜色。 Spacer => Some(Data::new( Kind::Spacer(dimension(args.first())?), vec![], 0.0, 0.0, )), + // 三种容器共用参数解码,Kind 决定后续横排/纵排/叠放,不在这里计算子项坐标。 Row | Column | Overlay => Some(Data::new( if matches!(self.operation, Row) { + // 行布局随后沿水平主轴累计孩子位置。 Kind::Row } else if matches!(self.operation, Overlay) { + // 叠放布局共享内容原点,不采用线性 gap 累加位置。 Kind::Overlay } else { + // 此入口仅剩 Column,后续沿纵向排列。 Kind::Column }, // 线性容器固定前两参为 padding/gap,后续都是孩子对象;组成合法性在 Block 分配前统一检查。 @@ -417,7 +490,9 @@ impl NativeCallable for Function { dimension(args.first())?, dimension(args.get(1))?, )), + // 空 Page 尚未登记到任何窗口,直到 render/open 才安排展示。 Page => Some(Data::new(Kind::Page, vec![], 0.0, 0.0)), + // 二维场景只有孩子对象序列;各对象的 x/y 等世界属性在快照提取时读取。 Scene2D => Some(Data::new( Kind::Scene2D, args.iter() @@ -427,6 +502,7 @@ impl NativeCallable for Function { 0.0, )), CanvasViewport => { + // 视口自身参加有限布局,第三实参保存 Scene2D 引用,不把场景孩子展开成行列内容。 let mut data = Data::new( Kind::CanvasViewport { width: dimension(args.first())?, @@ -441,12 +517,15 @@ impl NativeCallable for Function { Some(data) } Viewport3D => { + // 当前 desktop 提供相机/控制器的名义类型,所有默认资源在同一 VM 内创建。 let desktop = self .session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; + // 默认相机使用透视投影;这里分配模型对象,不立即生成场景渲染快照。 let definition = desktop_type(cx.store(), "PerspectiveCamera", desktop)?; + // 空参数选择场景模块的默认姿态,之后源属性仍可显式换相机或改参数。 let camera = crate::scene3d_model::allocate( cx, desktop, @@ -454,6 +533,7 @@ impl NativeCallable for Function { crate::scene3d_model::Constructor::Perspective, &[], )?; + // 普通 children 先为空,世界场景独立存于 scene3d 字段;面板 UI 会在提取时派生。 let mut data = Data::new( Kind::Viewport3D { width: dimension(args.first())?, @@ -465,6 +545,7 @@ impl NativeCallable for Function { ); data.camera = Some(camera); data.scene3d = Some(object(args.get(2))?); + // 每个新视口独立控制器,避免两个不同相机的输入共享原生控制状态。 let definition = desktop_type(cx.store(), "OrbitController", desktop)?; // 三维视口默认独立透视相机及 OrbitController,Scene3D 引用则来自调用者,可显式共享世界。 data.controller = Some(crate::scene3d_model::allocate( @@ -478,6 +559,7 @@ impl NativeCallable for Function { } // 按钮标签也是普通 TextBlock 孩子,布局和生命周期沿同一组成树,不是原生按钮硬编码文字。 Button => { + // 先创建真实文字孩子,让按钮尺寸和文本测量、字体缓存、子树生命周期保持一致。 let label = new_block( cx, Data::new( @@ -494,6 +576,7 @@ impl NativeCallable for Function { )?; Some(Data::new(Kind::Button, vec![label], 10.0, 0.0)) } + // 非构造操作继续进入页面/实例方法分派;None 不是失败,也不创建占位对象。 _ => None, }; // 统一检查文本等级和 UTF-8 字节量,再按 requested_type 生成实际对象,保留子类工厂身份。 @@ -501,6 +584,7 @@ impl NativeCallable for Function { if let Kind::Text { size, value, .. } = &data.kind && (!(1..=8).contains(size) || value.len() > 16_384) { + // 两条文本工厂共用语义限制;防止类型工厂参数能转 u32 却超出实际支持字号等级。 return Err(invalid("text exceeds size limits")); } return Ok(NativeResult::Return(Value::Object(new_typed_block( @@ -513,7 +597,9 @@ impl NativeCallable for Function { if matches!(self.operation, OpenValue | Present) && let Value::Object(value) = args[0] { + // 对象值可能实现 Presentable;标量不可能走这一协议路径,稍后按普通文字转换处理。 let (desktop, current) = { + // master 用于共享能力类型,current 用于传给源 present 的窗口资源,二者不能混用。 let state = self .session .lock() @@ -530,15 +616,20 @@ impl NativeCallable for Function { .is::() && cx.store().supports(value, capability)? { + // 只有非 Block 且显式实现能力的对象才执行源 present,不能凭同名属性鸭子类型推断。 + // 能力实现依附于类型,需从该类型的方法表取 present,而非随意的实例属性。 let definition = cx .store() .type_of(value)? .ok_or(invalid("untyped Presentable"))?; + // lookup_method 处理继承规则,缺少实现即能力契约错误。 let function = cx .store() .lookup_method(definition, "present")? .ok_or(invalid("missing present method"))?; + // 源 present 接收实际目标窗口,不能总把主窗口传给副窗口中的调用。 let window = object(Some(&cx.store().get_attr(current, "window")?))?; + // 源方法返回后由 PresentResult 检查真实 Block,并按 open 标志决定是否包装 Page。 let complete = cx.allocate(ContextNativeFunction::new( "validate presentation", PresentResult { @@ -559,6 +650,7 @@ impl NativeCallable for Function { endor_core::Instruction::new(endor_core::Opcode::Call, 1), endor_core::Instruction::simple(endor_core::Opcode::ReturnValue), ]; + // 常量表按字节码的 0..3 索引依次提供续接、源方法、receiver 和 Window。 let code = endor_core::EndorCode::from_instructions( &instructions, vec![ @@ -570,8 +662,11 @@ impl NativeCallable for Function { endor_core::CodeLayout::default(), )? .with_location("Presentable.present", "", 1); + // 模板包装成托管代码后,驱动函数的强边负责维持常量/代码存活。 let code = cx.allocate(endor_core::EndorCodeObject::new(code))?; + // 驱动只访问常量,不需要用户命名空间,私有空字典避免污染全局。 let globals = cx.new_dict()?; + // 返回 Invoke 把驱动接入同一 VM 帧循环,不在 NativeContext 内递归进入 VM。 let driver = cx.new_function("Presentable.present", code, globals)?; attach_desktop_type(cx, driver, "Function", desktop)?; return Ok(NativeResult::Invoke { @@ -582,10 +677,13 @@ impl NativeCallable for Function { } // ui.open 把普通结果转换为根组件,再在必要时包一层 Page 并展示;ui.present 仅转换,不自动打开页面。 if matches!(self.operation, OpenValue) { + // 协议对象已在上面返回 Invoke;这里仅处理已有 Block 或普通值的文字回退。 let root = present_value(cx, &args[0], &self.session)?; + // Page 可直接保留,普通 Block 外包一层新 Page,以便加入窗口的页面注册表。 let page = if cx.store().payload::(root, "Block")?.page { root } else { + // 包装持有原 root,不复制业务对象,因此源对象/状态仍可继续被其他位置展示。 new_block( cx, Data::new(Kind::Page, vec![root], 0.0, 0.0), @@ -596,6 +694,7 @@ impl NativeCallable for Function { return Ok(NativeResult::Return(Value::Object(page))); } if matches!(self.operation, Present) { + // 只做值到视觉的转换,不调用 show,不改变活动页或窗口归属。 return Ok(NativeResult::Return(Value::Object(present_value( cx, &args[0], @@ -605,7 +704,9 @@ impl NativeCallable for Function { // 从这里起都是需要接收者的 Block/Page 方法,未绑定 owner 时不能继续修改任意全局对象。 let owner = self.owner.ok_or(invalid("method has no owner"))?; if matches!(self.operation, Bind) { + // 属性名与计算函数分开解码;connect 建立依赖和可取消 Subscription,不立即变成静态值。 let field = text(args.first())?; + // 函数对象的签名/效果由绑定模块检查,不能只因它是 Object 就当作合法计算闭包。 let compute = object(args.get(1))?; return Ok(NativeResult::Return(Value::Object( crate::binding::connect( @@ -622,17 +723,22 @@ impl NativeCallable for Function { } // moveTab 重排同窗口页面索引并强制下一帧更新标签;移动的是原 Page,非新页面副本。 if matches!(self.operation, MoveTab) { + // 页面位置是当前顺序下标,不是 Tab ID 或子组成 slot,先做平台 usize 表示检查。 let position = usize::try_from(integer(args.first())?) .map_err(|_| invalid("tab position out of range"))?; + // 同锁内找所属窗口并重排,不能只用 current:源代码可持有其他窗口的 Page。 let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // 未登记的 Page 没有可重排标签,不能自动打开它再进行移动。 let id = page_window(cx.store(), &state, owner)?.ok_or(invalid("Page is not open"))?; + // 对真实所属窗口设置强制帧,确保即使内容根没变,标签顺序仍会发布。 let view = state .presentations .get_mut(&id) .ok_or(invalid("missing window"))?; + // model/window 维护实际顺序,Presentation 只负责其派生投影和刷新标记。 let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?; crate::window::move_page(cx, window, owner, position)?; view.force_frame = true; @@ -640,13 +746,17 @@ impl NativeCallable for Function { } // 接收者可能是共享转发别名,解析到最终 Data 后修改副本,最后用一次 replace_payload 写入结构边。 let (data_ref, data) = block::resolve(cx.store(), owner)?; + // 在副本上编辑结构,再经末尾 replace_payload 更新强边与版本,避免直接篡改借用 payload。 let mut data = data.clone(); + // 默认返回 Unit/Nil,append/insert/订阅等分支会改成自己的业务结果;查询分支直接返回。 let mut result = Value::Nil; match self.operation { SetText => match &mut data.kind { Kind::Text { value, .. } => { + // 新文字必须为真实字符串且受 UTF-8 字节预算约束,再替换 payload/普通属性。 let next = text(args.first())?; if next.len() > 16_384 { + // 检查在写属性前,错误输入不会先清掉旧 text_binding 或部分改文字。 return Err(invalid("text exceeds 16 KiB")); } *value = next; @@ -654,21 +764,27 @@ impl NativeCallable for Function { data.text_binding = None; cx.set_attr(owner, "text", args[0].clone())?; } + // receiver 的名义类型校验之外再验证 payload,避免对不兼容原生种类写文本。 _ => return Err(invalid("setText requires a text Block")), }, SetColor => match &mut data.kind { Kind::Text { color: value, .. } | Kind::Rect { color: value, .. } => { + // 颜色格式先校验,随后把普通 color 属性与本地 payload 副本保持一致。 *value = color(args.first(), "#242422")?; cx.set_attr(owner, "color", Value::Text(value.clone()))?; } + // 容器/空白不支持这一颜色修改接口,不能往无关 payload 中补出虚构颜色字段。 _ => return Err(invalid("this Block has no color")), }, Append | Insert | Remove | Replace | Move => { if !data.is_container() { + // 只允许有组成语义的容器;叶 Block 不因调用 append 而隐式变成 Column。 return Err(invalid("expected a container Block")); } if matches!(self.operation, Append | Insert) { + // 新增边需要新 slot;append 取末尾,insert 的首参是可插入缝位置。 let (position, argument) = if matches!(self.operation, Insert) { + // 平台索引转换和容器长度检查分开,len 本身允许表示“插到末尾”。 let position = usize::try_from(integer(args.first())?) .map_err(|_| invalid("child position out of range"))?; if position > data.children.len() { @@ -676,11 +792,14 @@ impl NativeCallable for Function { } (position, args.get(1)) } else { + // append 不接位置,唯一实参就是新孩子;不会误把它解析成索引。 (data.children.len(), args.first()) }; + // 统一孩子对象解码;validate 再检查真实 Block 和无环组成。 let child = object(argument)?; // 插入前禁止孩子图回到当前 Data,避免循环组成;position 是插入位置,返回值却是稳定 slot。 block::validate(cx.store(), child, Some(data_ref))?; + // 保存当前高水位作为返回身份,再推进 next_slot,删除旧边也不回退该计数。 let slot = data.next_slot; // slot 单调增加且限制到 i64 正范围,不复用已删除身份;前后插入/重排不会把其他孩子 slot 改号。 data.next_slot = slot @@ -692,6 +811,7 @@ impl NativeCallable for Function { } else { // remove/replace/move 首参为稳定 slot 而非当前数组下标;只有 move 的第二参才是目标位置。 let slot = integer(args.first())?; + // 在当前顺序中查该稳定身份的位置,不能直接把 slot 转 usize 当数组下标。 let index = data .children .iter() @@ -699,31 +819,39 @@ impl NativeCallable for Function { .ok_or(invalid("child slot does not exist"))?; match self.operation { Remove => { + // 删除这一稳定边,不重新编号其余孩子;是否销毁 Block 由托管引用关系决定。 data.children.remove(index); } Replace => { + // 新孩子先过无环/类型校验,再替换旧边对象;边 slot 保持不变。 let child = object(args.get(1))?; block::validate(cx.store(), child, Some(data_ref))?; // replace 保留该组成边 slot,但换实际 Block;出现键还含 Block 身份,因此可能产生新的 Mount 上下文。 data.children[index].block = child; } Move => { + // 第二参是目标顺序下标,首参仍是稳定 slot;目标必须是已有序列内位置。 let to = usize::try_from(integer(args.get(1))?) .map_err(|_| invalid("child position out of range"))?; if to >= data.children.len() { + // move 不新增孩子,因此 len 是越界,不同于 insert 允许插到末尾的 len。 return Err(invalid("child position out of range")); } + // 移动完整 Child 值,slot 与对象身份都保持,不分配新的出现组成键。 let child = data.children.remove(index); data.children.insert(to, child); } + // 外层已排除 append/insert,此处只可能是 remove/replace/move。 _ => unreachable!(), } } } // children 查询返回 slot文本->Block 的托管字典,不把当前顺序假装成稳定 slot 连号数组。 Children => { + // 查询创建新字典但不改原容器;key 是 slot 文本,迭代顺序不作为下标身份保证。 let result = cx.new_dict()?; for child in data.children { + // VM 字典赋值记录真实 Block 强边,调用结果可安全独立持有孩子而非引用 Rust 临时切片。 cx.dict_set( result, Value::Text(child.slot.to_string()), @@ -734,17 +862,21 @@ impl NativeCallable for Function { } Render | Navigate => { if !matches!(data.kind, Kind::Page) { + // 只有 Page 有根替换与导航历史,不能对普通 ContainerBlock 偷用页面语义。 return Err(invalid("expected a Page")); } + // 新内容须真实 Block 且不能包含当前 Page 的共享 Data,防止出现循环组成。 let root = object(args.first())?; block::validate(cx.store(), root, Some(data_ref))?; if matches!(self.operation, Navigate) { // 首次 navigate 把当前根纳入历史,之后新导航先截断前进分支,再追加新根,保持浏览历史语义。 if data.history.is_empty() { if let Some(old) = data.children.first() { + // 首次导航才把现有根作为历史起点;空 Page 没有可保留的起始根。 data.history.push(old.block); } } else { + // 从后退位置发起新导航会丢弃原前进分支,不能保留两条互相矛盾的线性历史。 data.history.truncate(data.history_index + 1); } data.history.push(root); @@ -760,12 +892,15 @@ impl NativeCallable for Function { } Back | Forward => { if data.history.is_empty() { + // 无历史是正常无动作,不把索引减法做在空列表上,也不制造一次空展示请求。 return Ok(NativeResult::Return(Value::Nil)); } // 后退/前进钳到历史首尾,复用已保留根对象而非重新执行旧源码。 let next = if matches!(self.operation, Back) { + // saturating_sub 在首项停住,不绕回列表尾部。 data.history_index.saturating_sub(1) } else { + // 前进上界为最后已有项,不为不存在的未来页分配对象。 (data.history_index + 1).min(data.history.len() - 1) }; data.history_index = next; @@ -777,11 +912,14 @@ impl NativeCallable for Function { } // 分离先创建新 Window,再把同一 Page 展示到它;跨窗口出现上下文由后续展示同步重建。 Detach => { + // 短锁中只创建资源,锁释放后再 show_in,避免内部二次锁定同一个 Session。 let window = { + // 新窗口编号和 Presentation 同步建立,使用主注册域的 Window 默认类型。 let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // detach 创建默认 Window,不继承 Page 的源类型作为窗口类型。 let definition = desktop_type(cx.store(), "Window", state.master)?; create_window(cx, &mut state, definition)? }; @@ -789,24 +927,29 @@ impl NativeCallable for Function { return Ok(NativeResult::Return(Value::Object(window))); } PageWindow => { + // 这是查询:按真实注册表找 owner 的归属,不能直接返回当前窗口冒充其宿主。 let state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // 未打开/已关闭 Page 正常返回 nil,签名为 nullable(Window)。 let value = if let Some(id) = page_window(cx.store(), &state, owner)? { cx.store() .get_attr(state.presentations[&id].desktop, "window")? } else { + // 仍存活的 Page 可以暂时没有窗口归属,不因此新建窗口或报错。 Value::Nil }; return Ok(NativeResult::Return(value)); } Open | Activate | MoveWindow => { + // moveToWindow 必须明确目标;open/activate 的缺省则保留原归属或使用当前窗口。 if matches!(self.operation, MoveWindow) && matches!(args.first(), None | Some(Value::Nil)) { return Err(invalid("Page.moveToWindow requires a Window")); } + // None 与显式 nil 在 open 路径等价;非空实参先解码对象,show_in 再检查 Window payload/域。 let target = match args.first() { None | Some(Value::Nil) => None, Some(value) => Some(object(Some(value))?), @@ -816,20 +959,23 @@ impl NativeCallable for Function { } // unmount 清内容但不关闭 Page 标签,也不直接等同于销毁整个业务对象。 Unmount => { + // 内容强边在统一 replace_payload 时撤销,Page 自己仍在标签注册表中可再次 render。 data.children.clear(); } Close => { // 关闭 Page 当场取消页面和所属 Mount 任务,即使同一输入稍后又打开它,也不能复活旧异步工作。 crate::tasks::cancel_owner(cx, owner)?; + // 找出现位置并撤登记的过程只操作宿主表/任务,不在锁内执行用户生命周期钩子。 let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; if let Some(id) = page_window(cx.store(), &state, owner)? { - // Closing the tab ends its presentation even if source - // reopens it before the next layout can detach the Mounts. + // 关闭即终止旧出现的任务归属,不等待下一次布局实际卸载才取消。 + // 弱 Page 身份用于精确筛选这个页面的 Mount,不能取消整个窗口其他页面任务。 let page = cx.store().downgrade(owner)?; for weak in state.presentations[&id].mounts.ordered() { + // 升级成功且 Mount.page 匹配才取消;不会把共享 Block 的其他出现一并取消。 if let Some(mount) = cx.upgrade(weak)? && cx .store() @@ -837,9 +983,11 @@ impl NativeCallable for Function { .page == Some(page) { + // 取消 owner/key 调度资格,旧 I/O 即使稍后完成也不能再向此出现交付。 crate::tasks::cancel_owner(cx, mount)?; } } + // 撤注册后为原窗口选择替代页或安排清空,生命周期卸载仍交后续刷新处理。 remove_page( cx, state @@ -852,16 +1000,19 @@ impl NativeCallable for Function { return Ok(NativeResult::Return(Value::Nil)); } OnClick => { + // 简便 onClick 保存单个处理器,validate_handler 检查真实 callable 契约而非仅有对象身份。 let handler = object(args.first())?; crate::event_dispatch::validate_handler(cx.store(), handler)?; data.click = Some(handler); result = Value::Object(owner); } On => { + // 字符串事件名用于传播匹配,按 UTF-8 字节限制长度,不允许空事件名。 let kind = text(args.first())?; if kind.is_empty() || kind.len() > 128 { return Err(invalid("invalid event kind")); } + // 先校验回调再建立 Connection,避免非法处理器留下不可取消的半条订阅。 let handler = object(args.get(1))?; crate::event_dispatch::validate_handler(cx.store(), handler)?; // 订阅默认非捕获;可用 Bool 或 capture/bubble 文本明确阶段,其他值拒绝而不做真值转换。 @@ -875,23 +1026,28 @@ impl NativeCallable for Function { // 清掉已断开连接后再计算容量,保留仍有效订阅顺序;新 Connection 拥有 handler,返回 token 仅弱引用连接。 let mut active = Vec::new(); for token in &data.subscriptions { + // data.subscriptions 实际保存 Connection 身份,已断开者 handler=None 不再占容量。 if cx .store() .payload::(*token, "Connection")? .handler .is_some() { + // 按原序保留仍有效连接,事件路由中的监听顺序因此不因清理而改变。 active.push(*token); } } if active.len() >= MAX_UI_NODES { + // 清理后仍满才拒绝,避免死亡连接永久占满一个 Block 的订阅预算。 return Err(invalid("too many event subscriptions")); } + // Connection 强持 handler,由 Block 的 subscriptions 拥有;任务和事件生命周期可据此断开。 let connection = cx.allocate(crate::event_dispatch::Connection { kind, handler: Some(handler), capture, })?; + // 返回的 Subscription 仅弱指连接,不应因用户保留令牌而让整个已卸载组件和回调常驻。 let token = cx.allocate(crate::event_dispatch::Subscription { connection: cx.store().downgrade(connection)?, })?; @@ -903,6 +1059,7 @@ impl NativeCallable for Function { // 基类事件/挂载回调默认空实现,子类可覆盖;不在这条默认路径生成额外状态。 OnEvent | OnMount | OnUnmount => return Ok(NativeResult::Return(Value::Nil)), Click | Dispatch => { + // 事件/订阅类型来自当前展示域,传播上下文仍由明确 owner/可选 Mount 指定。 let desktop = self .session .lock() @@ -910,17 +1067,22 @@ impl NativeCallable for Function { .desktop; // click 新建种类为 click 的 Event,dispatch 使用调用者提供事件;两者都将可选具体 Mount 交传播模块验证。 let (event, mount) = if matches!(self.operation, Click) { + // click 总创建一份新 Event,调用方只可补具体 Mount;不会把旧事件对象重复分发。 let definition = desktop_type(cx.store(), "Event", desktop)?; ( crate::event_dispatch::new_event(cx, definition, "click")?, args.first(), ) } else { + // dispatch 接现成事件,第二实参才是可选出现;传播模块继续验证两者语义。 (object(args.first())?, args.get(1)) }; + // None 保留“未指定出现”含义,显式非法值传播错误;不从任意当前 Mount 猜目标。 let mount = mount.map(|value| object(Some(value))).transpose()?; + // 返回 Invoke/Return 由事件模块决定,源捕获/目标/冒泡回调仍走同一 VM 帧循环。 return crate::event_dispatch::dispatch(cx, owner, event, mount, desktop); } + // 其余操作应在前面的专用分支处理;漏分派必须显式报错,不能悄悄返回成功 Unit。 _ => return Err(invalid("unsupported Block operation")), } // 非提前返回的结构编辑/订阅操作在末尾统一经过写屏障,触发拥有边更新和变更日志。 diff --git a/src/bridge/ui_bridge/developer_pages.rs b/src/bridge/ui_bridge/developer_pages.rs index 9f8be78..08c590b 100644 --- a/src/bridge/ui_bridge/developer_pages.rs +++ b/src/bridge/ui_bridge/developer_pages.rs @@ -1,30 +1,38 @@ //! 把内置开发工具和起始页接入托管 Page。工厂在所属 VM 中编译和执行,而不是在窗口线程另建业务状态。 //! -//! Developer workspaces are managed Pages, using the same roots and placement API. +//! bootstrap::install 调用 install 和 install_start_page;前者注册 ui.developerTools,后者注册 ui.newTab。 +//! GUI 发出的 developer-open:* 输入由 UiBridge::dispatch 转成一次 VM 调用,再进入 OpenTool::invoke。 +//! 这里管理 Page 的创建与归属;Console/Network 等工具的原生内容由 shell/developer_tools 按页面标记绘制。 use super::*; #[derive(Debug)] /// 工具打开能力,保存所属会话和 ui 对象;调用时校验工具名,再在该 VM 中展示页面。 struct OpenTool { + // 查找现有页面和安排 show 时使用同一展示域;不另建开发工具专属 VM。 session: Shared, + // 保存 ui.newTab 工厂入口;ContextNativeFunction 的 captures 同时声明这个托管强边。 ui: ObjectRef, } impl NativeCallable for OpenTool { - /// 仅接受一个工具名称字符串,工具类型与白名单在 invoke 检查。 + /// VM 在进入 invoke 前检查一个显式参数;这里只有数量检查,字符串类型与白名单由 invoke 检查。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } - /// 把白名单工具名转换成对应托管 Page;New tab 转交预编译工厂,其余管理页复用已有实例或创建新页。 + /// VM 执行 ui.developerTools 时调用:New tab 委托源工厂,其余工具优先激活已有页面,否则创建标记页。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { + // 同时验证数量和字符串表示;不能把任意对象的可读表示当作工具名字接受。 let [Value::Text(name)] = args else { + // 错误发生在任何页面分配/登记之前,因此无须撤销一次半完成的工具打开。 return Err(invalid("developerTools requires a tool name")); }; // 工具名白名单包含四个管理页和特殊新标签动作,拒绝任意字符串进入工厂/工具标记路径。 if !["Console", "Sources", "Network", "Settings", "New tab"].contains(&name.as_str()) { + // 白名单错误交回 VM,不把名字继续写入 _tesseraTool 让 GUI 猜测工具类型。 return Err(invalid("unknown developer tool")); } // 新标签交给预编译 ui.newTab 托管工厂,经 NativeResult::Invoke 回到同一 VM 执行。 if name == "New tab" { + // 只解析函数身份,Invoke 让 VM 自己推进源工厂,保留调用栈/预算与托管返回值语义。 let factory = object(Some(&cx.store().get_attr(self.ui, "newTab")?))?; return Ok(NativeResult::Invoke { function: factory, @@ -39,16 +47,19 @@ impl NativeCallable for OpenTool { .nodes .clone(); for weak in nodes { + // NativeContext::upgrade 的临时引用由原生调用域管理;这里只保留实际选中的工具页作为返回值。 if let Some(page) = cx.upgrade(weak)? && cx.store().get_attr(page, "_tesseraTool").ok() == Some(Value::Text(name.clone())) { // 已有同类工具页则激活并聚焦其所属原生窗口,不每次打开都创建重复 Page。 show(cx, page, &self.session)?; let window = { + // show 可能保留旧归属;重新查所属窗口,Focus 应发给真实窗口而不是当前主窗口。 let state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; + // show 成功后仍无登记表示桥接关系异常,不能随意选择其他窗口聚焦。 let id = page_window(cx.store(), &state, page)? .ok_or(invalid("tool is not open"))?; object(Some( @@ -56,6 +67,7 @@ impl NativeCallable for OpenTool { .get_attr(state.presentations[&id].desktop, "window")?, ))? }; + // 只登记原生命令,GUI 在发布后再执行系统聚焦;VM worker 不直接操作 OS 窗口。 crate::window::enqueue(cx, window, crate::window::Action::Focus)?; return Ok(NativeResult::Return(Value::Object(page))); } @@ -67,14 +79,16 @@ impl NativeCallable for OpenTool { &self.session, None, )?; + // title 是用户可见标签,_tesseraTool 是宿主识别内容的私有标记,不能只设置其中一个。 cx.set_attr(page, "title", Value::Text(name.clone()))?; cx.set_attr(page, "_tesseraTool", Value::Text(name.clone()))?; show(cx, page, &self.session)?; Ok(NativeResult::Return(Value::Object(page))) } } -/// 把本模块的类型、方法或能力注册到所属运行时上下文。 +/// bootstrap::install 在任务/类型能力就绪后调用,注册开发工具 callable;此时不打开任何页面。 pub(super) fn install(vm: &mut Vm, ui: ObjectRef, session: &Shared) -> Result<()> { + // payload 共享 Session,captures 显式持有 ui;仅存 Rust ObjectRef 不会形成 VM 可追踪强边。 let function = vm.allocate(ContextNativeFunction::new( "developerTools", OpenTool { @@ -89,16 +103,17 @@ pub(super) fn install(vm: &mut Vm, ui: ObjectRef, session: &Shared) -> Result<() Ok(()) } -/// Compile the bundled Endor page once per VM, in a private module namespace. -/// Its functions/classes and callbacks remain ordinary managed VM objects. -/// 编译并安装 VM 私有的起始页工厂,供新页面动作重复调用。 +/// bootstrap 在 UiBridge 组装完成后调用,编译内置起始页模块并把 newTab 工厂挂到 ui。 +/// 每个 VM 仅安装一次,后续新标签操作重复调用工厂;源类/闭包仍由正常 VM 对象系统管理。 pub(super) fn install_start_page(vm: &mut Vm, bridge: &UiBridge) -> Result<()> { // 源码随 binary 嵌入,每个 VM 安装时解析/编译一次;函数工厂之后重复执行不再重复编译模块。 let source = frontend::EndorReader::from_text( "tessera:start-page.endor", include_str!("start_page.endor"), ); + // 按完整 Module 解析,保证类型与工厂定义属于一次安装环境,而不是用户 REPL 的多个输入片段。 let ast = frontend::parse_source_ast(&source, frontend::ParseMode::Module)?; + // 这里只生成代码模板,后面的 load 才把模板变成当前 VM 的托管代码对象。 let compiled = compiler::compile_ast( &ast, &source, @@ -111,9 +126,12 @@ pub(super) fn install_start_page(vm: &mut Vm, bridge: &UiBridge) -> Result<()> { }, ) .map_err(|error| source.annotate(error))?; + // 私有全局字典防止内置页面辅助类污染用户命名;导出闭包可通过托管边继续持有其环境。 let globals = vm.new_dict()?; + // 暂存安装/执行错误,闭包返回之后仍统一释放 globals 的局部拥有根。 let result = (|| -> Result<()> { bridge.install_globals(vm, globals)?; + // 加载所得代码有独立根,execute 成功/失败后都需 vm.release(code)。 let code = compiled.load(vm)?; // 执行模块定义类和工厂,使用单次预算;这里不主动调用 newTab,不在安装时打开页面。 let value = vm.execute_with_limits( diff --git a/src/bridge/ui_bridge/page_input.rs b/src/bridge/ui_bridge/page_input.rs index 0eb6789..a8c9e7d 100644 --- a/src/bridge/ui_bridge/page_input.rs +++ b/src/bridge/ui_bridge/page_input.rs @@ -1,11 +1,13 @@ //! 把页面、2D 视口和 3D 视口的相机输入转换成托管对象更新。 //! 输入携带具体出现的 Mount;嵌套世界 UI 先做逆投影,避免把屏幕位移直接当成场景坐标。 //! -//! Camera mutations run on the owning VM worker, just like other page input. +//! UiBridge::dispatch 按输入 kind 分流到下面三个方法:整页、局部二维视口和局部三维场景分别处理。 +//! 它们只在所属 VM worker 上修改托管相机属性或调用 controller.update,不直接改 GUI 已发布帧。 +//! 姿态变化随后由正常依赖/快照刷新观察;任何新帧仍需走 layout/paint/commit 的完整发布路径。 use super::*; impl UiBridge { - /// 解码指定 3D 视口的旋转/平移/缩放输入,再调用其托管相机控制器。 + /// dispatch 的 scene-pan/orbit/zoom 分支调用,把命中出现位置的输入包装成 CameraGesture 交给控制器。 pub(super) fn dispatch_scene_view( &self, vm: &mut Vm, @@ -13,6 +15,7 @@ impl UiBridge { ) -> Result<()> { // 锁内只读取当前发布版本与弱 Mount 身份,离开锁后才进入 VM 操作,避免持 session 锁调用用户代码。 let (weak, desktop) = { + // 当前窗口已由外层选定;本锁内同时读版本和 Mount,避免把旧几何与新弱身份混用。 let state = self .session .lock() @@ -30,9 +33,11 @@ impl UiBridge { state.desktop, ) }; + // upgrade 返回临时拥有根,下方即使任一步失败也通过闭包外的 release 清理。 let mount = vm .upgrade(weak)? .ok_or_else(|| anyhow::anyhow!("3D viewport Mount expired"))?; + // 不能直接 ? 返回整个方法,否则错误时跳过 Mount 根释放;用闭包暂存处理结果。 let result = (|| { // 除 session 发布版本外,再检查目标自身仍挂接、版本匹配且确为 3D 视口,拒绝旧出现操作。 let occurrence = vm.payload::(mount, "Mount")?.clone(); @@ -42,8 +47,11 @@ impl UiBridge { && occurrence.geometry.scene3d_viewport, "obsolete 3D viewport" ); + // resolve 跟随 Block 的共享替换关系,再读取实际视觉状态,不能只信任旧 payload。 let (_, data) = block::resolve(vm.object_store(), occurrence.block)?; + // visual_data 合并当前可见属性,后面的相机/控制器来自模型而非原生画面的旧快照。 let data = block::visual_data(vm.object_store(), data)?; + // 没有显式相机就无法解释手势,拒绝更新而不是猜一个默认相机改变场景语义。 let camera = data .camera .ok_or_else(|| anyhow::anyhow!("missing 3D camera"))?; @@ -51,18 +59,25 @@ impl UiBridge { let controller = data .controller .ok_or_else(|| anyhow::anyhow!("3D controller disabled"))?; + // 同一视觉对象的边框/内边距由快照解析,手势锚点与可用尺寸都应扣除这部分。 let node = block::snapshot(vm.object_store(), occurrence.block)?; + // inset 将外框换算成视口内容盒,避免控制器把边框尺寸算成场景宽高。 let inset = crate::layout::insets(&node); + // 几何来自具体 Mount,两个窗口中共享同一场景仍可能有不同视图投影。 let geometry = &occurrence.geometry; + // 外层二维视图逆变换用于锚点定位;没有视图变换时使用恒等变换。 let outer = geometry.view.unwrap_or_default(); + // delta 的第一分量还会用作缩放倍率;缺失说明输入信封不满足相机事件要求。 let delta = input .delta .ok_or_else(|| anyhow::anyhow!("3D camera delta required"))?; // 缩放使用倍率而非二维位移;旋转/平移通过 logical_delta 处理嵌套二维/面板投影。 // 不可逆映射时忽略本次连续增量,不在无效坐标上继续更新相机。 let local_delta = if input.kind == "scene-zoom" { + // 缩放不产生平移 dx/dy,只把原第一分量写到 gesture.factor。 [0., 0.] } else { + // 平移/旋转先撤销外层投影;映射失败表示本帧增量无法可靠应用。 let Some(delta) = geometry.logical_delta(input.point, delta) else { return Ok(()); }; @@ -79,6 +94,7 @@ impl UiBridge { )); // 将输入封装为名义 CameraGesture 临时对象;不把原生 Input 或窗口指针直接交给源控制器。 let gesture = vm.new_object()?; + // 临时手势也有独立拥有根;把附类型/写字段/控制器错误暂存,外层始终 release(gesture)。 let result = (|| { vm.attach_type( gesture, @@ -95,10 +111,12 @@ impl UiBridge { ("pointX", point.x - geometry.x - inset.left), ("pointY", point.y - geometry.y - inset.top), ] { + // 全部数值通过统一语言数值编码写属性,控制器读取的是普通托管字段。 vm.set_attr(gesture, name, crate::native_visual::value(value))?; } // 绑定 controller.update 保留接收者语义,再带 camera/gesture 两个显式参数进行有界调用。 let function = vm.bind_attr(controller, "update")?; + // callable 校验在 VM 执行边界继续完成,非对象方法值直接作为契约错误返回。 let result = match function { Value::Object(function) => vm.call_with_limits( function, @@ -124,7 +142,7 @@ impl UiBridge { result } - /// 把具体 CanvasViewport 的位移或缩放锚点映射到其显式 Camera2D。 + /// dispatch 的 viewport-pan/zoom 分支调用,逆投影具体 CanvasViewport 输入并改写其 Camera2D 属性。 pub(super) fn dispatch_viewport_view( &self, vm: &mut Vm, @@ -132,6 +150,7 @@ impl UiBridge { ) -> Result<()> { // 二维视口同样先校验发布版本,再从活动表捕获原 Mount,不能因目标消失自动换成另一个视口。 let weak = { + // 锁内只检查发布版本并复制弱身份;下面读取相机和写 VM 属性时不持有会话锁。 let state = self .session .lock() @@ -146,10 +165,13 @@ impl UiBridge { .get(&input.mount) .ok_or_else(|| anyhow::anyhow!("viewport is no longer active"))? }; + // 临时根保证输入处理期间 Mount 仍可读取,方法尾部无论成功失败都释放。 let mount = vm .upgrade(weak)? .ok_or_else(|| anyhow::anyhow!("viewport Mount expired"))?; + // 闭包集中实际处理,使所有 ? 都先回到统一释放路径。 let result = (|| { + // 弱身份能升级不代表仍挂载/版本有效,先验证具体出现上下文。 let occurrence = vm.payload::(mount, "Mount")?; anyhow::ensure!( occurrence.mounted @@ -157,32 +179,44 @@ impl UiBridge { && occurrence.geometry.canvas_viewport, "obsolete viewport occurrence" ); + // 复制几何和 Block 身份,结束 occurrence 对 VM 的借用后才能改写相机。 let geometry = occurrence.geometry.clone(); + // Block 只用于找相机/边距,输入定位仍来自 Mount 的几何。 let block = occurrence.block; + // 共享替换后的实际视觉状态才包含当前相机引用。 let (_, data) = block::resolve(vm.object_store(), block)?; + // 托管属性覆盖 payload 缺省值,保持与布局/绘制同一视觉读取约定。 let data = block::visual_data(vm.object_store(), data)?; + // 局部视口必须有 Camera2D,不把整页 Surface 当作相机替代。 let camera = data .camera .ok_or_else(|| anyhow::anyhow!("viewport has no camera"))?; // 读取已校验的相机快照作为本次基准;共享相机用户会通过普通属性变更观察到相同更新。 let old = crate::viewport::camera(vm.object_store(), camera)?; + // 该分量在 zoom 时表示倍率,在 pan 时表示外部逻辑位移,随后分支分别解释。 let delta = input .delta .ok_or_else(|| anyhow::anyhow!("viewport input has no delta"))?; + // 基于旧快照计算三个新值,完成验证后才依次回写托管属性。 let (mut x, mut y, mut zoom) = (old.x, old.y, old.zoom); if input.kind == "viewport-zoom" { + // 缩放必须有锚点,不能让缺失指针导致隐式绕视口原点缩放。 let pointer = input .point .ok_or_else(|| anyhow::anyhow!("zoom requires anchor"))?; + // 先用屏幕/视图坐标做外框检查,再逆投影到布局逻辑坐标。 let pointer = crate::geometry::Point::new(pointer[0], pointer[1]); anyhow::ensure!( geometry.contains(pointer), "zoom anchor is outside viewport" ); + // 外框命中不保证投影可逆;None 作为错误返回,不能继续使用原屏幕数值。 let pointer = geometry .logical_point(pointer) .ok_or_else(|| anyhow::anyhow!("zoom anchor cannot be projected"))?; + // 当前 Block 快照给出布局边距,用它取得正确内容盒的缩放锚点。 let node = block::snapshot(vm.object_store(), block)?; + // inset 与布局模块共用同一规则,避免相机手势与绘制对内容原点理解不同。 let inset = crate::layout::insets(&node); // 已验证指针落在出现外框还不够;扣除 inset 后必须落入实际内容盒,禁止用边框作为缩放锚点。 let local = [ @@ -201,6 +235,7 @@ impl UiBridge { x += local[0] / old.zoom - local[0] / zoom; y += local[1] / old.zoom - local[1] / zoom; } else { + // 平移没有锚点保持公式,只需逆投影增量再除相机自身缩放。 let Some(delta) = geometry.logical_delta(input.point, delta) else { return Ok(()); }; @@ -216,11 +251,12 @@ impl UiBridge { vm.set_attr(camera, "zoom", crate::native_visual::value(zoom))?; Ok(()) })(); + // release 发生在 result? 之前,保证投影/属性写入错误不会泄漏升级得到的 Mount。 vm.release(mount)?; result } - /// 处理旧 Page 模式的兼容相机事件;独立视口使用各自的输入路径。 + /// dispatch 的 page-pan/zoom 分支调用,兼容旧 Page 表面相机;独立 CanvasViewport 不走此方法。 pub(super) fn dispatch_page_view( &self, vm: &mut Vm, @@ -228,6 +264,7 @@ impl UiBridge { ) -> Result<()> { // 兼容 Page 相机不通过 Mount 定位,而是读取当前活动页面及其 Surface 副本;发布版本仍必须一致。 let (page, mut surface, viewport) = { + // 只复制当前已发布页的相机与真实视口,不持锁执行下面的坐标计算或 VM 属性写入。 let state = self .session .lock() @@ -244,17 +281,21 @@ impl UiBridge { state.layout.viewport, ) }; + // 外层信封校验之后仍要求本路径必需的增量字段,防止不完整调用静默改变相机。 let delta = input .delta .ok_or_else(|| anyhow::anyhow!("page view requires delta"))?; if input.kind == "page-zoom" { + // 旧 Page 模式只允许 Canvas 缩放,有限文档或 Window 页面保持各自尺寸语义。 anyhow::ensure!( surface.mode == crate::page::Mode::Canvas, "zoom requires canvas mode" ); + // 以光标为缩放锚点,先要求它存在,再检查可用视口范围。 let pointer = input .point .ok_or_else(|| anyhow::anyhow!("zoom requires anchor"))?; + // 转为页面几何共用的 Point,便于调用 Surface::to_world 做缩放前逆变换。 let pointer = crate::geometry::Point::new(pointer[0], pointer[1]); anyhow::ensure!( pointer.x >= 0.0 @@ -270,6 +311,7 @@ impl UiBridge { world.y - pointer.y / surface.zoom, ); } else { + // 平移按当前 zoom 换成世界距离,不直接把屏幕像素累加进世界 pan。 surface.pan.x += delta[0] / surface.zoom; surface.pan.y += delta[1] / surface.zoom; } @@ -277,6 +319,7 @@ impl UiBridge { if let Some(viewport) = viewport { surface.clamp(viewport); } + // 内容模式 clamp 之外再加通用坐标预算;Canvas 可自由漫游,但也不能无限增长偏移。 let limit = MAX_DIMENSION as f32; surface.pan.x = surface.pan.x.clamp(-limit, limit); surface.pan.y = surface.pan.y.clamp(-limit, limit); diff --git a/src/bridge/ui_bridge/signatures.rs b/src/bridge/ui_bridge/signatures.rs index adf6a42..6a95e08 100644 --- a/src/bridge/ui_bridge/signatures.rs +++ b/src/bridge/ui_bridge/signatures.rs @@ -1,10 +1,14 @@ //! 声明宿主方法在 Endor 类型系统中的参数和返回契约。 //! 签名用于构造/调用检查,应与 calls 和各 NativeCallable 实现保持一致;这里不执行实际业务操作。 //! -//! Canonical contracts for native Block methods and constructors. +//! bootstrap 在类型安装的第二阶段调用这两个函数,把完整签名写入 TypeMethod、Function 和 NativeFactory。 +//! ui_bridge::bind 也会为状态对象的绑定函数调用 operation_signature。 +//! VM 随后在调用边界读取这些签名,验证数量、类型及调用效果;业务实现位于 calls/scene3d 等模块。 +//! 参数注解并不决定是否必填:Operation::arity 给出实参数量,注解表则检查实际出现的每一个参数。 use super::*; -/// 根据宿主方法类别提供 Endor 方法的签名,供类型与调用检查使用。 +/// bootstrap 安装实例方法时调用,构造包含 receiver 的 VM 方法契约。 +/// receiver 是方法声明所在类型,不是某次调用的实例;实际接收者可以是该类型的合法子类。 pub(super) fn method_signature( store: &endor_core::ObjectStore, desktop: ObjectRef, @@ -13,11 +17,14 @@ pub(super) fn method_signature( ) -> Result { use AnnotationType::{Any, Function, Nominal, Number, String as TextType, Unit}; use Operation::*; + // 闭包把宿主约定的名字解析成当前 desktop 的真实 Type;查找失败中止安装,不能降级为 Any。 let nominal = |name| desktop_type(store, name, desktop).map(Nominal); // 先描述显式实参,不含隐式 receiver;后面统一在 parameters 首部补接收者。 // ReadOnly/Mutating/External 是 VM 调用效果契约,不是线程调度策略。 let (arguments, returns, effect) = match operation { + // 三个数分别供位置/旋转操作使用;方法修改现有三维对象,没有额外返回值。 Translate3D | Rotate3D => (vec![Number, Number, Number], Unit, CallEffect::Mutating), + // 控制器处理被控对象和相机手势;手势是类型化协议值,不能接任意文本或标量。 CameraControl => ( vec![nominal("Object")?, nominal("CameraGesture")?], Unit, @@ -29,9 +36,13 @@ pub(super) fn method_signature( nominal("Subscription")?, CallEffect::Mutating, ), + // 查询已有出现位置,返回托管列表;不修改对象或展示关系。 Mounts => (vec![], nominal("ObjectList")?, CallEffect::ReadOnly), + // Children 的实际值仍由容器实现解释;这里的 Any 不改变只读效果约束。 Children => (vec![], Any, CallEffect::ReadOnly), + // 可选文本用于标记失效类别;是否可省略由 arity 决定,非此处的 vec 长度决定。 Invalidate => (vec![TextType], Unit, CallEffect::Mutating), + // 修改既有文本/颜色,内容合法性(如颜色格式)由调用实现进一步校验。 SetText | SetColor => (vec![TextType], Unit, CallEffect::Mutating), // append/insert 返回稳定 slot 数值;remove/replace/move 返回 Unit,调用方不能把它们当返回孩子对象的集合方法。 Append => (vec![nominal("Block")?], Number, CallEffect::Mutating), @@ -40,15 +51,19 @@ pub(super) fn method_signature( Number, CallEffect::Mutating, ), + // 数字注解只验证 Number;非负整数、稳定槽位是否存在等语义由 calls 检查。 Remove => (vec![Number], Unit, CallEffect::Mutating), Replace => (vec![Number, nominal("Block")?], Unit, CallEffect::Mutating), Move => (vec![Number, Number], Unit, CallEffect::Mutating), + // 页面内容替换/导航接受可视 Block,并改写待展示状态;此处不提前执行布局。 Render | Navigate => (vec![nominal("Block")?], Unit, CallEffect::Mutating), - // 链式 API 返回接收者名义类型,保留子类返回契约,而不是统一降为 Block。 + // 链式 API 返回声明 receiver 的名义类型;源子类实例也满足此基类契约,不承诺精确动态子类。 OnClick => (vec![Function], Nominal(receiver), CallEffect::Mutating), // 事件和生命周期方法可能执行任意用户回调,使用 External,不能冒充只读视觉查询。 OnEvent => (vec![nominal("Event")?], Unit, CallEffect::External), + // Mount 标识一次具体出现位置;同一 Block 的多次展示不能共用一个隐式上下文。 OnMount | OnUnmount => (vec![nominal("Mount")?], Unit, CallEffect::External), + // click/dispatch 返回实际事件,便于调用者观察传播状态;Mount 可选性由 arity 给出。 Click => ( vec![nominal("Mount")?], nominal("Event")?, @@ -59,28 +74,38 @@ pub(super) fn method_signature( nominal("Event")?, CallEffect::External, ), + // 迁移要求真实 Window 实例并返回页面,窗口是否关闭、是否属于当前运行时由实现继续检查。 MoveWindow => ( vec![nominal("Window")?], Nominal(receiver), CallEffect::Mutating, ), + // 标签位置是数值,Activate 无显式实参;二者都改变展示选择并允许链式使用。 MoveTab => (vec![Number], Nominal(receiver), CallEffect::Mutating), Activate => (vec![], Nominal(receiver), CallEffect::Mutating), + // detach 新建目标窗口,返回 Window 而非当前页面;资源创建属于修改操作。 Detach => (vec![], nominal("Window")?, CallEffect::Mutating), + // 这些操作只修改生命周期或历史状态,均没有业务返回值。 Unmount | Close | CloseWindow | Back | Forward => (vec![], Unit, CallEffect::Mutating), + // 先以 Any 占位,后面用 nullable(Window) 收窄,允许省略/显式 nil 选择默认归属。 Open => (vec![Any], Nominal(receiver), CallEffect::Mutating), + // 页面尚未打开时无所属窗口,稍后把返回 Any 改为 Window | nil。 PageWindow => (vec![], Any, CallEffect::ReadOnly), + // 事件名、回调和可选捕获选项;返回 Subscription 令牌供取消,不直接返回 receiver。 On => ( vec![TextType, Function, Any], nominal("Subscription")?, CallEffect::Mutating, ), + // 自由工厂 Operation 不应误装成实例方法;缺少明确契约就拒绝安装,防止默认放宽检查。 _ => return Err(invalid("unsupported native method signature")), }; // Operation 维护显式参数的必需/最大个数;方法签名整体加一,将 receiver 纳入调用约定。 let arity = operation.arity(); + // parameters[0] 固定为 receiver,因此后面细化显式参数时都必须把索引加一。 let mut parameters = vec![Nominal(receiver)]; parameters.extend(arguments); + // 先把简单 AnnotationType 升格为 CallType;下面再改写少数联合/可空位置。 let mut signature = CallSignature { arity: endor_core::CallArity::range(arity.required + 1, arity.maximum + 1), parameters: parameters.into_iter().map(CallType::from).collect(), @@ -91,15 +116,20 @@ pub(super) fn method_signature( // 少数参数/返回需要联合或可空类型,先用占位 Any 建基础签名,再按下标细化。 // On 第三个显式选项位于 parameters[3],可接受 Bool 或字符串。 match operation { + // 参数允许 nil,但非空值必须是本 desktop 注册的 Window 实例。 Open => signature.parameters[1] = CallType::nullable(nominal("Window")?), + // 没有归属是正常查询结果,与错误窗口对象的类型失败不同。 PageWindow => signature.returns = CallType::nullable(nominal("Window")?), + // 兼容布尔捕获标记和命名阶段字符串;具体字符串语义在事件连接操作中检查。 On => signature.parameters[3] = CallType::Union(vec![AnnotationType::Bool, TextType]), + // 其余操作已经完整表达,不应额外增加可空或联合类型。 _ => {} } Ok(signature) } -/// 将具体宿主 Operation 映射到参数/返回类型契约。 +/// bootstrap 和 ui_bridge::bind 调用,描述不含 receiver 的自由函数/状态绑定函数原生参数。 +/// NativeFactory 复用本结果作为 options 窄签名,再在外部包上 Type 首参和源子类字段。 pub(super) fn operation_signature( store: &endor_core::ObjectStore, desktop: ObjectRef, @@ -107,27 +137,36 @@ pub(super) fn operation_signature( ) -> Result { use AnnotationType::{Any, Bool, Number, String as TextType, Trait, Type, Unit}; use Operation::*; + // 注册表来自所属 VM 的 desktop;同名但不同身份的外部 Type 不能满足当前名义契约。 let nominal = |name| desktop_type(store, name, desktop).map(AnnotationType::Nominal); // 自由 UI 操作没有隐式 receiver;同时声明固定参数、可变尾参、返回名义类型和调用效果。 let (parameters, variadic, returns, effect) = match operation { + // 类型关系查询不改变类型表;IsInstance 签名首参为 Any,但 calls 当前仍要求实际值为对象。 IsInstance => (vec![Any, Type], None, Bool, CallEffect::ReadOnly), IsSubtype => (vec![Type, Type], None, Bool, CallEffect::ReadOnly), + // 注册能力实现会修改类型关系;implements/supports 只查询已有关系。 Implement => (vec![Type, Trait], None, Unit, CallEffect::Mutating), Implements => (vec![Type, Trait], None, Bool, CallEffect::ReadOnly), Supports => (vec![Any, Trait], None, Bool, CallEffect::ReadOnly), + // open 可把普通值包装成视觉/Page 并安排展示,返回供继续操作的 Page。 OpenValue => (vec![Any], None, nominal("Page")?, CallEffect::Mutating), // present 可能转交用户展示协议,效果交由被委托调用确定,不能声明成固定 ReadOnly。 Present => (vec![Any], None, nominal("Block")?, CallEffect::Delegated), + // state 可分配新的状态对象;ReadOnly 允许新对象初始化,不授权修改原有对象。 State => (vec![Any], None, Any, CallEffect::ReadOnly), + // set/increment 的 receiver 已存在 Function.owner 中,不计入本处参数表。 StateSet => (vec![Any], None, Unit, CallEffect::Mutating), Increment => (vec![], None, Unit, CallEffect::Mutating), + // stats 读取宿主计数而非纯托管属性依赖,按 External 描述,不作为只读布局计算接口。 Stats => (vec![], None, Any, CallEffect::External), + // viewport 返回尺寸协议值;窗口尺寸尚缺失等情形仍由实际实现处理。 Viewport => ( vec![], None, nominal("MeasureResult")?, CallEffect::ReadOnly, ), + // ui.window 查询现有窗口,Window 类型构造则创建新资源,二者不能共享调用效果。 Window => (vec![], None, nominal("Window")?, CallEffect::ReadOnly), NewWindow => (vec![], None, nominal("Window")?, CallEffect::Mutating), // 这些构造操作返回新对象,ReadOnly 允许分配并初始化新值,但不允许因此修改已有业务对象。 @@ -137,18 +176,21 @@ pub(super) fn operation_signature( nominal("TextBlock")?, CallEffect::ReadOnly, ), + // TextOf 前两个参数表示对象/属性名;Text 直接接收字符串,其后字号和颜色可选。 Text => ( vec![TextType, Number, TextType], None, nominal("TextBlock")?, CallEffect::ReadOnly, ), + // 矩形宽高为必填,后续数值/颜色的缺省规则由 Operation::arity 与构造实现共同约束。 Rect => ( vec![Number, Number, Number, TextType], None, nominal("RectBlock")?, CallEffect::ReadOnly, ), + // 空白块只需要长度;滚动块额外要求宽高和一个 Block 内容。 Spacer => (vec![Number], None, nominal("Spacer")?, CallEffect::ReadOnly), Scroll => ( vec![Number, Number, nominal("Block")?], @@ -160,15 +202,20 @@ pub(super) fn operation_signature( Row | Column | Overlay => ( vec![Number, Number], Some(nominal("Block")?), + // 共享参数规则,但返回值必须保留具体容器名义类型;matches 只选择类型名,不执行排版。 nominal(if matches!(operation, Row) { + // 主轴水平排列的容器。 "Row" } else if matches!(operation, Overlay) { + // 多个孩子叠放在同一内容区域的容器。 "Overlay" } else { + // 此分支入口只有 Row/Column/Overlay,前两者排除后剩 Column。 "Column" })?, CallEffect::ReadOnly, ), + // 按钮接收初始文本,Page 无显式参数;两者都只创建值,不自动打开窗口。 Button => ( vec![TextType], None, @@ -176,6 +223,7 @@ pub(super) fn operation_signature( CallEffect::ReadOnly, ), Page => (vec![], None, nominal("Page")?, CallEffect::ReadOnly), + // 三维视口的宽高和 Scene3D 在调用边界检查;摄像机、场景元素等由场景模块继续验证。 Viewport3D => ( vec![Number, Number, nominal("Scene3D")?], None, @@ -184,12 +232,15 @@ pub(super) fn operation_signature( ), // 3D 构造器参数来自 Kind 元数据:Number/String 映成基础注解,其余名称从本 desktop 注册表查名义类型。 SceneObject(kind) => { + // 元数据与真实构造器共用一份参数定义,避免桥接手工重复参数表后与 3D 实现漂移。 let parameters = kind .parameters() .iter() .map(|name| match *name { + // 基础数值/字符串是语言原生注解,不需要到宿主类型字典查询。 "Number" => Ok(Number), "String" => Ok(TextType), + // Vec3/Geometry3D 等名称必须解析为本会话真实 Type;未知名让整个签名安装失败。 name => nominal(name), }) .collect::, VmError>>()?; @@ -197,14 +248,17 @@ pub(super) fn operation_signature( parameters, // 3D 集合尾参使用 Object 名义约束;各具体构造操作仍需在实现中检查允许的场景对象类别。 if kind.variadic() { + // 允许额外对象参数的场景组等构造器用 Object 作尾参上界。 Some(nominal("Object")?) } else { + // 固定参数构造器禁止多余实参,不能统一使用 Any 尾参放开上界。 None }, nominal(kind.name())?, CallEffect::ReadOnly, ) } + // 二维场景可接受零到多个 Block;场景视口固定接宽、高、Scene2D,不把孩子直接当场景。 Scene2D => ( vec![], Some(nominal("Block")?), @@ -217,12 +271,14 @@ pub(super) fn operation_signature( nominal("CanvasViewport")?, CallEffect::ReadOnly, ), + // x/y/zoom 都声明为 Number,但可缺省;缩放范围等数值约束由 new_camera2d 校验。 Camera2D => ( vec![Number, Number, Number], None, nominal("Camera2D")?, CallEffect::ReadOnly, ), + // 实例方法专用 Operation 不在自由函数表内,误用在这里应立即暴露安装错误。 _ => return Err(invalid("unsupported UI function signature")), }; // 最终 arity 直接沿用 Operation,参数注解与可变尾参分开保存;不在此处执行操作或分配业务对象。 diff --git a/src/bridge/ui_bridge/start_page.endor b/src/bridge/ui_bridge/start_page.endor index 73eb030..e80700b 100644 --- a/src/bridge/ui_bridge/start_page.endor +++ b/src/bridge/ui_bridge/start_page.endor @@ -1,14 +1,18 @@ // The page, cards and actions belong to Endor. The host only owns its sidebar. // 默认页由真实 Endor 对象组成:宿主负责窗口和侧栏,卡片布局、绘制和按钮回调在源代码中定义。 +// developer_pages::install_start_page 在每个 VM 安装此模块;OpenTool 的 New tab 动作调用下面的 newTab。 +// 渲染器通过视觉协议调用 measure/paint,按钮事件通过 VM 分派到 StartActions 的绑定方法。 // Note 复用 Column 的内容测量,但固定卡片宽度;paint 只提供背景,孩子仍沿正常树绘制。 class Note: Column { // 卡片构造的最后一个子类字段保存背景色;前面的变参仍按 Column 的孩子参数处理。 tint: String + // source_layout 调用这个覆盖方法;显式借用 Column 的测量结果,只替换卡片宽度,保留内容自然高度。 function measure(self: Note, mount: Mount, constraints: Constraints): MeasureResult { // 沿用 Column 内容测量得到高度,但卡片宽固定为 224;不把背景圆角绘制误当布局尺寸。 measured = Column.measure(self, mount, constraints) return MeasureResult(224, measured.height) } + // source_paint 在已完成几何上调用;这里返回绘制命令,未直接向 OS 或像素缓冲写入。 function paint(self: Note, mount: Mount, geometry: Bounds): DisplayList { // 只生成当前卡片背景,命令使用宿主给定 Bounds 的绝对布局位置,孩子仍由各自 paint 绘制。 fill = PaintCommand() @@ -36,25 +40,35 @@ class StartActions { home: Page // 窗口页面是普通有限 Column;可用区域来自宿主发布的逻辑视口。 function blank(self: StartActions, event: Event): Unit { + // “窗口页面”按钮的 click 回调创建独立 Page,标题和内容属于新页面,home 仍指向原画布。 page = Page() + // 标题供 project_tabs 投影到原生侧栏,不影响页面身份。 page.title = "窗口页面" + // 返回按钮保留原 home 的绑定方法,点击时激活已有页而非新建一个画布。 back = Button("返回画布") // 绑定的是原 home 的 activate 方法;此回调恢复已有页身份,不重新构造主画布。 back.onClick(self.home.activate) + // render 设置内容并安排展示,Column 的 padding/gap 只影响这张页的有限布局。 page.render(Column(56, 20, TextBlock("恰好一个窗口", 3), TextBlock("页面跟随可用区域调整。内容和工具栏由 Endor 自己组织。"), back)) } // 延伸方向属于容器与滚动组合:Column 安排内容,有限 ScrollBlock 控制可见范围。 function vertical(self: StartActions, event: Event): Unit { + // “纵向延伸”按钮调用;保持 Page 默认有限布局,滚动行为由后面的 ScrollBlock 承担。 page = Page() + // 标签标题和其他示例区分,Page 本身不切换到旧式无限页面模式。 page.title = "纵向页面" + // 外 Column 累加卡片高度,内 Column 组织说明文字;自然内容高度可超过可见视口。 content = Column(48, 28, Column(0, 12, TextBlock("向下,继续生长", 3), TextBlock("纵向滚动和 Column 排版可以独立组合。"))) + // 每张 Note 的最后一参对应 tint 子类字段,前面的视觉值仍是 Column 的子项。 content.append(Note(24, 16, TextBlock("01 / 收集想法", 2), RectBlock(176, 130, 10, "#E2EADB"), TextBlock("从一个小小的念头开始。"), "#F0F3E9")) content.append(Note(24, 16, TextBlock("02 / 连接片段", 2), RectBlock(176, 130, 10, "#E6DBEF"), TextBlock("内容保留在同一条连续的页面里。"), "#F4EEF7")) content.append(Note(24, 16, TextBlock("03 / 慢慢展开", 2), RectBlock(176, 130, 10, "#EDDCC5"), TextBlock("追加一个子组件,内容自然延伸。"), "#FAF2E7")) content.append(Note(24, 16, TextBlock("04 / 留一点空白", 2), TextBlock("不必把所有想法都放在第一屏。"), "#EDF1F6")) // 固定初始视口内装完整内容;下一行限制滚动轴,flex 和页面 stretch 再让视口服从窗口分配。 view = ScrollBlock(1024, 800, content) + // 只消费纵向滚轮,横向内容不会因普通滚动意外平移。 view.axis = "vertical" + // flex 请求父布局分配剩余主轴空间,crossAlign 让有限视口横向填满页面。 view.flex = 1 page.crossAlign = "stretch" // 最后提交有限视口作为 Page 内容,触发展示请求;真正的测量、绘制与原生发布由宿主刷新流程完成。 @@ -62,18 +76,25 @@ class StartActions { } // Row 内可继续放 Column 卡片;横向滚动不要求改变 Page 的身份或生命周期。 function horizontal(self: StartActions, event: Event): Unit { + // “横向延伸”按钮调用;横向排列和可见区域分别交给 Row 与 ScrollBlock。 page = Page() + // 仅设置标签内容,实际页面资源仍在当前 VM 的 Window 注册表中登记。 page.title = "横向页面" + // Row 沿横轴连接 Note;各 Note 保留自己的纵向 Column 测量/背景绘制覆盖。 content = Row(48, 32, Note(24, 16, TextBlock("从左,走向右", 2), RectBlock(176, 180, 10, "#DDE8F1"), TextBlock("Row 内仍然可以包含 Column。"), "#EDF3F8")) + // append 按顺序创建稳定子 slot,横向坐标由布局累加,不需要手工填写 x/y。 content.append(Note(24, 16, TextBlock("收集", 2), RectBlock(176, 210, 10, "#D7E5DA"), TextBlock("一张卡片,一个想法。"), "#EFF4EC")) content.append(Note(24, 16, TextBlock("整理", 2), RectBlock(176, 170, 10, "#E5D9EB"), TextBlock("横向滚动,保留思考的顺序。"), "#F4EFF7")) content.append(Note(24, 16, TextBlock("继续", 2), RectBlock(176, 230, 10, "#EFDFC7"), TextBlock("Shift + 滚轮浏览更多内容。"), "#FAF3E8")) content.append(Note(24, 16, TextBlock("不止一屏", 2), TextBlock("右侧仍有空间。"), "#EEF1F5")) + // 这些尺寸是构造缺省值,flex/stretch 会让视口接受当前窗口的实际可用空间。 view = ScrollBlock(1024, 800, content) // Row 自然宽度可超出视口,只有横向滚动;卡片内部仍是 Column 的纵向排版。 view.axis = "horizontal" + // 保留横向滚动内容的自然宽度,视口本身仍参加有限页面的弹性布局。 view.flex = 1 page.crossAlign = "stretch" + // 只安排展示,不在 click 回调里递归运行 measure/paint;worker 在回调结束后刷新。 page.render(view) } // 重新调用工厂得到独立页面与相机,已有画布继续保留自己的视角。 @@ -85,8 +106,10 @@ class StartActions { function counter(self: StartActions, event: Event): Unit { // 共享的是同一个托管状态对象;textOf 两次读取该对象 value,increment 更新后两个展示一起刷新。 count = ui.state(0) + // 按钮保留 increment 的绑定 owner,同一状态可同时驱动两个 textOf 派生视觉对象。 add = Button("增加计数") add.onClick(count.increment) + // 创建承载两个观察视图的新页面,状态对象不会因函数返回立即消失:绑定/回调仍持有它。 page = Page() page.title = "共享对象" page.render(Column(48, 24, TextBlock("一个对象,两个视图", 2), Row(0, 32, ui.textOf(count, "value", 3), ui.textOf(count, "value", 3)), add)) @@ -94,14 +117,21 @@ class StartActions { } // 页面工厂:先建立业务对象/动作,再建立世界组成,最后用一个有限视口显示它。 function newTab(): Page { + // OpenTool 的 New tab 与 StartActions.canvas 都调用这个工厂;每次都创建不同页面身份。 page = Page() + // 后续标签投影读此字段,不使用标题作为查找或去重依据。 page.title = "自由画布" // 先建立引用当前 Page 的动作对象,再绑定按钮;回调中的 home 指向这一轮工厂创建的页面。 actions = StartActions(page) + // 每个按钮只保存动作入口,点击前不提前创建示例 Page。 fixed = Button("窗口页面") + // 绑定 vertical 回调后才会创建纵向滚动内容。 vertical = Button("纵向延伸") + // 横向示例和纵向示例各有独立 Page/ScrollBlock,互不共享滚动偏移。 horizontal = Button("横向延伸") + // 新画布动作再调用工厂,使相机导航状态与此画布隔离。 canvas = Button("新的画布") + // QuietButton 只隐藏原生按钮背景,其 click 事件和孩子文字仍沿普通 Button 协议工作。 shared = QuietButton("试试:一个对象,两个视图 ↗") // 订阅绑定方法而非提前调用;事件触发后在原 VM 中创建/展示相应示例页。 fixed.on("click", actions.blank) @@ -109,37 +139,45 @@ function newTab(): Page { horizontal.on("click", actions.horizontal) canvas.on("click", actions.canvas) shared.on("click", actions.counter) + // hero 内部用有限行列排版,只有整个 hero 在世界场景中的摆放采用显式 x/y。 hero = Column(0, 16, TextBlock("给想法,一点空间。", 3, "#303941"), TextBlock("页面可以是一扇窗口,也可以一直延伸。", 1, "#7D858C"), Spacer(8), Row(0, 10, fixed, vertical), Row(0, 10, horizontal, canvas), shared) // x/y 属于 Scene2D 子组件的世界位置;卡片内部依旧由普通 Row/Column 排版。 hero.x = 316 hero.y = 254 // Scene2D 只规定孩子的世界位置;加入后仍可追加普通卡片,位置不受窗口宽高限制。 world = Scene2D(hero) + // 左侧卡片由 Note 测高并绘制 tint 背景,文字和矩形作为正常孩子分别绘制。 left = Note(24, 14, TextBlock("随手记下", 2), RectBlock(176, 100, 10, "#D5E4DA"), TextBlock("想法不必排成一条直线。"), "#EDF3E9") left.x = 36 left.y = 92 // 将卡片以稳定 slot 加入场景;其 x/y 在前面单独设置,不依赖追加次序推算位置。 world.append(left) + // 顶部卡片只有文本,Note 仍复用内容测量,不需要为没有矩形的情况增加特殊宿主代码。 top = Note(24, 14, TextBlock("四种页面,同一个世界"), TextBlock("窗口 · 纵向 · 横向 · 自由画布", 1, "#7D858C"), "#EFF1F7") top.x = 466 top.y = 58 world.append(top) + // 右侧卡片展示“组件布局”和“相机视角”分开:移动相机不改写这个对象的世界坐标。 right = Note(24, 14, RectBlock(176, 138, 12, "#DDDAEA"), TextBlock("把片段放在一起", 2), TextBlock("布局属于组件,视角属于摄像机。"), "#F1EDF5") right.x = 826 right.y = 190 world.append(right) + // 底部说明中的换行属于源文本内容,不能把它误当实际控制输入或页面模式配置。 bottom = Note(24, 14, TextBlock("不止眼前这一屏", 2), TextBlock("触控板或滚轮平移\n空格拖动 / 鼠标中键拖动\n双指捏合 / ⌘ Ctrl + 滚轮缩放"), "#F7F0E5") bottom.x = 104 bottom.y = 524 world.append(bottom) + // 场景边缘再放一张普通 Note,展示超出初始屏幕范围的内容仍可通过相机漫游到达。 end = Note(24, 14, TextBlock("继续向外探索", 2), RectBlock(176, 100, 10, "#CEDFE7"), TextBlock("画布上的对象仍是普通 Endor 组件。"), "#EAF1F5") end.x = 716 end.y = 614 world.append(end) // 视口参加有限布局并占用剩余空间;移动它的 Camera2D 不改写卡片世界位置。 view = CanvasViewport(1024, 800, world) + // 有限视口申请剩余空间,页面交叉轴拉伸,不把 Scene2D 改造成窗口尺寸的有限容器。 view.flex = 1 page.crossAlign = "stretch" page.render(view) + // render 已登记/安排展示,返回 Page 给 OpenTool/调用者;返回值不是 GUI 线程的原生窗口对象。 return page } diff --git a/src/bridge/ui_bridge/tree.rs b/src/bridge/ui_bridge/tree.rs index e930762..3ece657 100644 --- a/src/bridge/ui_bridge/tree.rs +++ b/src/bridge/ui_bridge/tree.rs @@ -1,7 +1,9 @@ //! 定义从托管对象提取的不可变视觉树。共享业务对象可以有多次展示,树节点与 Mount 出现的身份不是同一概念。 //! 节点保存排版、绘制和协议投影信息;浅比较用于决定是否复用已有快照。 //! -//! Owned immutable visual derivation; no managed VM handles. +//! model::snapshot/Block 快照提取构造 UiNode;render::layout/source_layout、绘制和命中流程读取它。 +//! 这里不持有 ObjectRef,因此 Arc 树可作为纯数据派生结果跨线程传递,不授权另一线程访问 VM。 +//! shallow_eq 由快照缓存判断能否复用旧 Arc;children/has_flex/allocates_children 供结构遍历和布局选择使用。 use std::sync::Arc; #[derive(Debug, Clone, PartialEq)] @@ -9,33 +11,46 @@ use std::sync::Arc; pub struct UiNode { // 只有页面根带兼容 Page 相机;其余字段是普通有限盒子的布局策略。 pub surface: Option, + // 兼容 flex 权重,父布局据此分配剩余主轴空间;不表示节点自身已经测量出的尺寸。 pub flex: f32, + // 显式 grow/shrink 等分配策略;与 flex 数值一起参与缓存相等性和父级分配判断。 pub flex_policy: crate::layout::FlexPolicy, + // 父容器主轴空白的对齐策略,与孩子排列次序和具体尺寸计算分开。 pub main_align: crate::layout::MainAlign, + // 容器为未指定 align_self 的孩子提供的交叉轴缺省策略。 pub cross_align: crate::layout::CrossAlign, + // 孩子对父 cross_align 的单独覆盖;None 表示继承,不能解释为禁止 stretch。 pub align_self: Option, + // 父布局分配时需要预留的外边距,不作为孩子内容绘制的本地原点偏移。 pub margin: crate::layout::Insets, + // 两轴的自然/固定/填充尺寸规则,由盒模型参与测量和最终安排。 pub sizing: crate::box_model::Sizing, // 单边可选覆盖值;None 表示沿用 kind 的统一 padding,不代表边距为零。 pub padding_edges: [Option; 4], pub border: crate::layout::Insets, + // 留原始颜色字符串供显示列表解码;颜色变更也必须使 shallow_eq 为 false。 pub border_color: String, + // 是否把后代限制在内容可视区域内,绘制/命中需要采用同一剪裁关系。 pub clip_children: bool, // 这些字符串只是派生身份/协议版本键,不是可以操作托管堆的 ObjectRef。 pub block_id: String, + // 源视觉协议相关的派生版本键;即使盒模型未变,协议变化仍可能要求重新测量或绘制。 pub protocol_key: String, // slot 保持组成身份;数组次序确定当前绘制/布局顺序,二者不是同一概念。 pub slots: Vec, // 从按钮、点击连接或自定义事件等能力推导的交互资格,供命中宽阶段使用,不代表最终 hitTest 必然成功。 pub interactive: bool, + // 标识原始 pointer 事件能力,与高层 click 的可交互标记分别参与输入分派。 pub pointer_events: bool, + // 具体叶/容器/场景内容,原生测量与软件绘制按此枚举选择规则。 pub kind: UiKind, } impl UiNode { - /// 按节点种类返回其视觉子项;叶节点使用空切片,避免为遍历另建组成。 + /// 布局、快照和命中结构遍历调用,返回已有孩子切片,不分配新列表或递归复制子树。 pub fn children(&self) -> &[Arc] { match &self.kind { + // 这些种类都显式包含 children;三维面板 UI 也复用普通 UiNode 子树遍历。 UiKind::Column { children, .. } | UiKind::Overlay { children, .. } | UiKind::Canvas { children, .. } @@ -45,11 +60,12 @@ impl UiNode { | UiKind::Row { children, .. } | UiKind::Button { children, .. } | UiKind::Scroll { children, .. } => children, + // Text/Rect/Spacer 没有视觉子树;返回静态空切片,调用方无需按种类再分支。 _ => &[], } } - /// 判断是否需要对子项执行弹性主轴分配。 + /// 父布局/allocates_children 查询此孩子是否需要分配空间;判断的是节点需求,不会在此实际改尺寸。 pub fn has_flex(&self) -> bool { // 不仅旧 flex 数值,非默认 FlexPolicy 或任一轴 Fill 也要求父节点执行分配,不能仅按自然尺寸排布。 self.flex > 0.0 @@ -58,17 +74,18 @@ impl UiNode { || self.sizing.height == crate::box_model::Dimension::Fill } - /// 判断当前节点是否会为子项指定实际分配范围,供源布局选择安排路径。 + /// source_layout 选择安排路径时调用,检查任一孩子的弹性需求或交叉轴拉伸是否要求明确分配。 pub fn allocates_children(&self) -> bool { // 任何孩子需要弹性或拉伸即进入分配路径;align_self 优先于父 cross_align。 self.children().iter().any(|child| { + // any 短路即可确定是否需要分配;align_self 优先级高于容器 cross_align。 child.has_flex() || child.align_self.unwrap_or(self.cross_align) == crate::layout::CrossAlign::Stretch }) } - /// 比较节点自身配置和直接子项身份,决定能否复用旧 Arc;不是深拷贝业务图。 + /// model::snapshot 缓存重建节点后调用,判断可否保留旧 Arc;返回 true 不会修改任何节点或 VM 对象。 pub fn shallow_eq(&self, other: &Self) -> bool { // 先比较所有公共视觉/协议配置,任一变化都不能复用旧节点 Arc,即使子内容完全相同。 if self.surface != other.surface @@ -89,6 +106,7 @@ impl UiNode { || self.interactive != other.interactive || self.pointer_events != other.pointer_events { + // 共有字段任一不同都足以否决复用,不再比较 kind 或递归访问后代。 return false; } // 直接孩子按 Arc 指针比较而不是递归深比较;依赖提取保证未变子树复用 Arc,父级只需验证其身份。 @@ -102,6 +120,7 @@ impl UiNode { }; // 同类变体再比较其专有数据和直接子项;变体不同必为 false,避免布局种类切换后误复用缓存。 match (&self.kind, &other.kind) { + // a/b 与 d/e 是尺寸,c/f 是场景视图,g/h 是控制开关,i/j 是面板尺寸;孩子还需同身份。 ( UiKind::Viewport3D { width: a, @@ -120,6 +139,7 @@ impl UiNode { .. }, ) => a == d && b == e && c == f && g == h && i == j && same_children(), + // 二维视口除了宽高还比较相机快照;相机变了即使场景孩子没变也不能复用旧节点。 ( UiKind::CanvasViewport { width: a, @@ -135,14 +155,18 @@ impl UiNode { }, ) => a == d && b == e && c == f && same_children(), (UiKind::Scene2D { positions: a, .. }, UiKind::Scene2D { positions: b, .. }) => { + // 显式世界位置和孩子身份一起决定组成,不能因同一批对象只移动位置而漏掉刷新。 a == b && same_children() } (UiKind::Overlay { padding: a, .. }, UiKind::Overlay { padding: b, .. }) => { + // Overlay 没有 gap/累计主轴位置,专有属性只比 padding,再检查孩子序列。 a == b && same_children() } (UiKind::Canvas { positions: a, .. }, UiKind::Canvas { positions: b, .. }) => { + // 兼容 Page Canvas 与 Scene2D 相同地保留显式位置,但仍要求两边变体相同。 a == b && same_children() } + // 文本叶没有孩子,内容、字号和颜色相等即可;共有字段已在上面验证。 ( UiKind::Text { value: a, @@ -155,6 +179,7 @@ impl UiNode { color: cb, }, ) => a == b && sa == sb && ca == cb, + // 矩形依次比较宽、高、圆角和颜色,不以外框尺寸相同掩盖绘制变化。 ( UiKind::Rect { width: a, @@ -169,7 +194,9 @@ impl UiNode { color: h, }, ) => (a, b, c, d) == (e, f, g, h), + // Spacer 唯一专有属性是占位长度。 (UiKind::Spacer(a), UiKind::Spacer(b)) => a == b, + // 滚动偏移属于 Mount,此处只比轴/有限尺寸/内容规则及孩子,避免共享节点携带单次出现偏移。 ( UiKind::Scroll { axis: aa, @@ -188,6 +215,7 @@ impl UiNode { children: _, }, ) => aa == ab && a == c && b == d && pa == pb && ga == gb && same_children(), + // 这三种容器共用 padding/gap 比较,但模式配对仍只允许 Column 对 Column 等同种比较。 ( UiKind::Column { padding: a, gap: b, .. @@ -212,6 +240,7 @@ impl UiNode { padding: c, gap: d, .. }, ) => a == c && b == d && same_children(), + // 任何不同变体都不能复用,哪怕专有数据恰巧具有相同数值。 _ => false, } } @@ -222,27 +251,36 @@ impl UiNode { pub enum UiKind { // 有限 3D 视口包含不可变场景视图和面板 UI 孩子;panels 与 children/slots 按顺序对应。 Viewport3D { + // 有限布局盒子尺寸,不是三维世界的几何范围。 width: f32, height: f32, + // 已提取的场景、相机和投影数据,render 不需要为每个像素回读 VM。 view: Arc, + // 是否存在可用控制器,输入宽阶段据此判断相机手势能力。 controls: bool, + // 各世界 UI 面板的逻辑尺寸,与 children 次序对应,供投影与布局共用。 panels: Vec, + // 面板 UI 的普通二维树;业务对象身份仍由 UiNode.block_id/slots 描述。 children: Vec>, }, // 二维世界组成保留孩子的显式位置,不使用行列的累计偏移定位。 Scene2D { + // 每个孩子的世界 x/y,数组下标与 children 对齐,不按叠放顺序推算位置。 positions: Vec<[f32; 2]>, children: Vec>, }, // 有限布局盒子观察独立 Scene2D,相机仅影响投影,不改变场景节点原始逻辑位置。 CanvasViewport { + // 参加父布局的有限盒子尺寸;不是 Scene2D 孩子的总包围盒。 width: f32, height: f32, + // 纯数据相机快照,只影响本视口的世界到屏幕映射,不改写原世界树。 camera: crate::viewport::CameraView, children: Vec>, }, // 孩子重叠布置,顺序同时决定叠放次序;普通有限布局与世界 Canvas 不同。 Overlay { + // 统一内边距,单边覆盖仍由 UiNode.padding_edges 提供。 padding: f32, children: Vec>, }, @@ -259,16 +297,21 @@ pub enum UiKind { }, // 文字叶节点保留源字号等级与颜色文本;排版及最终设备比例由后续布局/显示列表转换处理。 Text { + // 已读取的文本内容,字体测量与软件绘制使用同一字符串。 value: String, + // 宿主字号等级,实际字体像素大小由 text/布局映射规则确定。 size: u32, + // 源颜色格式已在提取路径验证,绘制阶段再转成原生颜色值。 color: String, }, + // 纯绘制叶节点;几何尺寸与圆角供布局/显示列表共用,不含托管对象句柄。 Rect { width: f32, height: f32, radius: f32, color: String, }, + // Column 沿纵轴累加孩子位置,Row 沿横轴;二者都保存统一内边距和孩子间距。 Column { padding: f32, gap: f32, @@ -283,6 +326,7 @@ pub enum UiKind { Spacer(f32), // 滚动盒保存有限分配尺寸、允许轴和普通内容;实际偏移按 Mount 出现分别维护,不放到共享节点里。 Scroll { + // axis 指定允许消费的滚动方向;实际可消费距离还取决于具体 Mount 的内容/视口范围。 axis: crate::scroll::Axis, width: f32, height: f32,