648 lines
32 KiB
Rust
648 lines
32 KiB
Rust
//! 托管 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。
|
||
/// Method::invoke 先检查尺寸/标题/只读规则;工具激活和 close 也直接调用本入口,因此 closed 与动作参数并不在这里重验。
|
||
/// 返回成功只表示 outbox 已排队;worker 成功发布并封装 session 后,shell::app 才可能执行动作。
|
||
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 是否执行取决于后续成功发布。
|
||
// 例如 Resize(seq1)、Focus(seq2)、Resize(seq3) 会留下 Focus(seq2)、Resize(seq3),不能保留首次 Resize 的旧序号。
|
||
queue.retain(|old| !old.same_kind(&command));
|
||
queue.push(command);
|
||
Ok(())
|
||
}
|
||
|
||
/// 将页面登记到其窗口,维护托管所有权与页面顺序。
|
||
/// UiBridge::show_in 在调用前处理跨窗撤登记;本入口只保证该窗口内不重复,不负责查找或迁移其他窗口的同一 Page。
|
||
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。
|
||
/// 正常发布路径会封装返回值;故障清理路径也调用本函数但直接丢弃结果,所以 drain 本身不能视为发布成功。
|
||
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 与页面容器;桥接强制生成该窗口的首帧,GUI 根据成功发布的窗口投影创建 OS 资源。
|
||
/// Action 没有 Create 变体;这里返回的三元组由 ui_bridge::create_window 用于建立独立 Presentation。
|
||
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 的任务,再撤销页面登记,防止晚到结果继续更新已关闭资源。
|
||
// cancel_owner 只取消传入 owner 的作用域;具体 Mount 的任务随后在桥接刷新卸载出现时清理,不在此遍历 Mount 表。
|
||
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.
|
||
/// 撤销已失效窗口的托管资源关系,不重新执行源关闭请求。
|
||
/// UiBridge::shutdown/retire_window 先处理任务或 Mount,再调用这里断 Page 拥有边;不等同于源 close 的排队关闭流程。
|
||
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)
|
||
}
|