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

544 lines
22 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 或关闭窗口时不能让旧命令作用到新资源。
//!
//! 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<ObjectRef>,
tabs: ObjectRef,
// 页面身份的有序强拥有集合,tabs 字典同时提供托管注册入口;移动顺序只调整此 Vec。
pages: Vec<ObjectRef>,
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)) {
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<i32, VmError> {
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> {
let Value::Number(value) = value else {
return Err(invalid("window size requires numbers"));
};
let number = value
.to_string()
.parse::<f64>()
.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<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> {
let Some(Value::Object(owner)) = args.first() else {
return Err(invalid("Window method requires a Window"));
};
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"));
}
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::<ManagedWindow>(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::<ManagedWindow>(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::<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> {
let mut data = vm.payload::<ManagedWindow>(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::<ManagedWindow>(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::<ManagedWindow>(window, "Window")?.pages)
}
/// 取走当前窗口累积的命令,后续由成功发布结果携带到 GUI。
pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result<Vec<Command>, VmError> {
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> {
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::<ManagedWindow>(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::<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> {
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)
}