520 lines
18 KiB
Rust
520 lines
18 KiB
Rust
//! 托管 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 {
|
|
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,
|
|
pub desktop: ObjectRef,
|
|
pub closed: bool,
|
|
pub tasks: Option<ObjectRef>,
|
|
tabs: ObjectRef,
|
|
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.
|
|
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"));
|
|
};
|
|
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())
|
|
}
|
|
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(match self.operation {
|
|
Operation::Resize | Operation::MoveTo => 3,
|
|
Operation::SetTitle => 2,
|
|
_ => 1,
|
|
}))
|
|
}
|
|
|
|
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
|
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();
|
|
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)?)
|
|
}
|
|
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"));
|
|
};
|
|
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.
|
|
cx.replace_payload(owner, data)?;
|
|
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"));
|
|
}
|
|
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();
|
|
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(())
|
|
}
|
|
|
|
/// 在窗口之间移动页面的所有权和登记顺序,保留同一业务页面身份。
|
|
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",
|
|
));
|
|
}
|
|
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"))?;
|
|
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)?;
|
|
}
|
|
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))?;
|
|
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()?;
|
|
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))?;
|
|
Ok((window, desktop, tabs))
|
|
}
|
|
|
|
/// 发起窗口关闭和相应生命周期清理,拒绝重复或不再有效的操作。
|
|
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(());
|
|
}
|
|
crate::tasks::cancel_owner(cx, window)?;
|
|
for page in data.pages.clone() {
|
|
crate::tasks::cancel_owner(cx, page)?;
|
|
unregister_page(cx, window, page)?;
|
|
}
|
|
data = cx
|
|
.store()
|
|
.payload::<ManagedWindow>(window, "Window")?
|
|
.clone();
|
|
data.closed = true;
|
|
cx.replace_payload(window, data)?;
|
|
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();
|
|
vm.dict_clear(data.tabs)?;
|
|
data.pages.clear();
|
|
data.closed = true;
|
|
vm.replace_payload(window, data)
|
|
}
|