diff --git a/src/model/window.rs b/src/model/window.rs index b68c2e7..d9a4952 100644 --- a/src/model/window.rs +++ b/src/model/window.rs @@ -2,6 +2,10 @@ //! 源码 resize/focus/setTitle 先产生纯数据命令;只有所属输入成功发布后,GUI 线程才应用它们。 //! 窗口 ID 和运行时 session 共同限定资源归属;切换 VM 或关闭窗口时不能让旧命令作用到新资源。 //! +//! 调用路径:bootstrap 调用 install 建立主窗口;桥接新窗口/页面 API 调用 create、登记、重排与 close。 +//! ReplSession 收集 take_commands 后,由 API worker 为成功发布的命令附 session,GUI 再执行对应 OS 操作。 +//! 生命周期退役使用 retire/unregister_page_host 的宿主入口,不借已停止 VM 重跑源回调。 +//! //! Managed window resources and versioned, coalesced native projections. //! The VM owns Page roots; only scalar commands cross to the GUI thread. use crate::{native_visual, object_list, ui_bridge::desktop_type}; @@ -11,22 +15,32 @@ use endor_core::{ }; use std::sync::{Arc, Mutex}; +// 主窗口的资源 ID 固定为 1,壳可在 VM 替换后继续存在;真正命令归属还要检查 session。 pub const MAIN_WINDOW: u64 = 1; +// 源 resize 的每轴逻辑尺寸上限,独立于世界场景坐标和软件渲染像素预算。 pub const MAX_WINDOW_DIMENSION: f64 = 4096.0; #[derive(Debug, Clone, PartialEq)] /// 可投影到原生窗口的动作类别,如尺寸、标题、焦点与关闭。 pub enum Action { + // 纯数据逻辑宽高请求,不会从 VM 线程直接调用 OS resize。 Resize { width: f64, height: f64 }, + // 完整新标题文本,同类请求合并时取最后一次。 SetTitle(String), + // 屏幕整数坐标,允许负数支持多显示器布局。 MoveTo { x: i32, y: i32 }, + // 请求操作系统聚焦窗口,区别于某个 Mount 的页面焦点。 Focus, + // 源关闭流程完成后的原生退役请求,旧 session 不得关闭新资源。 Close, } #[derive(Debug, Clone, PartialEq)] /// 成功发布后由 GUI 执行的一条原生窗口请求,含资源身份和动作顺序。 pub struct Command { + // 目标原生资源 ID,与外层 Envelope.session 一起确定归属。 pub window: u64, + // 单窗口动作先后序号,合并可产生空洞但不得反向执行。 pub sequence: u64, + // 请求内容只含可离开 VM 的数值/文本,不含 ObjectRef。 pub action: Action, } impl Command { @@ -40,20 +54,27 @@ impl Command { #[derive(Debug, Clone)] /// 附带运行时 session 的命令,阻止旧域请求在新 VM 上生效。 pub struct Envelope { + // 产生该命令的运行时世代,API 发布状态切换 VM 时丢弃旧世代请求。 pub session: u64, + // 已合并排序的窗口动作,GUI 先核对世代再作用到 OS 窗口。 pub command: Command, } #[derive(Debug, Clone)] /// 托管窗口资源状态与页面拥有关系;本对象存在不等于 OS 窗口已创建。 pub struct ManagedWindow { + // VM 内窗口资源 ID,创建托管对象不代表同 ID OS 窗口已经完成创建。 pub id: u64, // 每窗口拥有独立 desktop、tabs 与页面顺序;同 VM 的类型注册表可共享,不复制业务模型。 pub desktop: ObjectRef, + // 逻辑关闭标志,阻止继续操作/登记页面;id/pages 查询仍允许。 pub closed: bool, + // 窗口 owner 的任务作用域强引用,真实关闭时先取消再退役页面。 pub tasks: Option, + // 托管页面字典拥有关系,与 pages 顺序 Vec 一起维护;字典键为 Page 身份文本。 tabs: ObjectRef, // 页面身份的有序强拥有集合,tabs 字典同时提供托管注册入口;移动顺序只调整此 Vec。 pages: Vec, + // 最近已入队动作的单调序号,enqueue 先检查溢出再替换 payload。 sequence: u64, // Transport state only. Draining it must not change a language object's // version or create a render-dependency feedback loop. @@ -79,11 +100,15 @@ impl ObjectPayload for ManagedWindow { } /// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。 fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { + // GC 追踪独立桌面,保证挂载字典与待展示状态属于可见托管拥有图。 visit(self.desktop); + // 只追踪存在的任务作用域,任务自己的图再由任务 payload 暴露。 if let Some(tasks) = self.tasks { visit(tasks); } + // tabs 字典与 pages 都是真实拥有边,不能仅在宿主 Vec 保存句柄而漏掉引用追踪。 visit(self.tabs); + // 依序强持有全部登记 Page,不只保留当前活动页;重排不改变这些对象身份。 for page in &self.pages { visit(*page); } @@ -95,6 +120,7 @@ fn invalid(message: &'static str) -> VmError { } /// 把源数值转换为有限屏幕坐标,拒绝越界或不可表示的值。 fn screen_coordinate(value: &Value) -> Result { + // moveTo 参数要求整数 Number,不把文本屏幕位置或小数隐式截断。 let Value::Number(value) = value else { return Err(invalid("screen position requires integer coordinates")); }; @@ -107,13 +133,16 @@ fn screen_coordinate(value: &Value) -> Result { } /// 从源 Value 取得窗口操作需要的数值,保留错误类型诊断。 fn scalar(value: &Value) -> Result { + // resize 每轴要求 Number,错误类型在进入原生命令队列前拒绝。 let Value::Number(value) = value else { return Err(invalid("window size requires numbers")); }; + // 先转 f64 再检查有限性和逻辑窗口尺寸范围,超界不会在 OS 侧被静默钳制。 let number = value .to_string() .parse::() .map_err(|_| invalid("invalid window size"))?; + // 每轴至少为 1、最多 4096,NaN/无穷同样非法。 if !number.is_finite() || !(1.0..=MAX_WINDOW_DIMENSION).contains(&number) { return Err(invalid("window size exceeds limits")); } @@ -122,19 +151,29 @@ fn scalar(value: &Value) -> Result { #[derive(Debug, Clone, Copy)] /// 当前宿主 API 的操作分类,分派代码负责参数校验与状态更新。 enum Operation { + // 返回稳定资源数值身份,窗口已关闭后仍可查询。 Id, + // 读取此窗口 desktop 的当前逻辑视口,未提供尺寸时失败。 Viewport, + // 返回按登记顺序的新对象列表,成员仍为原 Page 引用。 Pages, + // 排队修改原生逻辑尺寸,读协议期间禁止。 Resize, + // 排队移动 OS 窗口到带符号整数屏幕位置。 MoveTo, + // 排队更新标题,按字节限长并拒绝控制字符。 SetTitle, + // 排队原生聚焦;不会直接选择某个页面 Mount。 Focus, } #[derive(Debug)] /// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。 struct Method { + // 安装时从当前桌面类型构造的未绑定方法契约,首个参数是 Window receiver。 signature: endor_core::CallSignature, + // 统一适配器的行为分派,同时决定 arity 和结果/效果类型。 operation: Operation, + // 类型注册所在主桌面,由 native captures 保活;读取 viewport 时使用 receiver 自己的 desktop。 desktop: ObjectRef, } impl NativeCallable for Method { @@ -142,7 +181,7 @@ impl NativeCallable for Method { fn signature(&self) -> Option { Some(self.signature.clone()) } - /// 计入 Window 接收者:resize/moveTo 共三参,setTitle 两参,其他查询/关闭/聚焦只要接收者。 + /// 计入 Window 接收者:resize/moveTo 共三参,setTitle 两参,其他查询/聚焦只要接收者。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(match self.operation { Operation::Resize | Operation::MoveTo => 3, @@ -151,11 +190,13 @@ impl NativeCallable for Method { })) } - /// 验证窗口资源与关闭状态;查询返回托管值,原生变更生成受世代保护的待执行命令,关闭走资源生命周期。 + /// 验证窗口资源与关闭状态;查询返回托管值,原生变更生成待执行命令;世代封装与关闭生命周期由外层入口处理。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { + // 未绑定原生方法从首参取得真正 receiver,再验证 ManagedWindow payload。 let Some(Value::Object(owner)) = args.first() else { return Err(invalid("Window method requires a Window")); }; + // 复制当前资源状态,后续分配查询返回值时不持有 payload 借用。 let data = cx .store() .payload::(*owner, "Window")? @@ -164,25 +205,33 @@ impl NativeCallable for Method { if data.closed && !matches!(self.operation, Operation::Id | Operation::Pages) { return Err(invalid("native window is closed")); } + // 即使签名元数据已声明 arity,入口仍检查实际实参数,避免直接 native 调用越界。 let expected = match self.operation { Operation::Resize | Operation::MoveTo => 3, Operation::SetTitle => 2, _ => 1, }; + // 形状不符立即拒绝,不能读取缺少的宽高/标题实参。 if args.len() != expected { return Err(invalid("incorrect Window method argument count")); } + // 查询构造托管返回值;变更分支只排队命令,最终统一交 VM 返回协议处理。 let result = match self.operation { + // 关闭对象仍能返回原 ID,不能借查询 ID 来重开资源。 Operation::Id => Value::Number((data.id as i64).into()), + // 复制成员列表到新 ObjectList,成员身份和原登记顺序保持,原窗口集合不被修改。 Operation::Pages => { Value::Object(object_list::collection(cx, data.pages, self.desktop)?) } // 读取该 Window 自己 desktop 的逻辑视口,封装成 MeasureResult;尚未由原生侧提供尺寸时明确失败。 Operation::Viewport => { + // receiver 的逻辑 viewport 尚未由宿主提供时不构造假定默认尺寸。 let Value::Object(input) = cx.store().get_attr(data.desktop, "viewport")? else { return Err(invalid("window viewport is unavailable")); }; + // 逐项读取此窗口现有视口记录,MeasureResult 只包装已知尺寸。 let width = cx.store().get_attr(input, "width")?; + // 高度与宽度都来自 receiver.desktop,而非安装方法的主 desktop。 let height = cx.store().get_attr(input, "height")?; Value::Object(native_visual::record( cx, @@ -191,6 +240,7 @@ impl NativeCallable for Method { vec![("width", width), ("height", height)], )?) } + // 余下为原生资源变更,先检查当前只读范围再解码动作参数。 operation => { // 原生命令虽暂存在内存,也属于变更,不能从只读测量/绘制回调发起窗口操作。 if cx.store().is_read_only() { @@ -198,16 +248,20 @@ impl NativeCallable for Method { "window operations are forbidden during read-only rendering", )); } + // 参数全部解码成功后才 enqueue,非法标题或尺寸不会产生半个窗口动作。 let action = match operation { + // 两轴独立转受限屏幕整数,可负但不可有小数。 Operation::MoveTo => Action::MoveTo { x: screen_coordinate(&args[1])?, y: screen_coordinate(&args[2])?, }, + // 两轴独立验证有限逻辑尺寸,不把业务世界坐标范围用于 OS 窗口大小。 Operation::Resize => Action::Resize { width: scalar(&args[1])?, height: scalar(&args[2])?, }, Operation::SetTitle => { + // 标题必须是源文本,不能使用对象 str 等隐式转换触发额外源行为。 let Value::Text(title) = &args[1] else { return Err(invalid("window title requires text")); }; @@ -220,6 +274,7 @@ impl NativeCallable for Method { Operation::Focus => Action::Focus, _ => unreachable!(), }; + // 所有正常方法请求经同一排队屏障,调用成功仍不代表 GUI 已执行。 enqueue(cx, *owner, action)?; Value::Nil } @@ -233,6 +288,7 @@ pub fn enqueue( owner: ObjectRef, action: Action, ) -> Result<(), VmError> { + // 内部关闭也可经此入口入队 Close;此函数不重复做公开方法的 closed 检查。 let mut data = cx .store() .payload::(owner, "Window")? @@ -242,12 +298,15 @@ pub fn enqueue( .sequence .checked_add(1) .ok_or(invalid("window command sequence exhausted"))?; + // 把已验证动作与资源 ID/新序号组合为独立传输值。 let command = Command { window: data.id, sequence: data.sequence, action, }; + // 克隆 Arc 使 outbox 借用与即将 move 的 payload 分离;transport 不含托管根。 let queue = data.outbox.clone(); + // 队列锁毒化时报错,不继续修改 VM 版本或丢失原请求。 let mut queue = queue .lock() .map_err(|_| invalid("window outbox poisoned"))?; @@ -266,15 +325,18 @@ pub fn register_page( window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { + // 登记会改变 Page 拥有关系,不允许从只读布局/绘制回调产生可见资源副作用。 if cx.store().is_read_only() { return Err(invalid( "opening a Page is forbidden during read-only rendering", )); } + // 读取当前窗口页集合副本,后面以 payload 替换让 VM 正确更新拥有边。 let mut data = cx .store() .payload::(window, "Window")? .clone(); + // 关闭资源不能重新登记页面,避免旧窗口被页面 open 隐式复活。 if data.closed { return Err(invalid("native window is closed")); } @@ -296,11 +358,13 @@ pub fn unregister_page( window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { + // 撤销登记同样是业务图写入,只读协议中必须在删字典/列表前拒绝。 if cx.store().is_read_only() { return Err(invalid( "closing a Page is forbidden during read-only rendering", )); } + // 复制现有有序页集合,下面先检查目标确实在该窗口登记。 let mut data = cx .store() .payload::(window, "Window")? @@ -321,7 +385,9 @@ pub fn unregister_page_host( window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { + // 宿主已确定收尾时直接使用 VM 写屏障,不创建源调用或执行关闭回调。 let mut data = vm.payload::(window, "Window")?.clone(); + // 存在才同时移除顺序拥有边和 tabs 字典边,缺失则幂等返回。 if data.pages.contains(&page) { data.pages.retain(|value| *value != page); vm.dict_delete(data.tabs, &Value::Text(page.to_string()))?; @@ -337,21 +403,26 @@ pub fn move_page( page: ObjectRef, position: usize, ) -> Result<(), VmError> { + // 只读取同窗口已登记 Page 集合,本函数不创建/迁移页面或安排新生命周期。 let mut data = cx .store() .payload::(window, "Window")? .clone(); + // 已关闭窗口不能再重排其旧标签上下文。 if data.closed { return Err(invalid("native window is closed")); } + // 按原 Page 身份定位当前位置,名字/标题可能重复,不能据此识别页面。 let from = data .pages .iter() .position(|candidate| *candidate == page) .ok_or(invalid("Page is not open in this window"))?; + // position 是最终数组索引,不是允许等于 len 的插入缝。 if position >= data.pages.len() { return Err(invalid("tab position out of range")); } + // 验证来源/位置后,实际写入前仍要拦截只读范围。 if cx.store().is_read_only() { return Err(invalid( "tab ordering is forbidden during read-only rendering", @@ -359,6 +430,7 @@ pub fn move_page( } // 只重排当前窗口中的页面,不迁移到另一窗口,也不销毁或重建 Page/Mount;跨窗口迁移由桥接层组合操作。 if from != position { + // 先从原位置移除同一引用,再插入最终位置;不分配 Page 副本或改 tabs 字典身份。 let page = data.pages.remove(from); data.pages.insert(position, page); cx.replace_payload(window, data)?; @@ -367,11 +439,14 @@ pub fn move_page( } /// 借用一个有效托管窗口的已登记页面集合。 pub fn pages(store: &ObjectStore, window: ObjectRef) -> Result<&[ObjectRef], VmError> { + // 借用真实 payload 页顺序,调用方不能把返回切片跨 VM 修改保存。 Ok(&store.payload::(window, "Window")?.pages) } /// 取走当前窗口累积的命令,后续由成功发布结果携带到 GUI。 pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result, VmError> { + // 读取 payload 只取得共享 transport outbox;drain 不应制造业务依赖写入。 let data = vm.payload::(window, "Window")?; + // 锁内移出本轮全部动作,若毒化则保留错误,不假装没有窗口命令。 let mut queue = data .outbox .lock() @@ -388,8 +463,11 @@ pub fn install( desktop: ObjectRef, tabs: ObjectRef, ) -> Result { + // bootstrap 已建立名义 Window 类型,安装此类型的方法与主窗口实例。 let definition = desktop_type(vm.object_store(), "Window", desktop)?; + // 所有原生方法也附本域 Function 类型,使源回调/反射查询识别一致。 let function_type = desktop_type(vm.object_store(), "Function", desktop)?; + // 显式列出此适配器提供的方法;close 由桥接资源生命周期入口单独处理。 for (name, operation) in [ ("id", Operation::Id), ("viewport", Operation::Viewport), @@ -400,26 +478,32 @@ pub fn install( ("focus", Operation::Focus), ] { use endor_core::{AnnotationType as T, CallEffect, CallSignature}; + // 按操作声明参数、返回和效果,读查询可用于只读协议,资源变更均为 Mutating。 let (parameters, returns, effect) = match operation { Operation::Id => (vec![], T::Number, CallEffect::ReadOnly), + // pages 返回默认 ObjectList,其成员是同一 VM 的真实 Page。 Operation::Pages => ( vec![], T::Nominal(desktop_type(vm.object_store(), "ObjectList", desktop)?), CallEffect::ReadOnly, ), + // viewport 返回名义 MeasureResult,不能只声明 Any 让错误记录蒙混通过。 Operation::Viewport => ( vec![], T::Nominal(desktop_type(vm.object_store(), "MeasureResult", desktop)?), CallEffect::ReadOnly, ), + // 两数变更返回 Unit,显式标 Mutating 防止只读调用绕过入口检查。 Operation::Resize | Operation::MoveTo => { (vec![T::Number, T::Number], T::Unit, CallEffect::Mutating) } Operation::SetTitle => (vec![T::String], T::Unit, CallEffect::Mutating), Operation::Focus => (vec![], T::Unit, CallEffect::Mutating), }; + // 未绑定方法签名先放 Window receiver,再追加显式参数,与 Method.arity 一致。 let mut arguments = vec![T::Nominal(definition)]; arguments.extend(parameters); + // native 捕获桌面类型环境,真正 receiver 仍由调用实参提供,可服务同 VM 的副窗口。 let function = vm.allocate(ContextNativeFunction::new( name, Method { @@ -448,6 +532,7 @@ pub fn install( vm.attach_type(window, definition)?; vm.set_attr(desktop, "window", Value::Object(window))?; // 主 desktop 拥有统一窗口注册表,以纯数字 ID 文本索引;注册表让附加窗口在 VM 图内可达。 + // 主桌面强持有统一窗口字典,副窗口建立后也在这里保持可达。 let windows = vm.new_dict()?; vm.set_attr(desktop, "windows", Value::Object(windows))?; vm.dict_set( @@ -468,18 +553,23 @@ pub fn create( id: u64, definition: ObjectRef, ) -> Result<(ObjectRef, ObjectRef, ObjectRef), VmError> { + // 源 Window 构造也是资源变更,必须在分配新 desktop/tabs 前阻止只读协议调用。 if cx.store().is_read_only() { return Err(invalid( "window creation is forbidden during read-only rendering", )); } + // 每窗口独立桌面保存 viewport、mounts、请求等展示状态,业务名义类型仍与主域共享。 let desktop = cx.new_object()?; + // 副窗口页面字典从空集合开始,不复制主窗口的登记页面。 let tabs = cx.new_dict()?; // 附加桌面共享主域的同一 types 字典身份,保证跨窗口对象和协议使用同一名义类型体系。 + // 复制的是 Value::Object 身份,不克隆另一份类型体系,跨窗页面/回调仍使用同一名义类型。 let types = cx.store().get_attr(master, "types")?; cx.set_attr(desktop, "types", types)?; cx.set_attr(desktop, "viewport", Value::Nil)?; cx.set_attr(desktop, "tabs", Value::Object(tabs))?; + // 建立逻辑资源并附请求 Window 名义类型;此处只安排 VM 内可达图。 let window = cx.allocate(ManagedWindow { id, desktop, @@ -492,6 +582,7 @@ pub fn create( })?; cx.attach_type(window, definition)?; cx.set_attr(desktop, "window", Value::Object(window))?; + // 统一注册表必须由 master 提供,缺失说明运行时安装不完整,不能私自创建孤立窗口集合。 let Value::Object(windows) = cx.store().get_attr(master, "windows")? else { return Err(invalid("window registry missing")); }; @@ -502,20 +593,24 @@ pub fn create( /// 取消窗口/页面任务并撤销登记,标记 closed 后安排原生关闭;重复关闭直接返回。 pub fn close(cx: &mut NativeContext<'_>, window: ObjectRef) -> Result<(), VmError> { + // 关闭会取消任务并撤销 Page 强拥有边,必须先拒绝只读协议中的资源副作用。 if cx.store().is_read_only() { return Err(invalid( "window close is forbidden during read-only rendering", )); } + // 读取当前页集合做取消/撤销;循环内 payload 会多次更新,后面必须重读。 let mut data = cx .store() .payload::(window, "Window")? .clone(); + // 重复 close 幂等,不再次取消/撤销或生成重复原生 Close。 if data.closed { return Ok(()); } // 真实关闭边界先取消窗口与所有页面 owner 的任务,再撤销页面登记,防止晚到结果继续更新已关闭资源。 crate::tasks::cancel_owner(cx, window)?; + // 遍历旧页顺序副本,unregister_page 可以安全更新真实集合;每页 owner 任务先取消。 for page in data.pages.clone() { crate::tasks::cancel_owner(cx, page)?; unregister_page(cx, window, page)?; @@ -534,6 +629,7 @@ pub fn close(cx: &mut NativeContext<'_>, window: ObjectRef) -> Result<(), VmErro /// Host teardown for a stopped VM: no binding, dispatch or source execution. /// 撤销已失效窗口的托管资源关系,不重新执行源关闭请求。 pub fn retire(vm: &mut Vm, window: ObjectRef) -> Result<(), VmError> { + // 已停止域的宿主路径只解除托管页面拥有边并标记 closed,任务/OS 清理由外层退役流程负责。 let mut data = vm.payload::(window, "Window")?.clone(); // 故障域的宿主退役仅断开页面拥有边并标 closed,不跑用户回调、不重新生成源关闭命令。 vm.dict_clear(data.tabs)?;