Files
endorphin-tessera-gui/src/model/window.rs
T

640 lines
31 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 托管 Window 的页面所有权、原生命令与关闭生命周期。
//! 源码 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};
use endor_core::{
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef,
ObjectStore, Value, Vm, VmError,
};
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 {
/// 判断两条命令是否针对同一资源的同类动作,用于替换尚未执行的旧请求。
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 {
// 产生该命令的运行时世代,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<ObjectRef>,
// 托管页面字典拥有关系,与 pages 顺序 Vec 一起维护;字典键为 Page 身份文本。
tabs: ObjectRef,
// 页面身份的有序强拥有集合,tabs 字典同时提供托管注册入口;移动顺序只调整此 Vec。
pages: Vec<ObjectRef>,
// 最近已入队动作的单调序号,enqueue 先检查溢出再替换 payload。
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<Mutex<Vec<Command>>>,
}
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)) {
// 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);
}
}
}
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
fn invalid(message: &'static str) -> VmError {
VmError::InvalidArgument(message)
}
/// 把源数值转换为有限屏幕坐标,拒绝越界或不可表示的值。
fn screen_coordinate(value: &Value) -> Result<i32, VmError> {
// moveTo 参数要求整数 Number,不把文本屏幕位置或小数隐式截断。
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<f64, VmError> {
// resize 每轴要求 Number,错误类型在进入原生命令队列前拒绝。
let Value::Number(value) = value else {
return Err(invalid("window size requires numbers"));
};
// 先转 f64 再检查有限性和逻辑窗口尺寸范围,超界不会在 OS 侧被静默钳制。
let number = value
.to_string()
.parse::<f64>()
.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"));
}
Ok(number)
}
#[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 {
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
/// 计入 Window 接收者:resize/moveTo 共三参,setTitle 两参,其他查询/聚焦只要接收者。
fn arity(&self) -> Option<endor_core::CallArity> {
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<NativeResult, VmError> {
// 未绑定原生方法从首参取得真正 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::<ManagedWindow>(*owner, "Window")?
.clone();
// 已关闭窗口仍可查询稳定 ID 与页面集合,其他读取/操作拒绝,避免用旧 viewport 或重开原生资源。
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,
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",
));
}
// 参数全部解码成功后才 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"));
};
// 标题限制 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!(),
};
// 所有正常方法请求经同一排队屏障,调用成功仍不代表 GUI 已执行。
enqueue(cx, *owner, action)?;
Value::Nil
}
};
Ok(NativeResult::Return(result))
}
}
/// 把合法窗口操作追加到所属窗口的待发布命令,不能直接在 VM 线程操作 OS。
pub fn enqueue(
cx: &mut NativeContext<'_>,
owner: ObjectRef,
action: Action,
) -> Result<(), VmError> {
// 内部关闭也可经此入口入队 Close;此函数不重复做公开方法的 closed 检查。
let mut data = cx
.store()
.payload::<ManagedWindow>(owner, "Window")?
.clone();
// 命令序列单调递增且检查溢出;同类合并会产生序号空洞,但仍可按序比较先后。
data.sequence = data
.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"))?;
// 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> {
// 登记会改变 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::<ManagedWindow>(window, "Window")?
.clone();
// 关闭资源不能重新登记页面,避免旧窗口被页面 open 隐式复活。
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::<ManagedWindow>(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> {
// 宿主已确定收尾时直接使用 VM 写屏障,不创建源调用或执行关闭回调。
let mut data = vm.payload::<ManagedWindow>(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()))?;
vm.replace_payload(window, data)?;
}
Ok(())
}
/// 在同一窗口的已登记页面集合内重排标签位置,保留 Page 身份;不负责跨窗口迁移。
pub fn move_page(
cx: &mut NativeContext<'_>,
window: ObjectRef,
page: ObjectRef,
position: usize,
) -> Result<(), VmError> {
// 只读取同窗口已登记 Page 集合,本函数不创建/迁移页面或安排新生命周期。
let mut data = cx
.store()
.payload::<ManagedWindow>(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",
));
}
// 只重排当前窗口中的页面,不迁移到另一窗口,也不销毁或重建 Page/Mount;跨窗口迁移由桥接层组合操作。
if from != position {
// 先从原位置移除同一引用,再插入最终位置;不分配 Page 副本或改 tabs 字典身份。
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> {
// 借用真实 payload 页顺序,调用方不能把返回切片跨 VM 修改保存。
Ok(&store.payload::<ManagedWindow>(window, "Window")?.pages)
}
/// 取走当前窗口累积的命令,后续由成功发布结果携带到 GUI。
pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result<Vec<Command>, VmError> {
// 读取 payload 只取得共享 transport outbox;drain 不应制造业务依赖写入。
let data = vm.payload::<ManagedWindow>(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<ObjectRef>,
desktop: ObjectRef,
tabs: ObjectRef,
) -> Result<ObjectRef, VmError> {
// 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),
("pages", Operation::Pages),
("resize", Operation::Resize),
("moveTo", Operation::MoveTo),
("setTitle", Operation::SetTitle),
("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 {
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> {
// 源 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,
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))?;
// 统一注册表必须由 master 提供,缺失说明运行时安装不完整,不能私自创建孤立窗口集合。
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> {
// 关闭会取消任务并撤销 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::<ManagedWindow>(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)?;
}
// 循环内 unregister_page 已替换过 Window payload,必须重读最新副本,不能用最初 data 把已移除页面写回。
data = cx
.store()
.payload::<ManagedWindow>(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> {
// 已停止域的宿主路径只解除托管页面拥有边并标记 closed,任务/OS 清理由外层退役流程负责。
let mut data = vm.payload::<ManagedWindow>(window, "Window")?.clone();
// 故障域的宿主退役仅断开页面拥有边并标 closed,不跑用户回调、不重新生成源关闭命令。
vm.dict_clear(data.tabs)?;
data.pages.clear();
data.closed = true;
vm.replace_payload(window, data)
}