//! 托管 Window 的页面所有权、原生命令与关闭生命周期。 //! 源码 resize/focus/setTitle 先产生纯数据命令;只有所属输入成功发布后,GUI 线程才应用它们。 //! 窗口 ID 和运行时 session 共同限定资源归属;切换 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}; use endor_core::{ ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef, ObjectStore, Value, Vm, VmError, }; use std::sync::{Arc, Mutex}; pub const MAIN_WINDOW: u64 = 1; pub const MAX_WINDOW_DIMENSION: f64 = 4096.0; #[derive(Debug, Clone, PartialEq)] /// 可投影到原生窗口的动作类别,如尺寸、标题、焦点与关闭。 pub enum Action { Resize { width: f64, height: f64 }, SetTitle(String), MoveTo { x: i32, y: i32 }, Focus, Close, } #[derive(Debug, Clone, PartialEq)] /// 成功发布后由 GUI 执行的一条原生窗口请求,含资源身份和动作顺序。 pub struct Command { pub window: u64, pub sequence: u64, pub action: Action, } impl Command { /// 判断两条命令是否针对同一资源的同类动作,用于替换尚未执行的旧请求。 pub fn same_kind(&self, other: &Self) -> bool { // 比较动作变体而不是参数值:同窗口多次 Resize/SetTitle 可合并为最后一次,不同动作仍按序保留。 self.window == other.window && std::mem::discriminant(&self.action) == std::mem::discriminant(&other.action) } } #[derive(Debug, Clone)] /// 附带运行时 session 的命令,阻止旧域请求在新 VM 上生效。 pub struct Envelope { pub session: u64, pub command: Command, } #[derive(Debug, Clone)] /// 托管窗口资源状态与页面拥有关系;本对象存在不等于 OS 窗口已创建。 pub struct ManagedWindow { pub id: u64, // 每窗口拥有独立 desktop、tabs 与页面顺序;同 VM 的类型注册表可共享,不复制业务模型。 pub desktop: ObjectRef, pub closed: bool, pub tasks: Option, tabs: ObjectRef, // 页面身份的有序强拥有集合,tabs 字典同时提供托管注册入口;移动顺序只调整此 Vec。 pages: Vec, sequence: u64, // Transport state only. Draining it must not change a language object's // version or create a render-dependency feedback loop. // 仅保存纯数据传输队列,克隆 payload 仍共享同一 outbox;drain 不触发 VM 内容版本,避免反馈循环。 outbox: Arc>>, } impl ObjectPayload for ManagedWindow { /// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。 fn as_any(&self) -> &dyn std::any::Any { self } /// 返回运行时诊断/类型检查使用的稳定 payload 名称。 fn kind_name(&self) -> &'static str { "tessera-window" } /// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。 fn allows_attributes(&self) -> bool { true } /// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。 fn reference_assignment_only(&self) -> bool { true } /// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。 fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { visit(self.desktop); if let Some(tasks) = self.tasks { visit(tasks); } visit(self.tabs); for page in &self.pages { visit(*page); } } } /// 统一构造本模块的参数错误,让调用方停止处理无效输入。 fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } /// 把源数值转换为有限屏幕坐标,拒绝越界或不可表示的值。 fn screen_coordinate(value: &Value) -> Result { let Value::Number(value) = value else { return Err(invalid("screen position requires integer coordinates")); }; // 屏幕位置必须为整数,允许负坐标以覆盖多显示器;范围与 i32 转换均检查,不能把小数直接截断。 value .as_i64() .filter(|number| (-1_000_000..=1_000_000).contains(number)) .and_then(|number| i32::try_from(number).ok()) .ok_or(invalid("screen position exceeds limits")) } /// 从源 Value 取得窗口操作需要的数值,保留错误类型诊断。 fn scalar(value: &Value) -> Result { let Value::Number(value) = value else { return Err(invalid("window size requires numbers")); }; let number = value .to_string() .parse::() .map_err(|_| invalid("invalid window size"))?; if !number.is_finite() || !(1.0..=MAX_WINDOW_DIMENSION).contains(&number) { return Err(invalid("window size exceeds limits")); } Ok(number) } #[derive(Debug, Clone, Copy)] /// 当前宿主 API 的操作分类,分派代码负责参数校验与状态更新。 enum Operation { Id, Viewport, Pages, Resize, MoveTo, SetTitle, Focus, } #[derive(Debug)] /// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。 struct Method { signature: endor_core::CallSignature, operation: Operation, desktop: ObjectRef, } impl NativeCallable for Method { /// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。 fn signature(&self) -> Option { Some(self.signature.clone()) } /// 计入 Window 接收者:resize/moveTo 共三参,setTitle 两参,其他查询/关闭/聚焦只要接收者。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(match self.operation { Operation::Resize | Operation::MoveTo => 3, Operation::SetTitle => 2, _ => 1, })) } /// 验证窗口资源与关闭状态;查询返回托管值,原生变更生成受世代保护的待执行命令,关闭走资源生命周期。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let Some(Value::Object(owner)) = args.first() else { return Err(invalid("Window method requires a Window")); }; let data = cx .store() .payload::(*owner, "Window")? .clone(); // 已关闭窗口仍可查询稳定 ID 与页面集合,其他读取/操作拒绝,避免用旧 viewport 或重开原生资源。 if data.closed && !matches!(self.operation, Operation::Id | Operation::Pages) { return Err(invalid("native window is closed")); } 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")); } let result = match self.operation { Operation::Id => Value::Number((data.id as i64).into()), Operation::Pages => { Value::Object(object_list::collection(cx, data.pages, self.desktop)?) } // 读取该 Window 自己 desktop 的逻辑视口,封装成 MeasureResult;尚未由原生侧提供尺寸时明确失败。 Operation::Viewport => { let Value::Object(input) = cx.store().get_attr(data.desktop, "viewport")? else { return Err(invalid("window viewport is unavailable")); }; let width = cx.store().get_attr(input, "width")?; let height = cx.store().get_attr(input, "height")?; Value::Object(native_visual::record( cx, self.desktop, "MeasureResult", vec![("width", width), ("height", height)], )?) } operation => { // 原生命令虽暂存在内存,也属于变更,不能从只读测量/绘制回调发起窗口操作。 if cx.store().is_read_only() { return Err(invalid( "window operations are forbidden during read-only rendering", )); } let action = match operation { Operation::MoveTo => Action::MoveTo { x: screen_coordinate(&args[1])?, y: screen_coordinate(&args[2])?, }, Operation::Resize => Action::Resize { width: scalar(&args[1])?, height: scalar(&args[2])?, }, Operation::SetTitle => { let Value::Text(title) = &args[1] else { return Err(invalid("window title requires text")); }; // 标题限制 UTF-8 字节长度并拒绝控制字符;不会在这里执行标题文本或展开 shell 字符。 if title.len() > 4096 || title.chars().any(char::is_control) { return Err(invalid("invalid window title")); } Action::SetTitle(title.clone()) } Operation::Focus => Action::Focus, _ => unreachable!(), }; enqueue(cx, *owner, action)?; Value::Nil } }; Ok(NativeResult::Return(result)) } } /// 把合法窗口操作追加到所属窗口的待发布命令,不能直接在 VM 线程操作 OS。 pub fn enqueue( cx: &mut NativeContext<'_>, owner: ObjectRef, action: Action, ) -> Result<(), VmError> { let mut data = cx .store() .payload::(owner, "Window")? .clone(); // 命令序列单调递增且检查溢出;同类合并会产生序号空洞,但仍可按序比较先后。 data.sequence = data .sequence .checked_add(1) .ok_or(invalid("window command sequence exhausted"))?; let command = Command { window: data.id, sequence: data.sequence, action, }; let queue = data.outbox.clone(); let mut queue = queue .lock() .map_err(|_| invalid("window outbox poisoned"))?; // The heap barrier succeeds before any external transport state changes. // 必须先成功经过 VM 写屏障,才修改不可回滚的原生 outbox;只读调用不能先留下待发命令再报错。 cx.replace_payload(owner, data)?; // 同窗口同动作只保留最后请求,其他类别保持先后;此处只入队,GUI 是否执行取决于后续成功发布。 queue.retain(|old| !old.same_kind(&command)); queue.push(command); Ok(()) } /// 将页面登记到其窗口,维护托管所有权与页面顺序。 pub fn register_page( cx: &mut NativeContext<'_>, window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { if cx.store().is_read_only() { return Err(invalid( "opening a Page is forbidden during read-only rendering", )); } let mut data = cx .store() .payload::(window, "Window")? .clone(); if data.closed { return Err(invalid("native window is closed")); } // 登记幂等,首次同时写 tabs 与 payload;字典键使用页面身份文本而非标题,重命名不改变登记。 if !data.pages.contains(&page) { data.pages.push(page); cx.dict_set( data.tabs, Value::Text(page.to_string()), Value::Object(page), )?; cx.replace_payload(window, data)?; } Ok(()) } /// 从窗口的页面集合撤销指定页面,保持后续展示索引一致。 pub fn unregister_page( cx: &mut NativeContext<'_>, 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")? .clone(); // 不存在时撤销是空操作;存在时同时删有序列表与 tabs 字典边,不能只隐藏原生标签。 if !data.pages.contains(&page) { return Ok(()); } data.pages.retain(|value| *value != page); cx.dict_delete(data.tabs, &Value::Text(page.to_string()))?; cx.replace_payload(window, data)?; Ok(()) } /// Discard an unpublished Page registration without invoking guest callbacks. /// 宿主收尾路径撤销页面登记,用于已经由外层决定的关闭/退役流程。 pub fn unregister_page_host( vm: &mut Vm, window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { let mut data = vm.payload::(window, "Window")?.clone(); if data.pages.contains(&page) { data.pages.retain(|value| *value != page); vm.dict_delete(data.tabs, &Value::Text(page.to_string()))?; vm.replace_payload(window, data)?; } Ok(()) } /// 在同一窗口的已登记页面集合内重排标签位置,保留 Page 身份;不负责跨窗口迁移。 pub fn move_page( cx: &mut NativeContext<'_>, window: ObjectRef, page: ObjectRef, position: usize, ) -> Result<(), VmError> { let mut data = cx .store() .payload::(window, "Window")? .clone(); if data.closed { return Err(invalid("native window is closed")); } let from = data .pages .iter() .position(|candidate| *candidate == page) .ok_or(invalid("Page is not open in this window"))?; 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", )); } // 只重排当前窗口中的页面,不迁移到另一窗口,也不销毁或重建 Page/Mount;跨窗口迁移由桥接层组合操作。 if from != position { let page = data.pages.remove(from); data.pages.insert(position, page); cx.replace_payload(window, data)?; } Ok(()) } /// 借用一个有效托管窗口的已登记页面集合。 pub fn pages(store: &ObjectStore, window: ObjectRef) -> Result<&[ObjectRef], VmError> { Ok(&store.payload::(window, "Window")?.pages) } /// 取走当前窗口累积的命令,后续由成功发布结果携带到 GUI。 pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result, VmError> { let data = vm.payload::(window, "Window")?; let mut queue = data .outbox .lock() .map_err(|_| invalid("window outbox poisoned"))?; // 一次移走全部队列,再按序号排序;仅消费传输状态,不调用 replace_payload 或改变语言依赖版本。 let mut commands = std::mem::take(&mut *queue); commands.sort_by_key(|command| command.sequence); Ok(commands) } /// 把本模块的类型、方法或能力注册到所属运行时上下文。 pub fn install( vm: &mut Vm, roots: &mut Vec, desktop: ObjectRef, tabs: ObjectRef, ) -> Result { let definition = desktop_type(vm.object_store(), "Window", desktop)?; let function_type = desktop_type(vm.object_store(), "Function", desktop)?; for (name, operation) in [ ("id", Operation::Id), ("viewport", Operation::Viewport), ("pages", Operation::Pages), ("resize", Operation::Resize), ("moveTo", Operation::MoveTo), ("setTitle", Operation::SetTitle), ("focus", Operation::Focus), ] { use endor_core::{AnnotationType as T, CallEffect, CallSignature}; let (parameters, returns, effect) = match operation { Operation::Id => (vec![], T::Number, CallEffect::ReadOnly), Operation::Pages => ( vec![], T::Nominal(desktop_type(vm.object_store(), "ObjectList", desktop)?), CallEffect::ReadOnly, ), Operation::Viewport => ( vec![], T::Nominal(desktop_type(vm.object_store(), "MeasureResult", desktop)?), CallEffect::ReadOnly, ), 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), }; let mut arguments = vec![T::Nominal(definition)]; arguments.extend(parameters); let function = vm.allocate(ContextNativeFunction::new( name, Method { operation, desktop, signature: CallSignature::exact(arguments, returns, effect), }, vec![desktop], ))?; roots.push(function); vm.attach_type(function, function_type)?; vm.define_method(definition, name, function)?; } // 安装主窗口托管资源,原生 OS 窗口生命周期仍由 GUI 管理;页面集合初始为空。 let window = vm.allocate(ManagedWindow { id: MAIN_WINDOW, desktop, closed: false, tasks: None, tabs, pages: Vec::new(), sequence: 0, outbox: Arc::new(Mutex::new(Vec::new())), })?; roots.push(window); 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( windows, Value::Text(MAIN_WINDOW.to_string()), Value::Object(window), )?; vm.release(windows)?; Ok(window) } /// Create a managed native-window resource. The GUI creates the OS resource only /// after the owning candidate/input publishes its immutable projection. /// 建立托管窗口、desktop 与页面容器;原生窗口通过命令发布后出现。 pub fn create( cx: &mut NativeContext<'_>, master: ObjectRef, id: u64, definition: ObjectRef, ) -> Result<(ObjectRef, ObjectRef, ObjectRef), VmError> { if cx.store().is_read_only() { return Err(invalid( "window creation is forbidden during read-only rendering", )); } let desktop = cx.new_object()?; let tabs = cx.new_dict()?; // 附加桌面共享主域的同一 types 字典身份,保证跨窗口对象和协议使用同一名义类型体系。 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))?; let window = cx.allocate(ManagedWindow { id, desktop, closed: false, tasks: None, tabs, pages: Vec::new(), sequence: 0, outbox: Arc::new(Mutex::new(Vec::new())), })?; cx.attach_type(window, definition)?; cx.set_attr(desktop, "window", Value::Object(window))?; let Value::Object(windows) = cx.store().get_attr(master, "windows")? else { return Err(invalid("window registry missing")); }; cx.dict_set(windows, Value::Text(id.to_string()), Value::Object(window))?; // 返回创建的三个托管身份供桥接层建立 Presentation;此函数不直接申请 OS 资源或发起布局。 Ok((window, desktop, tabs)) } /// 取消窗口/页面任务并撤销登记,标记 closed 后安排原生关闭;重复关闭直接返回。 pub fn close(cx: &mut NativeContext<'_>, window: ObjectRef) -> Result<(), VmError> { if cx.store().is_read_only() { return Err(invalid( "window close is forbidden during read-only rendering", )); } let mut data = cx .store() .payload::(window, "Window")? .clone(); if data.closed { return Ok(()); } // 真实关闭边界先取消窗口与所有页面 owner 的任务,再撤销页面登记,防止晚到结果继续更新已关闭资源。 crate::tasks::cancel_owner(cx, window)?; for page in data.pages.clone() { crate::tasks::cancel_owner(cx, page)?; unregister_page(cx, window, page)?; } // 循环内 unregister_page 已替换过 Window payload,必须重读最新副本,不能用最初 data 把已移除页面写回。 data = cx .store() .payload::(window, "Window")? .clone(); data.closed = true; cx.replace_payload(window, data)?; // 标记托管 closed 后仍可由内部 enqueue 安排原生 Close;成功输入发布前不会直接关闭系统窗口。 enqueue(cx, window, Action::Close) } /// Host teardown for a stopped VM: no binding, dispatch or source execution. /// 撤销已失效窗口的托管资源关系,不重新执行源关闭请求。 pub fn retire(vm: &mut Vm, window: ObjectRef) -> Result<(), VmError> { let mut data = vm.payload::(window, "Window")?.clone(); // 故障域的宿主退役仅断开页面拥有边并标 closed,不跑用户回调、不重新生成源关闭命令。 vm.dict_clear(data.tabs)?; data.pages.clear(); data.closed = true; vm.replace_payload(window, data) }