2623 lines
132 KiB
Rust
2623 lines
132 KiB
Rust
//! Endor 与 Tessera 的展示事务协调层:维护每个窗口的 Page、Mount、布局缓存和发布版本。
|
||
//! Session 保存托管对象的展示上下文;跨线程发布的是不可变 UiNode/Frame,而不是可以在窗口线程执行的 VM。
|
||
//! 刷新按 prepare → layout → paint → commit 推进。挂起工作必须恢复或显式取消;失败不能把未完成的几何和生命周期状态留作已发布结果。
|
||
//!
|
||
//! 调用来源有两条:runtime::endor_host 通过 UiBridge 安装环境、推进刷新与清理;
|
||
//! Endor VM 则在源程序调用 ui 工厂、类型构造器或实例方法时,进入本模块的 NativeCallable 实现。
|
||
//! Function/TypeMethod/NativeFactory 是语言调用适配,Session 保存跨调用的窗口展示状态;
|
||
//! 真正的排版/绘制交给 render,业务对象与 Mount 数据交给 model,均不在这里操作 OS 窗口。
|
||
//! 本模块会在 worker 中访问 VM;Arc/Mutex 只用于共享原生会话,不授权 GUI 线程直接调用 VM。
|
||
mod bootstrap;
|
||
mod calls;
|
||
mod developer_pages;
|
||
mod page_input;
|
||
mod signatures;
|
||
mod tree;
|
||
|
||
pub use bootstrap::install;
|
||
use signatures::{method_signature, operation_signature};
|
||
use std::sync::{Arc, Mutex};
|
||
|
||
use anyhow::{Result, bail};
|
||
use endor_core::{
|
||
AnnotationType, CallEffect, CallSignature, CallType, ContextNativeFunction, EndorReal,
|
||
EndorTypeObject, NativeCallable, NativeContext, NativeResult, ObjectRef, TypeConstructor,
|
||
Value, Vm, VmError, WeakObjectRef,
|
||
};
|
||
|
||
use crate::{
|
||
block::{self, Child, Data, Kind},
|
||
document::parse_color,
|
||
};
|
||
|
||
/// new_typed_block 的节点弱表上限,也用于容器工厂的变参数量约束,限制单会话宿主开销。
|
||
pub const MAX_UI_NODES: usize = 4096;
|
||
/// 新建 Page 时按存活对象检查的上限;输入解码也用它拒绝明显越界的页面索引。
|
||
pub const MAX_PAGES: usize = 64;
|
||
/// dimension 将语言数字转换成原生布局长度前检查的上界,避免异常尺寸流入后续绘图。
|
||
pub const MAX_DIMENSION: f64 = 1_000_000.0;
|
||
|
||
// 编译器已知宿主全局类型/能力名,同时用于 install_globals;仅列名不代表绕过实际 VM 名义类型查找。
|
||
pub const TYPE_NAMES: &[&str] = &[
|
||
"Object",
|
||
"Type",
|
||
"Function",
|
||
"Trait",
|
||
"Visual",
|
||
"EventTarget",
|
||
"Children",
|
||
"Presentable",
|
||
"Block",
|
||
"ContainerBlock",
|
||
"ScrollBlock",
|
||
"ScrollView",
|
||
"TextBlock",
|
||
"RectBlock",
|
||
"Spacer",
|
||
"Row",
|
||
"Column",
|
||
"Overlay",
|
||
"Scene2D",
|
||
"CanvasViewport",
|
||
"Camera2D",
|
||
"RayHit3D",
|
||
"Panel3D",
|
||
"CameraGesture",
|
||
"Vec3",
|
||
"Triangle3D",
|
||
"Geometry3D",
|
||
"Mesh3D",
|
||
"Box3D",
|
||
"Group3D",
|
||
"Scene3D",
|
||
"DirectionalLight",
|
||
"PerspectiveCamera",
|
||
"OrthographicCamera",
|
||
"OrbitController",
|
||
"Viewport3D",
|
||
"Button",
|
||
"Page",
|
||
"Window",
|
||
"Mount",
|
||
"Event",
|
||
"Subscription",
|
||
"Task",
|
||
"ObjectList",
|
||
"Constraints",
|
||
"MeasureResult",
|
||
"Bounds",
|
||
"Point",
|
||
"Arrangement",
|
||
"DisplayList",
|
||
"PaintCommand",
|
||
];
|
||
/// install_globals 暴露的类型/能力查询函数名称;与 bootstrap 安装到 ui 的入口对应。
|
||
pub const QUERY_NAMES: &[&str] = &[
|
||
"isInstance",
|
||
"isSubtype",
|
||
"implement",
|
||
"implements",
|
||
"supports",
|
||
];
|
||
|
||
pub use tree::{UiKind, UiNode};
|
||
|
||
#[derive(Debug)]
|
||
/// 整个 VM 的桥接会话。节点索引跨窗口共享,presentations 按窗口隔离派生状态;current 决定方法当前访问哪份展示上下文。
|
||
struct Session {
|
||
// new_typed_block 登记、reclaim 清理的弱引用表;不因宿主记账让已不可达的 Block 保活。
|
||
nodes: Vec<WeakObjectRef>,
|
||
// 每个窗口独立拥有 Mount、布局缓存和待发布画面;切换窗口只改变 current,不移动业务对象。
|
||
presentations: std::collections::BTreeMap<u64, crate::presentation::Presentation>,
|
||
// Deref/DerefMut 选择哪个 Presentation;调用前必须保证对应键仍在 presentations 中。
|
||
current: u64,
|
||
// 主 desktop 的托管身份;create_window 用它建立副窗口与主资源域的关系。
|
||
master: ObjectRef,
|
||
// 已分配窗口身份的高水位,关闭窗口后也不降低,避免旧事件误指向新窗口。
|
||
next_window: u64,
|
||
// 跨窗口共用 Mount 高水位,避免某窗口取消/重建后与其他窗口已暴露出现 ID 重合。
|
||
next_mount: u64,
|
||
// 正常关闭副窗口后暂存尚待发布的 Close 等命令;Presentation 已移除也不能丢掉最后的原生退役请求。
|
||
retired_commands: Vec<crate::window::Command>,
|
||
// 以下是供 stats 读取的累计工作量,并非控制下一次刷新的剩余预算。
|
||
refresh_yields: usize,
|
||
refresh_restarts: usize,
|
||
layout_cancels: usize,
|
||
paint_cancels: usize,
|
||
paint_slices: usize,
|
||
binding_evaluations: usize,
|
||
// shutdown 设置;create_window 等入口读取,阻止卸载回调在退出期间重新建立窗口。
|
||
exiting: bool,
|
||
}
|
||
|
||
impl Session {
|
||
/// 由刷新/依赖收集流程调用,返回所有窗口的宿主 desktop,排除宿主记账读取造成的无关失效。
|
||
fn host_desktops(&self) -> std::collections::HashSet<ObjectRef> {
|
||
// 只取值里的 desktop 而非窗口编号;依赖跟踪按托管 ObjectRef 判断读取归属。
|
||
self.presentations
|
||
.values()
|
||
.map(|view| view.desktop)
|
||
.collect()
|
||
}
|
||
}
|
||
|
||
impl std::ops::Deref for Session {
|
||
type Target = crate::presentation::Presentation;
|
||
/// Rust 在 state.desktop 等字段访问时隐式调用,借用 current 对应窗口的 Presentation。
|
||
fn deref(&self) -> &Self::Target {
|
||
// Session 解引用始终指向 current Presentation;调用源回调前切换窗口,结束后恢复主窗口,不能把它当全局唯一展示。
|
||
self.presentations
|
||
.get(&self.current)
|
||
.expect("selected presentation")
|
||
}
|
||
}
|
||
impl std::ops::DerefMut for Session {
|
||
/// Rust 在修改当前展示字段时隐式调用;调用方须先选定仍存在的窗口,不能跨窗口复用缓存。
|
||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||
// 与只读 Deref 使用相同键;缺项表示桥接选择/退役流程破坏了内部不变量,因此直接 expect。
|
||
self.presentations
|
||
.get_mut(&self.current)
|
||
.expect("selected presentation")
|
||
}
|
||
}
|
||
// 各原生函数 payload 共享同一个会话;Mutex 保护宿主字段,不是 VM 托管堆的并发访问许可。
|
||
type Shared = Arc<Mutex<Session>>;
|
||
|
||
#[derive(Debug, Clone, Copy)]
|
||
/// ui/组件宿主入口的操作枚举;统一由调用适配器分派,不直接代表已经发生的业务变更。
|
||
enum Operation {
|
||
IsInstance,
|
||
Implement,
|
||
Implements,
|
||
Supports,
|
||
IsSubtype,
|
||
OpenValue,
|
||
Mounts,
|
||
Stats,
|
||
Viewport,
|
||
Window,
|
||
PageWindow,
|
||
NewWindow,
|
||
CloseWindow,
|
||
Detach,
|
||
MoveWindow,
|
||
MoveTab,
|
||
State,
|
||
StateSet,
|
||
Increment,
|
||
TextOf,
|
||
Bind,
|
||
Text,
|
||
Rect,
|
||
Spacer,
|
||
Scroll,
|
||
Row,
|
||
Column,
|
||
Overlay,
|
||
Scene2D,
|
||
CanvasViewport,
|
||
Camera2D,
|
||
SceneObject(crate::scene3d_model::Constructor),
|
||
Viewport3D,
|
||
CameraControl,
|
||
Translate3D,
|
||
Rotate3D,
|
||
Button,
|
||
Page,
|
||
SetText,
|
||
SetColor,
|
||
Append,
|
||
Insert,
|
||
Remove,
|
||
Replace,
|
||
Move,
|
||
Children,
|
||
Render,
|
||
Unmount,
|
||
Open,
|
||
Close,
|
||
Activate,
|
||
Navigate,
|
||
Back,
|
||
Forward,
|
||
OnClick,
|
||
On,
|
||
Dispatch,
|
||
OnEvent,
|
||
Invalidate,
|
||
OnMount,
|
||
OnUnmount,
|
||
Click,
|
||
Present,
|
||
}
|
||
|
||
impl Operation {
|
||
/// 被 signatures、Function/TypeMethod/NativeFactory 的 arity 查询调用,提供统一显式参数范围。
|
||
/// 这里不计接收者、Type 首参和源子类字段;各适配器负责给范围加上自己的调用前缀。
|
||
fn arity(self) -> endor_core::CallArity {
|
||
use Operation::*;
|
||
// 此 arity 只含显式参数;TypeMethod 另加 receiver,NativeFactory 另加 Type 和子类字段外壳。
|
||
let (minimum, maximum) = match self {
|
||
// 类型/能力关系与属性绑定都需要两个显式操作数,不接收可选尾参。
|
||
IsInstance | IsSubtype | Bind | Implement | Implements | Supports => (2, 2),
|
||
// 这些查询所需上下文来自 Session 或绑定 owner,源调用不再传额外实参。
|
||
Stats | Viewport | Window | PageWindow | Mounts => (0, 0),
|
||
// 状态工厂接初值,绑定 set 接新值;接收者已存于 Function.owner。
|
||
State | StateSet => (1, 1),
|
||
// increment 的目标已绑定,递增步长由实现固定,不接用户步长参数。
|
||
Increment => (0, 0),
|
||
// 对象和属性名必填,字号/颜色可选;类型约束另见 operation_signature。
|
||
TextOf => (2, 4),
|
||
// 滚动块、二维与三维视口都要求两个尺寸加一个内容/场景对象。
|
||
Scroll => (3, 3),
|
||
CanvasViewport => (3, 3),
|
||
Viewport3D => (3, 3),
|
||
// 3D 族从共用构造元数据读取,避免这里维护另一份容易漂移的参数数量表。
|
||
SceneObject(kind) => kind.arity(),
|
||
// 控制更新接对象与手势,变换接三个分量;这些操作不使用任意长度变参。
|
||
CameraControl => (2, 2),
|
||
Translate3D | Rotate3D => (3, 3),
|
||
// 相机三个参数都可省略;场景孩子则可为空,仍受全局节点数量预算限制。
|
||
Camera2D => (0, 3),
|
||
Scene2D => (0, MAX_UI_NODES),
|
||
// 文本内容/矩形宽高必填,其余表现参数可选;不接容器子项尾参。
|
||
Text => (1, 3),
|
||
Rect => (2, 4),
|
||
// 单值操作复用同一数量规则,具体参数的类型和语义仍由各自签名与调用分支决定。
|
||
MoveTab | MoveWindow | OpenValue | Spacer | SetText | SetColor | Append | Remove
|
||
| Render | Navigate | OnClick | Present => (1, 1),
|
||
// 容器两个布局选项在前,后面最多 MAX_UI_NODES 个孩子;+2 不应错计入孩子预算。
|
||
Row | Column | Overlay => (2, MAX_UI_NODES + 2),
|
||
// 按钮接初始文本,容器插入/替换/移动接槽位或位置与另一个值。
|
||
Button => (1, 1),
|
||
Insert | Replace | Move => (2, 2),
|
||
// 页面/窗口状态操作和查询使用绑定 owner 或当前会话,无显式参数。
|
||
Page | Unmount | Close | Activate | Back | Forward | Children | NewWindow
|
||
| CloseWindow | Detach => (0, 0),
|
||
// open 可指定 Window;click 可指定 Mount;invalidate 可指定类别,其余走缺省语义。
|
||
Open => (0, 1),
|
||
Click | Invalidate => (0, 1),
|
||
// on 必填事件名和回调,可选捕获阶段;dispatch 必填事件,可选具体出现位置。
|
||
On => (2, 3),
|
||
Dispatch => (1, 2),
|
||
// 生命周期/事件钩子由宿主传一个协议上下文,用户覆盖必须遵守同一数量约定。
|
||
OnEvent | OnMount | OnUnmount => (1, 1),
|
||
};
|
||
// 同时保留必填下界和可选/变参上界,供 VM 在进入实际操作前拒绝参数数量错误。
|
||
endor_core::CallArity::range(minimum, maximum)
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
/// 将一个宿主 Operation 及会话环境封装成 VM 可调用对象。
|
||
struct Function {
|
||
// bootstrap 先留空、类型齐全后补入;方法 wrapper 已校验签名时可用 None 转交同一业务分派。
|
||
signature: Option<CallSignature>,
|
||
// calls::invoke 的分支选择值;名称只用于查询与诊断,不能替代这个明确的操作标签。
|
||
operation: Operation,
|
||
// 自由工厂为 None,绑定方法保存 receiver;调用参数切片中不再重复包含该 receiver。
|
||
owner: Option<ObjectRef>,
|
||
// 回调需要访问所属窗口的展示状态;托管强边另外由 ContextNativeFunction 的 captures 声明。
|
||
session: Shared,
|
||
// 原生构造的实际源子类身份;自由 ui 工厂为 None,类型构造器显式传入 Some。
|
||
requested_type: Option<ObjectRef>,
|
||
}
|
||
|
||
/// One shared implementation on the nominal type. The VM binds receivers;
|
||
/// individual Blocks no longer allocate owner-capturing method closures.
|
||
#[derive(Debug)]
|
||
/// 类型查询/能力方法的原生包装,保存调用所需的声明与会话。
|
||
struct TypeMethod {
|
||
// 包含 receiver 的完整签名,由 VM 在 native 调用边界检查。
|
||
signature: CallSignature,
|
||
// 验证接收者后转交 Function 分派的操作,不为每个 Block 复制一个函数实现。
|
||
operation: Operation,
|
||
// 声明此方法的名义类型;is_instance 允许真正的源子类,但拒绝无关对象。
|
||
expected: ObjectRef,
|
||
// 方法安装时捕获的桥接会话,决定 owner 操作使用的展示环境。
|
||
session: Shared,
|
||
}
|
||
|
||
/// 由 snapshot 的事件能力提取调用,判断 onEvent 是否仍为默认原生实现,区分源覆盖。
|
||
pub(crate) fn is_intrinsic_event(
|
||
store: &endor_core::ObjectStore,
|
||
function: ObjectRef,
|
||
) -> Result<bool, VmError> {
|
||
// 先检查 payload 是否为 ContextNativeFunction;源函数或其他 callable 不可能是默认 TypeMethod。
|
||
let Some(function) = store
|
||
.object(function)?
|
||
.payload()
|
||
.as_any()
|
||
.downcast_ref::<ContextNativeFunction>()
|
||
else {
|
||
// 类型不同即非内建实现;对象读取失败则由前面的 ? 返回错误,不能当作正常 false。
|
||
return Ok(false);
|
||
};
|
||
// 原生函数也可能是其他 UI 操作;只有 TypeMethod 包装的 OnEvent 才符合默认事件入口。
|
||
Ok(function
|
||
.implementation::<TypeMethod>()
|
||
.is_some_and(|method| matches!(method.operation, Operation::OnEvent)))
|
||
}
|
||
|
||
impl NativeCallable for TypeMethod {
|
||
/// VM 的 callable 签名查询入口,返回 bootstrap 按声明类型建立的完整 receiver/参数/返回契约。
|
||
fn signature(&self) -> Option<CallSignature> {
|
||
Some(self.signature.clone())
|
||
}
|
||
/// 在 Operation 的显式参数范围两端各加一,计入 VM 传入的隐式接收者。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
// operation 的范围不含 receiver;这里构造的是 VM 实际传入 args 的范围。
|
||
let arity = self.operation.arity();
|
||
Some(endor_core::CallArity::range(
|
||
arity.required + 1,
|
||
arity.maximum + 1,
|
||
))
|
||
}
|
||
|
||
/// VM 分派实例方法后调用;先校验 args[0],再把 receiver 交给 Function.owner 执行业务操作。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 名义方法统一存在类型上,VM 把 receiver 放首参;校验后剥离 receiver 再交 Function 的显式参数分派。
|
||
// 不假设源语法一定来自 obj.method:显式调用同样必须提供有效的对象首参。
|
||
let owner = object(args.first())?;
|
||
if !cx.store().is_instance(owner, self.expected)? {
|
||
// 拒绝错误接收者,不进入分派器,防止按某种 Block 布局读取无关 payload。
|
||
return Err(invalid("incorrect receiver type for visual method"));
|
||
}
|
||
// 使用临时适配器共享分派;原 wrapper 已提供签名,转交时无需重复携带完整 receiver 签名。
|
||
Function {
|
||
signature: None,
|
||
operation: self.operation,
|
||
owner: Some(owner),
|
||
session: self.session.clone(),
|
||
requested_type: None,
|
||
}
|
||
.invoke(cx, &args[1..])
|
||
}
|
||
}
|
||
|
||
/// The Type dispatcher passes the requested subclass explicitly. Native
|
||
/// constructor options precede the subclass's inherited/own field arguments.
|
||
#[derive(Debug)]
|
||
/// 为源代码调用原生类型构造器提供受检查的分配入口。
|
||
struct NativeFactory {
|
||
// 对 VM 可见的宽外壳:Type 首参 + 原生参数 + 源子类字段。
|
||
signature: Option<CallSignature>,
|
||
// 仅描述原生选项;invoke 切分字段后再检查,不能用外壳的 Any 变参绕过类型契约。
|
||
options: Option<CallSignature>,
|
||
// 决定实际原生分配器,Function 将据此创建 Block、相机或三维对象。
|
||
operation: Operation,
|
||
// 与该类型注册域一致的展示会话,跨 VM 的同名 Type 不能混用。
|
||
session: Shared,
|
||
}
|
||
|
||
impl NativeCallable for NativeFactory {
|
||
/// VM 在类型构造的原生调用边界查询外壳签名;具体原生参数的窄签名由 invoke 进一步检查。
|
||
fn signature(&self) -> Option<CallSignature> {
|
||
self.signature.clone()
|
||
}
|
||
/// 要求原生必填参数再加一个请求 Type;上界暂开放,子类字段与原生选项在 invoke 内分别校验。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
// 原生参数有下界,但源子类可增加字段;不能把 Operation 的上界直接当总实参数量上界。
|
||
Some(endor_core::CallArity::range(
|
||
self.operation.arity().required + 1,
|
||
usize::MAX,
|
||
))
|
||
}
|
||
|
||
/// VM 调用 NativeTyped 构造器时进入;切开原生选项和子类字段,先验证,再分配并初始化结果。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// VM 类型分派把实际请求的子类放在首位,不能退回工厂所属的基类而丢失源子类身份。
|
||
let requested = object(args.first())?;
|
||
// 复制字段声明以结束对 store 的借用,后续调用 Function 和写属性需要可变访问 cx。
|
||
let fields = cx
|
||
.store()
|
||
.payload::<EndorTypeObject>(requested, "Type")?
|
||
.fields()
|
||
.to_vec();
|
||
// 切掉 Type 首参后的实参由两段组成:原生构造选项在前,按声明顺序的子类字段在后。
|
||
let values = &args[1..];
|
||
// 构造实参末尾是类型声明的继承/自有字段,前缀才是原生构造选项;checked_sub 防止缺字段造成下溢。
|
||
let native_count = values
|
||
.len()
|
||
.checked_sub(fields.len())
|
||
.ok_or(invalid("missing subclass field arguments"))?;
|
||
cx.store()
|
||
.check_class_field_arguments(requested, &values[native_count..])?;
|
||
// 外壳允许宽变参,但原生前缀仍按具体 Operation 完整签名检查,不能因子类存在绕过类型或 arity。
|
||
self.options
|
||
.as_ref()
|
||
.ok_or(invalid("native factory is not initialized"))?
|
||
.check_arguments(
|
||
cx.store(),
|
||
"native constructor options",
|
||
&values[..native_count],
|
||
)?;
|
||
// requested_type 通知原生分配器附上真实子类 Type;owner 为空,因为这是构造而非实例方法。
|
||
let result = Function {
|
||
signature: None,
|
||
operation: self.operation,
|
||
owner: None,
|
||
session: self.session.clone(),
|
||
requested_type: Some(requested),
|
||
}
|
||
.invoke(cx, &values[..native_count])?;
|
||
// 原生工厂必须同步返回对象;不能把挂起调用或普通标量误当已分配实例继续写字段。
|
||
let NativeResult::Return(Value::Object(instance)) = result else {
|
||
// 契约错误直接交回 VM,避免把不完整结果作为源程序的构造返回值。
|
||
return Err(invalid("native constructor did not return an instance"));
|
||
};
|
||
// 分配器必须返回精确 requested 类型,不能只返回基类实例;之后才初始化子类字段。
|
||
if cx.store().type_of(instance)? != Some(requested) {
|
||
// 精确身份不符说明分配器漏传/忽略了 requested_type,接受基类会破坏源子类语义。
|
||
return Err(invalid(
|
||
"native constructor returned an incorrect nominal type",
|
||
));
|
||
}
|
||
for (field, value) in fields.iter().zip(&values[native_count..]) {
|
||
// 前面已验证数量与类型,此处按同一声明顺序写入;set_attr 负责托管引用与变更记录。
|
||
cx.set_attr(instance, &field.name, value.clone())?;
|
||
}
|
||
// 将完成字段初始化的原生子类交回 VM,后续源代码才可使用它。
|
||
Ok(NativeResult::Return(Value::Object(instance)))
|
||
}
|
||
}
|
||
|
||
/// 各解码器和 NativeCallable 分支调用的参数错误构造器,交由 VM 保留调用诊断上下文。
|
||
fn invalid(message: &'static str) -> VmError {
|
||
VmError::InvalidArgument(message)
|
||
}
|
||
/// 供桥接实参/属性解码调用:只接受对象值,不把 nil、标量或缺失实参转换成虚构的对象身份。
|
||
fn object(value: Option<&Value>) -> Result<ObjectRef, VmError> {
|
||
// Option 同时覆盖缺失参数和实际值;返回 ObjectRef 仅复制句柄,不增加托管拥有根。
|
||
match value {
|
||
Some(Value::Object(value)) => Ok(*value),
|
||
_ => Err(invalid("expected an object")),
|
||
}
|
||
}
|
||
/// 被工厂、方法及 color 参数解码调用,只接收语言字符串值,不隐式调用对象的 repr。
|
||
fn text(value: Option<&Value>) -> Result<String, VmError> {
|
||
// 拷贝字符串是为了结束源 Value 的借用,让调用者可以把文本移入新的 Block payload。
|
||
match value {
|
||
Some(Value::Text(value)) => Ok(value.clone()),
|
||
_ => Err(invalid("expected text")),
|
||
}
|
||
}
|
||
/// 被 calls 中的容器索引/槽位等参数解析调用,要求可表示为 i64 的非负整数,再转为 u64。
|
||
fn integer(value: Option<&Value>) -> Result<u64, VmError> {
|
||
// 先走数字自身的精确转换,再做有符号到无符号检查;不能用浮点截断接受带小数的索引。
|
||
match value {
|
||
Some(Value::Number(value)) => value
|
||
.as_i64()
|
||
.and_then(|v| u64::try_from(v).ok())
|
||
.ok_or(invalid("expected a nonnegative integer")),
|
||
_ => Err(invalid("expected a nonnegative integer")),
|
||
}
|
||
}
|
||
/// 被文本/矩形工厂和颜色修改操作调用;缺省时使用调用方指定颜色,显式传入错误值则报错。
|
||
fn color(value: Option<&Value>, default: &str) -> Result<String, VmError> {
|
||
// transpose 将 Some(Err) 传播出去,同时保留 None;只有未传参数才使用 default。
|
||
let value = value
|
||
.map(|v| text(Some(v)))
|
||
.transpose()?
|
||
.unwrap_or_else(|| default.into());
|
||
// 此处只校验并保留源字符串,render/document 在使用颜色时按同一格式转换成像素颜色。
|
||
parse_color(&value).map_err(|_| invalid("color must use #RRGGBB or #RRGGBBAA"))?;
|
||
Ok(value)
|
||
}
|
||
/// 被原生工厂的尺寸/字号等参数解析调用,把 Endor 数字限制在可用的有限非负布局范围。
|
||
fn dimension(value: Option<&Value>) -> Result<f32, VmError> {
|
||
// 缺失和非 Number 值都不是合法必填尺寸;默认尺寸由调用方选择,解码器不自行猜测。
|
||
let Some(Value::Number(number)) = value else {
|
||
// 在访问 number 表示之前退出,防止把 nil 或文本当成零尺寸。
|
||
return Err(invalid("expected a finite nonnegative dimension"));
|
||
};
|
||
// 分别支持大整数、十进制和二进制实数表示,不接受非实值;转 f64 后检查有限与范围,再降 f32。
|
||
let value = match number.as_real() {
|
||
Some(EndorReal::Integer(v)) => v.to_string().parse::<f64>().ok(),
|
||
Some(EndorReal::Decimal(v)) => v.literal().parse::<f64>().ok(),
|
||
Some(EndorReal::BinaryFloat(v)) => Some(f64::from_bits(*v)),
|
||
_ => None,
|
||
}
|
||
.filter(|v| v.is_finite() && (0.0..=MAX_DIMENSION).contains(v))
|
||
.ok_or(invalid("dimension exceeds limits"))?;
|
||
// MAX_DIMENSION 远小于 f32 的有限上界;通过上面的检查后再按原生几何格式降低精度。
|
||
Ok(value as f32)
|
||
}
|
||
|
||
/// 由 calls 的 state 工厂调用,为状态对象安装 set/increment 等捕获 owner 的原生方法。
|
||
/// 普通 Block 实例方法走 TypeMethod 共享实现;这里处理的是需要固定 owner 的对象属性函数。
|
||
fn bind(
|
||
cx: &mut NativeContext<'_>,
|
||
owner: ObjectRef,
|
||
name: &'static str,
|
||
operation: Operation,
|
||
session: &Shared,
|
||
) -> Result<(), VmError> {
|
||
// 只在读 desktop 身份时持锁,随后分配/设属性不持有 Session 的锁,避免不必要的嵌套借用。
|
||
let desktop = session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
// 签名描述显式参数,owner 存入 payload;调用者不需要把状态对象再当首参传入。
|
||
let function = cx.allocate(ContextNativeFunction::new(
|
||
name,
|
||
Function {
|
||
signature: Some(operation_signature(cx.store(), desktop, operation)?),
|
||
operation,
|
||
owner: Some(owner),
|
||
session: session.clone(),
|
||
requested_type: None,
|
||
},
|
||
// 绑定方法显式捕获 owner 与 desktop,让托管 GC 识别函数的真实强边,Rust Shared 会话本身不隐式保活 VM 对象。
|
||
vec![owner, desktop],
|
||
))?;
|
||
// 名义函数检查与普通源函数一致;把 callable 写成 owner 属性后由该托管强边持有。
|
||
attach_desktop_type(cx, function, "Function", desktop)?;
|
||
cx.set_attr(owner, name, Value::Object(function))
|
||
}
|
||
|
||
/// PresentResult/present_value 等内部包装路径调用,创建没有源子类请求的默认原生 Block。
|
||
fn new_block(
|
||
cx: &mut NativeContext<'_>,
|
||
data: Data,
|
||
session: &Shared,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 复用类型校验和节点/Page 预算,不允许内部包装绕开普通工厂的资源限制。
|
||
new_typed_block(cx, data, session, None)
|
||
}
|
||
|
||
/// calls 的 Camera2D/CanvasViewport 分配路径调用,为视口建立有名义身份的独立相机状态。
|
||
fn new_camera2d(
|
||
cx: &mut NativeContext<'_>,
|
||
session: &Shared,
|
||
requested: Option<ObjectRef>,
|
||
args: &[Value],
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 相机和使用它的视口必须在同一个类型域;查询当前窗口 desktop 取得注册表。
|
||
let desktop = session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
// 源子类构造保留 requested;自由工厂和视口默认相机使用注册的 Camera2D。
|
||
let definition = requested.unwrap_or(desktop_type(cx.store(), "Camera2D", desktop)?);
|
||
// payload 标识相机种类,具体 x/y/zoom 则是下面写入的托管属性,供源代码和依赖追踪读取。
|
||
let camera = cx.allocate(crate::viewport::Camera2D)?;
|
||
cx.attach_type(camera, definition)?;
|
||
for (index, name, fallback) in [(0, "x", 0.0), (1, "y", 0.0), (2, "zoom", 1.0)] {
|
||
// 位置缺省为原点、缩放缺省为 1;显式错误值由 coordinate 返回错误,不能静默套用缺省。
|
||
let value = args
|
||
.get(index)
|
||
.map(crate::scroll::coordinate)
|
||
.transpose()?
|
||
.unwrap_or(fallback);
|
||
cx.set_attr(camera, name, crate::native_visual::value(value))?;
|
||
}
|
||
// 初始化完所有相机属性后走完整相机校验,尤其 zoom 必须处于 0.01..100,不能只通过一般坐标范围。
|
||
crate::viewport::camera(cx.store(), camera)?;
|
||
Ok(camera)
|
||
}
|
||
|
||
/// calls 的视觉工厂与 new_block 调用:按 Data.kind 分配 payload,保留请求子类并登记节点弱索引。
|
||
fn new_typed_block(
|
||
cx: &mut NativeContext<'_>,
|
||
data: Data,
|
||
session: &Shared,
|
||
requested: Option<ObjectRef>,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// payload 种类确定合法名义基类;同为 Block 也不能把矩形 payload 伪装成文本类型。
|
||
let type_name = match data.kind {
|
||
Kind::Text { .. } => "TextBlock",
|
||
Kind::Rect { .. } => "RectBlock",
|
||
Kind::Spacer(_) => "Spacer",
|
||
Kind::Scroll { .. } => "ScrollBlock",
|
||
Kind::Row => "Row",
|
||
Kind::Column => "Column",
|
||
Kind::Overlay => "Overlay",
|
||
Kind::Scene2D => "Scene2D",
|
||
Kind::CanvasViewport { .. } => "CanvasViewport",
|
||
Kind::Viewport3D { .. } => "Viewport3D",
|
||
Kind::Button => "Button",
|
||
Kind::Page => "Page",
|
||
};
|
||
// 在 data 移入分配器之前记住是否 Page;除通用节点预算外,Page 还受单独数量限制。
|
||
let page = matches!(data.kind, Kind::Page);
|
||
// 先读取注册域,再释放锁进行托管分配;登记弱表时会重新短暂持锁。
|
||
let desktop = session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
// expected 是当前 VM 中合法基类的精确身份,type_object 才是实例最终要附加的真实类型。
|
||
let expected = desktop_type(cx.store(), type_name, desktop)?;
|
||
// 没有源子类请求时使用基类;有请求时仍必须经过下面的继承关系验证。
|
||
let type_object = requested.unwrap_or(expected);
|
||
// 叶/容器原生种类对应不同类型基类,禁止用 TextBlock 分配器创建并非其子类的名义对象。
|
||
if !cx.store().is_subtype(type_object, expected)? {
|
||
return Err(invalid("native constructor cannot allocate this subclass"));
|
||
}
|
||
// 业务 payload 先建立,再附上已验证的名义类型;此处没有布局几何,几何属于后续 Mount。
|
||
let block = block::allocate(cx, data)?;
|
||
cx.attach_type(block, type_object)?;
|
||
{
|
||
// 这个作用域把锁限制在预算检查和弱索引写入期间,返回对象时不再持锁。
|
||
let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?;
|
||
// 节点弱表受宿主预算限制;实际死亡弱项通过后续 reclaim 压缩,不能把弱索引当永不增长的免费结构。
|
||
if state.nodes.len() >= MAX_UI_NODES {
|
||
return Err(VmError::ExecutionLimit("4096 node budget"));
|
||
}
|
||
if page {
|
||
// 弱表可能含已死亡节点,所以 Page 预算数存活页面,而非弱记录总数。
|
||
let mut pages = 0;
|
||
for weak in &state.nodes {
|
||
// 只有成功升级且 payload 的 page 标志为真的节点才计入;普通 Block 不占页面预算。
|
||
if let Some(node) = cx.upgrade(*weak)?
|
||
&& cx.store().payload::<block::Block>(node, "Block")?.page
|
||
{
|
||
pages += 1;
|
||
}
|
||
}
|
||
if pages >= MAX_PAGES {
|
||
// 在登记新节点前失败,运行时仍负责释放此次调用中已经分配但未发布的对象。
|
||
return Err(VmError::ExecutionLimit("64 page budget"));
|
||
}
|
||
}
|
||
// 只登记弱身份,真实拥有关系由返回值、容器子项或页面注册表建立。
|
||
state.nodes.push(cx.store().downgrade(block)?);
|
||
}
|
||
Ok(block)
|
||
}
|
||
|
||
/// calls 中 Window/Detach 操作调用,建立托管窗口和 Presentation;OS 窗口由 GUI 收到发布命令后创建。
|
||
fn create_window(
|
||
cx: &mut NativeContext<'_>,
|
||
state: &mut Session,
|
||
definition: ObjectRef,
|
||
) -> Result<ObjectRef, VmError> {
|
||
if cx.store().is_read_only() {
|
||
// 布局/绘制只读调用不能借“分配新值”偷偷增加已有窗口注册表和外部资源。
|
||
return Err(invalid(
|
||
"window creation is forbidden during read-only rendering",
|
||
));
|
||
}
|
||
if state.exiting {
|
||
// shutdown 已关闭资源域,卸载回调即使仍能执行也不能重新创建窗口延长退出。
|
||
return Err(invalid("runtime is exiting; window creation is forbidden"));
|
||
}
|
||
// 同一 VM 最多16窗口(含主壳);ID 单调分配并限制到 i64 正范围,关闭后不复用旧资源身份。
|
||
if state.presentations.len() >= 16 {
|
||
// 将主窗口一并计入,避免每个副窗口都独立持有缓存和托管根而无界增长。
|
||
return Err(VmError::ExecutionLimit("16 window budget"));
|
||
}
|
||
// checked_add 防溢出,i64 上界保证身份能放入语言整数使用的原生窗口命令路径。
|
||
let id = state
|
||
.next_window
|
||
.checked_add(1)
|
||
.filter(|id| *id <= i64::MAX as u64)
|
||
.ok_or(invalid("window identities exhausted"))?;
|
||
// model/window 建立托管资源关系,返回窗口对象、私有桌面及页面字典,三者用于不同层次。
|
||
let (window, desktop, tabs) = crate::window::create(cx, state.master, id, definition)?;
|
||
// 缓存和 Mount 按窗口独立;共享同一业务对象也不应共享它在两个窗口中的几何。
|
||
let mut presentation = crate::presentation::Presentation::new(desktop, tabs);
|
||
// 新窗口即使暂无内容也需发布有限空帧,让 GUI 根据投影创建 OS 窗口,而不是在 VM 中直接创建。
|
||
presentation.force_frame = true;
|
||
state.presentations.insert(id, presentation);
|
||
state.next_window = id;
|
||
Ok(window)
|
||
}
|
||
|
||
/// show_in 与页面/窗口方法调用,遍历真实注册表确定 Page 当前归属;未打开的 Page 返回 None。
|
||
fn page_window(
|
||
store: &endor_core::ObjectStore,
|
||
state: &Session,
|
||
page: ObjectRef,
|
||
) -> Result<Option<u64>, VmError> {
|
||
for (id, view) in &state.presentations {
|
||
// 从每个私有 desktop 取得托管 Window,不能只依据当前活动页判断窗口的全部页面。
|
||
let window = object(Some(&store.get_attr(view.desktop, "window")?))?;
|
||
if crate::window::pages(store, window)?.contains(&page) {
|
||
// 返回原生资源 ID 而非 Window ObjectRef,供 Session 的 presentations 索引使用。
|
||
return Ok(Some(*id));
|
||
}
|
||
}
|
||
// 无归属是新 Page 的正常状态,show_in 会再选 current;注册表读取失败已由 ? 传播。
|
||
Ok(None)
|
||
}
|
||
/// show_in 跨窗口迁移及 calls 关闭页面时调用,撤销登记并为源窗口安排替代活动页。
|
||
fn remove_page(
|
||
cx: &mut NativeContext<'_>,
|
||
view: &mut crate::presentation::Presentation,
|
||
page: ObjectRef,
|
||
) -> Result<(), VmError> {
|
||
// view 决定移除的是哪个窗口的关系;同一个 Page 的业务内容并不在这里销毁。
|
||
let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?;
|
||
crate::window::unregister_page(cx, window, page)?;
|
||
// 双层 pending 区分无请求与请求清空;移除当前/待选页时改选剩余第一页,或显式 pending=None页面。
|
||
if view.pending.unwrap_or(view.active) == Some(page) {
|
||
// 必须在 unregister_page 之后选取,才不会重新选中刚移走的 Page。
|
||
let next = crate::window::pages(cx.store(), window)?.first().copied();
|
||
cx.set_attr(
|
||
view.desktop,
|
||
"pending",
|
||
next.map(Value::Object).unwrap_or(Value::Nil),
|
||
)?;
|
||
view.pending = Some(next);
|
||
}
|
||
Ok(())
|
||
}
|
||
/// 页面 open/move 等调用与 show 的共同入口:验证目标、迁移注册关系,再安排一次待提交展示。
|
||
fn show_in(
|
||
cx: &mut NativeContext<'_>,
|
||
page: ObjectRef,
|
||
session: &Shared,
|
||
target: Option<ObjectRef>,
|
||
) -> Result<(), VmError> {
|
||
// 一次持锁覆盖“查旧归属—移除—登记新归属”,避免 Session 内出现中途被其他桥接调用观察的关系。
|
||
let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?;
|
||
// old 是迁移前归属;后面仅在新旧 ID 不同时撤源登记,重复 activate 不应搬动页面。
|
||
let old = page_window(cx.store(), &state, page)?;
|
||
// target 是托管 Window,id 是对应宿主窗口编号;验证后才能用它索引 Presentation。
|
||
let id = match target {
|
||
// 显式目标窗口既要未关闭,也要对应本会话 Presentation 的同一 desktop,拒绝其他 VM 的窗口身份。
|
||
Some(window) => {
|
||
// 先检查 payload 种类,再验证资源状态及 desktop 身份,不能只信任传入对象的数字 ID。
|
||
let data = cx
|
||
.store()
|
||
.payload::<crate::window::ManagedWindow>(window, "Window")?;
|
||
if data.closed {
|
||
// 已退役窗口仍可能被源代码持有,但不能再作为展示目标。
|
||
return Err(invalid("native window is closed"));
|
||
}
|
||
// ID 用于查表,desktop 的相等检查补上所属桥接会话验证。
|
||
let id = data.id;
|
||
if state
|
||
.presentations
|
||
.get(&id)
|
||
.is_none_or(|view| view.desktop != data.desktop)
|
||
{
|
||
// 缺少 Presentation 或 desktop 不匹配都说明不是本会话可操作的窗口。
|
||
return Err(invalid("window belongs to another runtime"));
|
||
}
|
||
id
|
||
}
|
||
// 不指定窗口时优先保留 Page 现有归属;未打开过的新页才进入 current,而非每次 activate 都迁移到主窗口。
|
||
None => old.unwrap_or(state.current),
|
||
};
|
||
if let Some(old) = old
|
||
&& old != id
|
||
{
|
||
// 跨窗口移动先撤源窗口登记与必要活动页选择,再在目标登记同一 Page,业务内容对象不复制。
|
||
remove_page(
|
||
cx,
|
||
state
|
||
.presentations
|
||
.get_mut(&old)
|
||
.ok_or(invalid("missing source window"))?,
|
||
page,
|
||
)?;
|
||
}
|
||
// 目标经过检查后取得可变展示上下文,用于写 pending;不会在这里改 active 或发布帧。
|
||
let view = state
|
||
.presentations
|
||
.get_mut(&id)
|
||
.ok_or(invalid("missing target window"))?;
|
||
// model 注册表保存 Page 的强拥有关系,也决定标签顺序;只写 pending 不足以完成登记。
|
||
let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?;
|
||
crate::window::register_page(cx, window, page)?;
|
||
cx.set_attr(view.desktop, "pending", Value::Object(page))?;
|
||
// 仅记录展示请求并由 desktop.pending 强持有 Page;实际 active、Mount、布局和 published_version 留待刷新提交。
|
||
view.pending = Some(Some(page));
|
||
Ok(())
|
||
}
|
||
/// calls 的激活/导航及 PresentResult 调用,不指定新目标窗口,委托 show_in 保留既有归属或选择 current。
|
||
fn show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result<(), VmError> {
|
||
show_in(cx, page, session, None)
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
/// 接收源 present 结果并将其转换成宿主可展示内容的原生续接步骤。
|
||
struct PresentResult {
|
||
// ui.open 的续接需确保有 Page 并安排展示;ui.present 的续接只返回视觉对象。
|
||
open: bool,
|
||
// 源 present 返回后仍要回到原桥接域分配包装页/修改 pending,因此续接保存同一会话。
|
||
session: Shared,
|
||
}
|
||
impl NativeCallable for PresentResult {
|
||
/// VM 的原生续接调用只提供被委托 present 的一个返回值;参数数量在进入 invoke 前检查。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(1))
|
||
}
|
||
|
||
/// VM 完成源 Presentable.present 后调用;验证返回 payload,并按 open 标志选择包装和展示行为。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 类型声明不能替代返回值防御检查,错误返回立即中止,不能把它包装成看似成功的文本。
|
||
let [Value::Object(root)] = args else {
|
||
return Err(invalid("Presentable.present must return a Block"));
|
||
};
|
||
// payload 检查保证返回的是原生视觉 Block;page 标志决定是否还需外包 Page。
|
||
let is_page = cx.store().payload::<block::Block>(*root, "Block")?.page;
|
||
// present 委托返回值必须为 Block;open=true 必要时包 Page 并展示,open=false 只返回转换结果。
|
||
let result = if self.open {
|
||
// open 对已是 Page 的值保留身份,对普通 Block 创建单子项 Page,均走同一 show 流程。
|
||
let page = if is_page {
|
||
*root
|
||
} else {
|
||
// 包装只新增页面,不复制返回对象;同一个 Block 可随后产生自己的 Mount。
|
||
new_block(
|
||
cx,
|
||
Data::new(Kind::Page, vec![*root], 0.0, 0.0),
|
||
&self.session,
|
||
)?
|
||
};
|
||
show(cx, page, &self.session)?;
|
||
page
|
||
} else {
|
||
// present 是值转换,不能附带把页面激活/打开的外部副作用。
|
||
*root
|
||
};
|
||
Ok(NativeResult::Return(Value::Object(result)))
|
||
}
|
||
}
|
||
|
||
/// calls 的 open/present 普通值路径调用:已有 Block 保持身份,其余转成默认 TextBlock。
|
||
/// 具有 Presentable 协议的源方法调用在 calls 的外层处理,返回后才进入 PresentResult。
|
||
fn present_value(
|
||
cx: &mut NativeContext<'_>,
|
||
value: &Value,
|
||
session: &Shared,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 快速路径只接受真实 Block payload,避免对已有视觉对象调用 repr 后丢失其行为与内容。
|
||
if let Value::Object(block) = value
|
||
&& cx
|
||
.store()
|
||
.object(*block)?
|
||
.payload()
|
||
.as_any()
|
||
.is::<block::Block>()
|
||
{
|
||
// 复用对象身份,不在此新增根或克隆业务 payload;调用边界管理返回值的拥有关系。
|
||
return Ok(*block);
|
||
}
|
||
// 非 Block 的标量/对象转可读文字,Presentable 方法委托在 calls.rs 的更外层处理,本函数不自己调用 present。
|
||
let value = match value {
|
||
Value::Nil => "nil".into(),
|
||
Value::Bool(v) => v.to_string(),
|
||
Value::Number(v) => v.to_string(),
|
||
Value::Text(v) => v.clone(),
|
||
Value::Object(v) => cx.store().repr(*v)?,
|
||
};
|
||
if value.len() > 16_384 {
|
||
// 按 UTF-8 字节限制文本开销;这是展示限制,不是“最多 16384 个 Unicode 字符”。
|
||
return Err(invalid("presentation exceeds 16 KiB"));
|
||
}
|
||
// 回退视觉使用默认颜色和字号种类;几何留给后续测量,不在转换阶段猜测像素尺寸。
|
||
new_block(
|
||
cx,
|
||
Data::new(
|
||
Kind::Text {
|
||
value,
|
||
size: 1,
|
||
color: "#242422".into(),
|
||
},
|
||
vec![],
|
||
0.0,
|
||
0.0,
|
||
),
|
||
session,
|
||
)
|
||
}
|
||
|
||
#[must_use = "pending layout must be resumed on its owning VM"]
|
||
/// 桥接布局的挂起/完成结果;PendingLayout 必须与其窗口上下文一起继续处理。
|
||
pub enum LayoutProgress {
|
||
// 交给 endor_host 保存;恢复时必须携带同一 VM、窗口和检查点,不能换域重跑。
|
||
Pending(Box<PendingLayout>),
|
||
// 几何已就绪的帧草稿;运行时仍需完成绘制和发布阶段,不等于此刻已显示到屏幕。
|
||
Complete(Box<crate::document::Frame>),
|
||
}
|
||
|
||
/// 一次未提交布局的上下文:目标窗口、根、布局/缓存、延后钩子以及用于失败恢复的检查点。
|
||
struct LayoutContext {
|
||
// 本次布局使用的页面模式/相机参数;窗口 resize 重定向时需要和新约束比较。
|
||
surface: crate::page::Surface,
|
||
// 不可变视觉树草稿,布局和后续绘制必须使用与这一轮匹配的对象快照。
|
||
root: Arc<UiNode>,
|
||
// 与本轮页面注册表一致的纯数据标签投影,不能在提交时混入下一轮的顺序。
|
||
tabs: Vec<crate::tabs::Tab>,
|
||
// 原生输入给出的整窗可用尺寸;不同于页面局部约束,完成布局时据此构造完整帧。
|
||
full_viewport: Option<crate::layout::Size>,
|
||
// 私有宿主对象和资源 ID 一起记录所属窗口,续体恢复不能误用 Session.current 的新值。
|
||
desktop: ObjectRef,
|
||
window: u64,
|
||
// 布局同步返回的生命周期序列拥有根,暂留到安全提交边界执行/释放,取消时也必须清理。
|
||
hooks: Option<ObjectRef>,
|
||
// 布局准备可能暂改 Mount payload 与 desktop.mounts,检查点保证失败或尺寸重定向能恢复未发布关系。
|
||
checkpoint: crate::mount::Checkpoint,
|
||
}
|
||
|
||
/// 挂起布局的拥有者。续体与上下文必须一起保留并最终恢复或取消。
|
||
pub struct PendingLayout {
|
||
// source_layout 拥有尚未完成的 VM 调用和测量状态,必须继续推进或显式取消。
|
||
work: crate::source_layout::Work,
|
||
// 与 work 配套的树、窗口和检查点;单独丢弃上下文会使取消后无法恢复未提交 Mount。
|
||
context: LayoutContext,
|
||
}
|
||
|
||
/// 运行时持有的桥接能力句柄,通过共享 Session 协调托管对象与每窗口展示。
|
||
pub struct UiBridge {
|
||
// runtime 安装全局名字和调用 ui.open 时使用;真正的 VM 拥有根由 Candidate 管理。
|
||
pub ui: ObjectRef,
|
||
// 工厂/方法与运行时共用展示会话,窗口切换、版本和缓存均从这里协调。
|
||
session: Shared,
|
||
// 本桥接域的任务调度器,运行时通过 poll_tasks 推进,退出通过 shutdown/cancel_tasks 清理。
|
||
tasks: crate::tasks::Scheduler,
|
||
}
|
||
|
||
/// dispatch_tab/页面事件等桥接控制路径调用,通过 VM 正常查找绑定方法,保留源覆盖与继承语义。
|
||
fn invoke_member(
|
||
vm: &mut Vm,
|
||
owner: ObjectRef,
|
||
method: &str,
|
||
arguments: &[Value],
|
||
instruction_budget: usize,
|
||
) -> Result<Value> {
|
||
// 使用 VM 正常绑定规则创建临时方法值;无论 call 成功/失败,先保存 result 再释放 bound 根。
|
||
let bound = vm.bind_attr(owner, method)?;
|
||
// 暂存调用结果而非直接 ? 返回,确保即使用户方法报错也会执行后面的 release_value(bound)。
|
||
let result = match &bound {
|
||
// 方法值为对象才可能调用;VM 再检查对象是否 callable,并应用帧数/指令预算。
|
||
Value::Object(function) => vm
|
||
.call_with_limits(
|
||
*function,
|
||
arguments,
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: instruction_budget,
|
||
},
|
||
)
|
||
.map_err(|error| crate::endor_host::runtime_error(vm, error)),
|
||
// 普通属性没有函数身份,不能把它当无参 getter 或忽略调用。
|
||
_ => Err(anyhow::anyhow!("object method is not callable")),
|
||
};
|
||
vm.release_value(bound)?;
|
||
result
|
||
}
|
||
|
||
/// prepare/刷新流程调用,把页面注册表投影成 GUI 可直接读取的标签数据;不向 GUI 传 ObjectRef。
|
||
fn project_tabs(
|
||
vm: &Vm,
|
||
state: &mut crate::presentation::Presentation,
|
||
) -> Result<Vec<crate::tabs::Tab>> {
|
||
// 从窗口模型取得完整页面顺序,不能只用当前 active 页面构造标签栏。
|
||
let window = object(Some(&vm.get_attr(state.desktop, "window")?))?;
|
||
// pages 是本次读取的托管身份列表;下方逐项转为不依赖 VM 的标题、数字 ID 和选中状态。
|
||
let pages = crate::window::pages(vm.object_store(), window)?;
|
||
// 标签选中反映待提交页面(若有请求),否则沿用 active;页面标题和顺序仍从真实窗口注册表提取。
|
||
let active = state.pending.unwrap_or(state.active);
|
||
// live 重建仍有效的弱身份到标签 ID 映射,过期/移出页面的旧记录不再保留。
|
||
let mut live = Vec::new();
|
||
// tabs 是对外返回的纯数据;和 live 分开,避免渲染线程接触托管弱引用。
|
||
let mut tabs = Vec::new();
|
||
for (index, page) in pages.iter().copied().enumerate() {
|
||
// 弱身份识别同一个 Page,即使它改标题或位置也不改变该窗口内的标签 ID。
|
||
let weak = vm.object_store().downgrade(page)?;
|
||
// 同一 Page 弱身份复用该窗口标签 ID,新增页用高水位;移出后 live 更新会移除旧映射,不靠标题匹配。
|
||
let id = if let Some((_, id)) = state.tab_ids.iter().find(|(w, _)| *w == weak) {
|
||
*id
|
||
} else {
|
||
// 新页面才分配单调增长的 ID;checked_add 避免溢出后把旧拖动事件指向另一标签。
|
||
state.next_tab = state
|
||
.next_tab
|
||
.checked_add(1)
|
||
.ok_or_else(|| anyhow::anyhow!("tab identities exhausted"))?;
|
||
state.next_tab
|
||
};
|
||
live.push((weak, id));
|
||
// 标题显示过滤控制字符并限制120个 Unicode 字符,缺失/空标题用默认序号;不修改原 Page.title。
|
||
let title = match vm.get_attr(page, "title") {
|
||
Ok(Value::Text(title)) if !title.is_empty() => title
|
||
.chars()
|
||
.filter(|c| !c.is_control())
|
||
.take(120)
|
||
.collect(),
|
||
_ => format!("Page {}", index + 1),
|
||
};
|
||
// 对外标签只保存可显示数据;工具标记用于 GUI 识别开发工具入口,不泄露 Page 对象。
|
||
tabs.push(crate::tabs::Tab {
|
||
id,
|
||
title,
|
||
developer_tool: match vm.get_attr(page, "_tesseraTool") {
|
||
// 仅接受源属性中的字符串工具名,缺失或其他值都投影为普通页面。
|
||
Ok(Value::Text(name)) => Some(name),
|
||
_ => None,
|
||
},
|
||
active: active == Some(page),
|
||
});
|
||
}
|
||
// 覆盖映射而不是追加,后续 dispatch_tab 只允许解析这次仍在注册表里的页面身份。
|
||
state.tab_ids = live;
|
||
Ok(tabs)
|
||
}
|
||
|
||
impl UiBridge {
|
||
/// endor_host 清理运行时调用,取消任务并退役所有窗口展示;callbacks 决定能否继续执行用户卸载钩子。
|
||
/// 返回窗口、Mount 和取消任务数量供诊断使用;不为退出再启动布局或绘制。
|
||
pub fn shutdown(&self, vm: &mut Vm, callbacks: bool) -> Result<(usize, usize, usize)> {
|
||
// 退出先关闭新任务入口并取消旧任务,再标 session.exiting,避免卸载回调重新创建任务/窗口。
|
||
let tasks = self.tasks.quiesce(vm)?;
|
||
// 保存各窗口卸载驱动的拥有根,必须等所有窗口先关闭后才逐个调用并释放。
|
||
let mut drivers = Vec::new();
|
||
// 统计退出前仍活动的出现位置,不按共享业务 Block 数量计数。
|
||
let mut mounts = 0;
|
||
// 闭包收集准备阶段错误但不立刻退出外层:此前已生成的驱动仍需释放。
|
||
let prepared = (|| -> Result<usize> {
|
||
// 只在修改窗口表时持锁;用户钩子执行移到锁作用域之外,避免回调重入死锁。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
state.exiting = true;
|
||
// 复制键后再逐窗可变借用,保证本轮遍历稳定且不持有 map 迭代借用。
|
||
let ids: Vec<_> = state.presentations.keys().copied().collect();
|
||
for id in &ids {
|
||
// ids 来自同一锁内的表,未删除过键,因此缺项表示内部不变量被破坏。
|
||
let view = state.presentations.get_mut(id).expect("known window");
|
||
// 每个窗口自己的 desktop 用于切断挂载关系,不能用主窗口 desktop 代替。
|
||
let desktop = view.desktop;
|
||
mounts += view.mounts.active.len();
|
||
// 正常域用空结构 synchronize 安排孩子先卸载的 hooks;故障域直接 retire,不能再次执行用户代码。
|
||
if callbacks {
|
||
if let Some(hooks) = view.mounts.synchronize(
|
||
vm,
|
||
desktop,
|
||
None,
|
||
Vec::new(),
|
||
&crate::document::Frame::default(),
|
||
false,
|
||
)? {
|
||
// synchronize 只准备卸载顺序;现在不运行回调,以便先关闭其他窗口。
|
||
drivers.push((*id, hooks));
|
||
}
|
||
} else {
|
||
// 故障 VM 不再执行源代码;直接断开挂载关系,把清理限于宿主操作。
|
||
view.mounts.retire(vm, desktop)?;
|
||
}
|
||
// 取得 model/window 的资源对象,以便退役页面拥有关系与窗口状态。
|
||
let window = object(Some(&vm.get_attr(desktop, "window")?))?;
|
||
// 所有窗口先标 closed 并断 Page 拥有关系,然后才在下面执行用户卸载 hooks,阻止回调重新打开已关闭窗口。
|
||
crate::window::retire(vm, window)?;
|
||
vm.set_attr(desktop, "pending", Value::Nil)?;
|
||
vm.set_attr(desktop, "focus", Value::Nil)?;
|
||
view.pending = Some(None);
|
||
view.active = None;
|
||
view.closing = true;
|
||
}
|
||
Ok(ids.len())
|
||
})();
|
||
// 保留准备失败或数量结果;后续钩子/释放错误仍可把结果转成 Err,但不跳过驱动释放。
|
||
let mut result = prepared.map(|windows| (windows, mounts, tasks));
|
||
// 若之前清理/回调失败,余下 hooks 不再执行,但所有驱动根仍逐项 release,避免失败路径泄漏。
|
||
for (id, hooks) in drivers {
|
||
if result.is_ok() {
|
||
// 只有此前所有退出操作正常时才调用源代码;每个驱动有独立有界 VM 预算。
|
||
let call = (|| -> Result<()> {
|
||
self.select_window(id)?;
|
||
// 钩子调用的返回值也是拥有值,即使无业务用途也必须释放。
|
||
let value = vm
|
||
.call_with_limits(
|
||
hooks,
|
||
&[],
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 1_000_000,
|
||
},
|
||
)
|
||
.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
|
||
vm.release_value(value)?;
|
||
Ok(())
|
||
})();
|
||
if let Err(error) = call {
|
||
// 一次失败后停止后续用户钩子,继续外层释放所有已准备的驱动根。
|
||
result = Err(error);
|
||
}
|
||
}
|
||
if let Err(error) = vm.release(hooks) {
|
||
// 无论钩子是否实际调用都释放;释放失败也报告,不能把错误吞作正常退出。
|
||
result = Err(error.into());
|
||
}
|
||
}
|
||
// 退出方法返回前恢复桥接默认上下文;Presentation 表尚未在本方法内删除主窗口。
|
||
self.select_window(crate::window::MAIN_WINDOW)?;
|
||
result
|
||
}
|
||
|
||
/// endor_host/worker 查询是否仍有协作任务,以决定继续推进还是等待外部事件;不执行任务本身。
|
||
pub fn has_cooperative_tasks(&self) -> bool {
|
||
self.tasks.has_cooperative_work()
|
||
}
|
||
|
||
/// endor_host 的 VM 轮询调用,在所属 VM 中交付至多 limit 项任务,保持回调串行。
|
||
pub fn poll_tasks(&self, vm: &mut Vm, limit: usize) -> Result<usize> {
|
||
self.tasks.poll(vm, limit)
|
||
}
|
||
/// endor_host 取消/退役运行时调用,取消本域全部任务;已在后台进行的 I/O 也不得再交付旧回调。
|
||
pub fn cancel_tasks(&self, vm: &mut Vm) -> Result<usize> {
|
||
self.tasks.cancel_all(vm)
|
||
}
|
||
/// endor_host 启动全窗口刷新时调用,取得经过迁移退旧优先排序的窗口身份快照。
|
||
pub fn window_ids(&self) -> Result<Vec<u64>> {
|
||
// 持锁期间只读/排序原生资源表,不执行 VM 回调;调用者取得独立列表后再开始逐窗工作。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 复制身份,不把 Session 的 map 借用暴露给运行时;后续每窗刷新仍显式 select_window。
|
||
let mut ids: Vec<_> = state.presentations.keys().copied().collect();
|
||
// 优先处理切离旧 active 页的窗口,使跨窗口迁移先退旧出现再挂新出现;同优先级按窗口 ID 稳定排序。
|
||
ids.sort_by_key(|id| {
|
||
// 第一个排序键区分“旧 active 即将退出”和其他窗口,第二个键确保同组顺序可重复。
|
||
let view = &state.presentations[id];
|
||
(
|
||
if view.active.is_some()
|
||
&& view.pending.is_some()
|
||
&& view.pending.flatten() != view.active
|
||
{
|
||
// pending = Some(None) 也属于切离旧页,需先安排卸载旧出现位置。
|
||
0
|
||
} else {
|
||
// 无旧页或没有切换请求,无需抢在迁移源窗口之前刷新。
|
||
1
|
||
},
|
||
*id,
|
||
)
|
||
});
|
||
Ok(ids)
|
||
}
|
||
/// runtime 在接受窗口输入前查询;锁异常、缺失或 closing 都视为不再可接受,不执行 VM 操作。
|
||
pub fn accepts_window(&self, id: u64) -> bool {
|
||
self.session.lock().is_ok_and(|state| {
|
||
// 控制线程只查询原生会话状态;页面对象身份与版本还有单独的后续校验。
|
||
state
|
||
.presentations
|
||
.get(&id)
|
||
.is_some_and(|view| !view.closing)
|
||
})
|
||
}
|
||
|
||
/// endor_host 的逐窗刷新、事件及退出钩子调用,选择后续 Session 字段访问的目标窗口。
|
||
pub fn select_window(&self, id: u64) -> Result<()> {
|
||
// current 的检查与写入放在同一短锁中,Deref 的“选中窗口存在”前提由本方法维护。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
if !state.presentations.contains_key(&id) {
|
||
// 先检查再改 current,失败时保留原选择,避免 Deref 命中不存在的 Presentation。
|
||
bail!("window is unavailable");
|
||
}
|
||
state.current = id;
|
||
Ok(())
|
||
}
|
||
/// endor_host 故障/关闭路径调用,直接退役副窗口并丢弃未发布命令,不触发源卸载回调。
|
||
pub fn retire_window(&self, vm: &mut Vm, id: u64) -> Result<()> {
|
||
if id == crate::window::MAIN_WINDOW {
|
||
// 主壳由原生生命周期管理,不能通过删除 Presentation 假装已关闭 OS 主窗口。
|
||
bail!("the primary shell is closed by the OS window control");
|
||
}
|
||
// 锁覆盖副窗口所有关系的撤销,后续不在此执行源回调,因此无需放锁再进入 VM。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
let Some(view) = state.presentations.get_mut(&id) else {
|
||
// 已退役的窗口再次清理是正常幂等操作,不能让重复关闭导致整个运行时失败。
|
||
return Ok(());
|
||
};
|
||
// 把要清理窗口的身份保存下来,随后依次断 Mount、窗口页面和焦点属性的托管引用。
|
||
let desktop = view.desktop;
|
||
// model/window 负责拥有关系;Session 中删项本身不足以关闭托管资源。
|
||
let window = object(Some(&vm.get_attr(desktop, "window")?))?;
|
||
view.mounts.retire(vm, desktop)?;
|
||
crate::window::retire(vm, window)?;
|
||
vm.set_attr(desktop, "pending", Value::Nil)?;
|
||
vm.set_attr(desktop, "focus", Value::Nil)?;
|
||
vm.set_attr(desktop, "focusRequested", Value::Bool(false))?;
|
||
// Draining is not publication: failed-domain effects are discarded.
|
||
// 故障宿主退役的命令只丢弃不发布,防止失败域遗留 Resize/Focus 等影响当前原生状态。
|
||
let _ = crate::window::take_commands(vm, window)?;
|
||
// 从主资源字典删除副窗口强边,允许仍无其他引用的私有窗口对象被回收。
|
||
let windows = object(Some(&vm.get_attr(state.master, "windows")?))?;
|
||
vm.dict_delete(windows, &Value::Text(id.to_string()))?;
|
||
state.presentations.remove(&id);
|
||
state.current = crate::window::MAIN_WINDOW;
|
||
Ok(())
|
||
}
|
||
|
||
/// endor_host 在正常全窗口刷新收尾调用,移除已 closing 的副窗口并保留其待发布原生命令。
|
||
pub fn finish_closed(&self, vm: &mut Vm) -> Result<()> {
|
||
// 正常收尾与故障 retire_window 不同:本锁内先保存命令,再删除托管/原生资源表项。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 正常 finish_closed 只移除已标 closing 的副窗口,主壳保持到 OS 退出;其最后命令转存 retired_commands 待发布。
|
||
let ids: Vec<_> = state
|
||
.presentations
|
||
.iter()
|
||
.filter(|(id, view)| **id != crate::window::MAIN_WINDOW && view.closing)
|
||
.map(|(id, _)| *id)
|
||
.collect();
|
||
// 该字典是托管拥有关系,需和原生 presentations 的删项保持一致。
|
||
let windows = object(Some(&vm.get_attr(state.master, "windows")?))?;
|
||
for id in ids {
|
||
// 键来自上面已筛选列表,持同一把锁时不会被其他路径删掉。
|
||
let desktop = state.presentations[&id].desktop;
|
||
// 在移除资源表前取出 outbox,避免正常关闭命令失去最后的读取机会。
|
||
let window = object(Some(&vm.get_attr(desktop, "window")?))?;
|
||
state
|
||
.retired_commands
|
||
.extend(crate::window::take_commands(vm, window)?);
|
||
vm.dict_delete(windows, &Value::Text(id.to_string()))?;
|
||
state.presentations.remove(&id);
|
||
}
|
||
state.current = crate::window::MAIN_WINDOW;
|
||
Ok(())
|
||
}
|
||
/// endor_host/worker 形成发布结果时调用,收集并清空各窗口 outbox;GUI 执行仍要校验 session。
|
||
pub fn take_window_commands(&self, vm: &Vm) -> Result<Vec<crate::window::Command>> {
|
||
// 需要可变锁消费 retired_commands;VM 侧 take_commands 也清空各自 outbox,不在此调用源方法。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 先取已移除窗口的最后命令,再合并仍活动窗口 outbox;按窗口/窗口内序号排序,不丢失正常 Close 请求。
|
||
let mut commands = std::mem::take(&mut state.retired_commands);
|
||
for view in state.presentations.values() {
|
||
// 活动和正在关闭但尚未移除的窗口都可能有命令,不能只收集当前窗口。
|
||
let window = object(Some(&vm.get_attr(view.desktop, "window")?))?;
|
||
commands.extend(crate::window::take_commands(vm, window)?);
|
||
}
|
||
// 保证同窗命令按递增序号执行;不同窗口之间只提供稳定顺序,不伪造跨窗口事务顺序。
|
||
commands.sort_by_key(|command| (command.window, command.sequence));
|
||
Ok(commands)
|
||
}
|
||
|
||
/// endor_host 创建会话/接收 resize 时调用,更新选中窗口的逻辑尺寸,返回是否真的发生变化。
|
||
pub fn set_viewport(&self, vm: &mut Vm, viewport: Option<crate::layout::Size>) -> Result<bool> {
|
||
if let Some(size) = viewport
|
||
&& (!size.width.is_finite()
|
||
|| !size.height.is_finite()
|
||
|| !(0.0..=MAX_DIMENSION as f32).contains(&size.width)
|
||
|| !(0.0..=MAX_DIMENSION as f32).contains(&size.height))
|
||
{
|
||
// 在分配托管输入前拒绝 NaN/无穷/负值/过大值,保留原来合法尺寸和发布画面。
|
||
bail!("invalid window viewport");
|
||
}
|
||
// 校验成功后再持锁更新当前窗口的尺寸,其他 Presentation 保留各自 viewport。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 尺寸比较在分配新输入对象前完成,避免相同 resize 通知改变 VM 版本而触发自我失效。
|
||
if state.layout.viewport == viewport {
|
||
// 相同输入返回 false,worker 不必因此强制再排版或修改依赖版本。
|
||
return Ok(false);
|
||
}
|
||
if let Some(size) = viewport {
|
||
// 只在有尺寸时创建托管协议对象;该局部对象先有根,设置 desktop 属性成功后再释放局部根。
|
||
let input = vm.new_object()?;
|
||
// 闭包暂存写属性错误,确保任意一个字段写入失败也能经过下面的 release(input)。
|
||
let result = (|| {
|
||
vm.set_attr(input, "width", Value::Number((size.width as f64).into()))?;
|
||
vm.set_attr(input, "height", Value::Number((size.height as f64).into()))?;
|
||
vm.set_attr(state.desktop, "viewport", Value::Object(input))
|
||
})();
|
||
// desktop 已建立 viewport 强边后释放局部根;字段/安装失败也先释放 input,再传播保存的错误。
|
||
vm.release(input)?;
|
||
result?;
|
||
} else {
|
||
// 未知尺寸用 nil 明确表示,不把旧尺寸残留在源 ui.viewport 可见输入里。
|
||
vm.set_attr(state.desktop, "viewport", Value::Nil)?;
|
||
}
|
||
state.layout.viewport = viewport;
|
||
// A fresh Console domain can own window inputs without replacing the
|
||
// preserved displayed page until it actually opens/renders something.
|
||
// 空的新 Console 域可以记录逻辑尺寸,但未展示任何 Page 时不应覆盖用户仍看到的旧域画面。
|
||
state.force_frame |= state.active.is_some() || state.pending.flatten().is_some();
|
||
Ok(true)
|
||
}
|
||
|
||
/// endor_host 完成 bootstrap 后调用,把 UI 命名空间、兼容名字和类型查询入口写入程序全局字典。
|
||
pub fn install_globals(&self, vm: &mut Vm, globals: ObjectRef) -> Result<()> {
|
||
vm.dict_set(globals, Value::Text("ui".into()), Value::Object(self.ui))?;
|
||
// 旧模块示例把 Block 写成 View;别名引用同一 Type,不建立另一个互不相容的同名类型。
|
||
vm.dict_set(
|
||
globals,
|
||
Value::Text("View".into()),
|
||
vm.get_attr(self.ui, "Block")?,
|
||
)?;
|
||
// 全局名引用 ui 中同一批名义类型/函数对象,模块和 Console 不会各复制一份类型体系。
|
||
for name in TYPE_NAMES.iter().chain(QUERY_NAMES) {
|
||
// bootstrap 必须已安装相应名字;取值失败就返回,不能悄悄让程序缺少某个声明的全局。
|
||
let value = vm.get_attr(self.ui, name)?;
|
||
vm.dict_set(globals, Value::Text((*name).into()), value)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
/// endor_host 处理主程序结果时查询,判断源程序是否已自行打开页面,从而避免重复包装/展示返回值。
|
||
pub fn has_page(&self) -> Result<bool> {
|
||
// 检查所有窗口;pending 的 Some(None) 是清空请求,不等于已经有一个新页面。
|
||
Ok(self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.presentations
|
||
.values()
|
||
.any(|view| view.pending.flatten().is_some() || view.active.is_some()))
|
||
}
|
||
/// runtime 的输入接纳路径调用,验证拖放两端的版本和注册顺序仍与发布画面一致,不修改页面。
|
||
pub fn accepts_tab_drop(&self, vm: &Vm, input: &crate::events::Input) -> bool {
|
||
let Some(target) = &input.tab_drop else {
|
||
// 普通事件没有拖放目标,本方法不额外限制;它们由其他输入身份检查处理。
|
||
return true;
|
||
};
|
||
let Ok(state) = self.session.lock() else {
|
||
// 无法可靠验证时拒绝,而不是让不可用的会话继续重排页面。
|
||
return false;
|
||
};
|
||
// 拖放源与目标都必须匹配各自发布版本,且真实 Page 注册顺序仍对应标签弱身份表,防止用陈旧列表下标重排。
|
||
for (id, version) in [
|
||
(input.window, input.layout_version),
|
||
(target.window, target.layout_version),
|
||
] {
|
||
let Some(view) = state.presentations.get(&id) else {
|
||
// 任一端窗口已经退役都使整次拖动失效。
|
||
return false;
|
||
};
|
||
if view.closing || view.published_version != version {
|
||
// 不仅源端,目标端布局变化也会让落点索引不再可靠。
|
||
return false;
|
||
}
|
||
let Ok(Value::Object(window)) = vm.get_attr(view.desktop, "window") else {
|
||
// 内部关系读取异常在这个布尔验证接口统一映射为拒绝,不产生半次迁移。
|
||
return false;
|
||
};
|
||
let Ok(pages) = crate::window::pages(vm.object_store(), window) else {
|
||
// 页面注册表不可读时同样不能相信 GUI 发来的标签顺序。
|
||
return false;
|
||
};
|
||
if view.tab_ids.len() != pages.len()
|
||
|| !pages
|
||
.iter()
|
||
.zip(&view.tab_ids)
|
||
.all(|(page, (weak, _))| vm.object_store().downgrade(*page).ok() == Some(*weak))
|
||
{
|
||
// 数量相同也可能已换页/重排,所以逐个比较弱身份而非只比较长度。
|
||
return false;
|
||
}
|
||
if id == target.window && target.index > pages.len() {
|
||
// 允许插到末尾(等于 len),拒绝超出末尾的过期/无效索引。
|
||
return false;
|
||
}
|
||
}
|
||
true
|
||
}
|
||
/// endor_host 的程序结果展示路径调用,经 ui.open 复用 Presentable 协议及普通值包装规则。
|
||
pub fn open_value(&self, vm: &mut Vm, value: &Value) -> Result<()> {
|
||
// 使用已安装 callable 而不直接调用 new_block,源 Presentable 的覆盖因此仍会执行。
|
||
let function = object(Some(&vm.get_attr(self.ui, "open")?))?;
|
||
// 结果 Page 已由页面注册表持有,本地返回根用完即释放,避免每次 open 泄漏一个根。
|
||
let page = vm.call(function, std::slice::from_ref(value))?;
|
||
vm.release_value(page)?;
|
||
Ok(())
|
||
}
|
||
/// dispatch 的标签事件分支调用,按发布时 Tab ID 找回 Page,再调用可被源代码覆盖的页面方法。
|
||
fn dispatch_tab(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
|
||
// 锁内只解析来源和身份,返回 Page 后释放锁,真正用户方法执行不能持有 Session 锁。
|
||
let page = {
|
||
// 只借用当前窗口已发布的标签映射,不升级成新的页面根;页面注册表仍持有 Page。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
if state.published_version != input.layout_version {
|
||
// 标签画面已经更新,旧 ID/顺序可能不再有效;拒绝后由 GUI 使用新发布数据。
|
||
bail!("event targets an obsolete presentation");
|
||
}
|
||
// input.mount 在标签路径表示 Tab ID,先映回弱 Page,再确认仍在当前窗口 pages 集合,不是 Mount 几何下标。
|
||
let weak = state
|
||
.tab_ids
|
||
.iter()
|
||
.find(|(_, id)| *id == input.mount)
|
||
.map(|(weak, _)| *weak)
|
||
.ok_or_else(|| anyhow::anyhow!("tab has expired"))?;
|
||
// 再在真实窗口注册表中验证弱身份,不能仅凭旧 tab_ids 让已移出的页面继续响应操作。
|
||
let window = object(Some(&vm.get_attr(state.desktop, "window")?))?;
|
||
crate::window::pages(vm.object_store(), window)?
|
||
.iter()
|
||
.copied()
|
||
.find(|page| vm.object_store().downgrade(*page).ok() == Some(weak))
|
||
.ok_or_else(|| anyhow::anyhow!("Page is no longer open"))?
|
||
};
|
||
if let Some(target) = &input.tab_drop {
|
||
// 拖放可能跨窗口;第二段锁只验证目标,避免下方 moveToWindow 源覆盖重入时死锁。
|
||
let window = {
|
||
// 回调前复制目标窗口身份;释放锁后再执行可覆盖的 moveToWindow。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 输入的窗口 ID 必须仍有展示上下文;不能为过期拖动自动重建一个窗口。
|
||
let view = state
|
||
.presentations
|
||
.get(&target.window)
|
||
.ok_or_else(|| anyhow::anyhow!("drop window has closed"))?;
|
||
if view.published_version != target.layout_version {
|
||
// 目标标签布局改变后插入位置不可沿用,即使源页面本身仍存活也要拒绝。
|
||
bail!("drop targets an obsolete presentation");
|
||
}
|
||
object(Some(&vm.get_attr(view.desktop, "window")?))?
|
||
};
|
||
if target.window != input.window {
|
||
// 跨窗口迁移走名义方法分派,给源子类决定是否接受/重定向,而非直接改宿主字典。
|
||
let value =
|
||
invoke_member(vm, page, "moveToWindow", &[Value::Object(window)], 300_000)?;
|
||
vm.release_value(value)?;
|
||
}
|
||
// An override may deliberately decline or redirect the move. Do
|
||
// not reorder an unrelated window after that source decision.
|
||
// 回调可能改写实际归属,必须在调用结束后重新读取,不能沿用最初 target 当作已成功。
|
||
let destination = {
|
||
// 重新加锁读取回调后的真实归属,不沿用迁移前的 Session 借用。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
page_window(vm.object_store(), &state, page)?
|
||
};
|
||
// 源 moveToWindow 可覆盖并拒绝/改投其他窗口;只有确认实际归属仍为预期目标才继续重排与激活。
|
||
if destination == Some(target.window) {
|
||
// 用回调之后的真实顺序定位页面,避免回调中增删标签使旧下标错指其他页面。
|
||
let pages = crate::window::pages(vm.object_store(), window)?;
|
||
// 找不到页面说明迁移过程中它已被关闭/移走;不能继续以默认 0 号位置重排。
|
||
let from = pages
|
||
.iter()
|
||
.position(|candidate| *candidate == page)
|
||
.ok_or_else(|| anyhow::anyhow!("Page is no longer open"))?;
|
||
// 同窗口拖动的 index 是移除前插入缝位置,向后拖时先减一;跨窗口新加入则不用这一偏移修正。
|
||
let position = if input.window == target.window && from < target.index {
|
||
target.index - 1
|
||
} else {
|
||
// 跨窗口插入或向前拖动保留原缝位置,不做同窗移除产生的偏移修正。
|
||
target.index
|
||
};
|
||
if position >= pages.len() {
|
||
// 用户方法改变了目标列表,当前计划位置已不可用,不把它静默夹到末尾。
|
||
bail!("drop destination changed during Page method");
|
||
}
|
||
// 重排同样保留源 moveTab 覆盖;结果是临时拥有值,随后显式释放。
|
||
let value = invoke_member(
|
||
vm,
|
||
page,
|
||
"moveTab",
|
||
&[Value::Number((position as i64).into())],
|
||
300_000,
|
||
)?;
|
||
vm.release_value(value)?;
|
||
// 最后激活被拖页面;若源 activate 另有决定,下一轮刷新以真实 pending 为准。
|
||
let value = invoke_member(vm, page, "activate", &[], 300_000)?;
|
||
vm.release_value(value)?;
|
||
}
|
||
return Ok(());
|
||
}
|
||
// 无拖放目标的标签事件只需一次页面方法调用;普通标签选择统一映射到 activate。
|
||
let method = match input.kind.as_str() {
|
||
"tab-close" => "close",
|
||
"tab-detach" => "detachToWindow",
|
||
_ => "activate",
|
||
};
|
||
// detach 的返回 Window 还可能用于屏幕定位,因此先保存返回根,不立即释放。
|
||
let value = invoke_member(vm, page, method, &[], 1_000_000)?;
|
||
// detach 返回值可能被源覆盖成其他值,仅确为 Window 且带屏幕位置才调用 moveTo;最后统一释放原返回值。
|
||
let placed = (|| -> Result<()> {
|
||
// 只有带坐标且真实返回 ManagedWindow 的拆离操作才移动 OS 资源;其他覆盖返回值正常保留语义。
|
||
if input.kind == "tab-detach"
|
||
&& let Some([x, y]) = input.screen_position
|
||
&& let Value::Object(window) = &value
|
||
&& vm
|
||
.payload::<crate::window::ManagedWindow>(*window, "Window")
|
||
.is_ok()
|
||
{
|
||
// moveTo 也经绑定方法调用,坐标转为语言数字;返回值和外层 detach 返回值分别释放。
|
||
let result = invoke_member(
|
||
vm,
|
||
*window,
|
||
"moveTo",
|
||
&[
|
||
Value::Number((x as i64).into()),
|
||
Value::Number((y as i64).into()),
|
||
],
|
||
100_000,
|
||
)?;
|
||
vm.release_value(result)?;
|
||
}
|
||
Ok(())
|
||
})();
|
||
// 即使 moveTo 出错也先释放 detach 返回根,再传播 placed,防止错误路径泄漏。
|
||
vm.release_value(value)?;
|
||
placed?;
|
||
Ok(())
|
||
}
|
||
|
||
/// endor_host 已选定输入窗口后调用,校验信封并分派宿主交互或托管事件;源代码仍在同一 VM 串行执行。
|
||
pub fn dispatch(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
|
||
// 信封层校验保证坐标/增量等字段满足事件格式,后面仍要验证窗口、版本、Mount 与可见性。
|
||
input
|
||
.validate()
|
||
.map_err(|message| anyhow::anyhow!(message))?;
|
||
if self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.current
|
||
!= input.window
|
||
{
|
||
// current 决定 Session 的隐式字段访问;不匹配就拒绝,避免把另一窗口输入应用到当前页。
|
||
bail!("event belongs to another window");
|
||
}
|
||
// 窗口关闭通过名义 close 方法,保留源覆盖语义;直接调用宿主原生退役只用于故障清理路径。
|
||
if input.kind == "window-close" {
|
||
// 复制 desktop 身份后立即释放锁,下面的 close 源覆盖可以安全重入其他 UI 方法。
|
||
let desktop = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.desktop;
|
||
// Window 是源代码可覆盖的资源对象;不是 winit 的 OS Window。
|
||
let window = object(Some(&vm.get_attr(desktop, "window")?))?;
|
||
// 绑定产生临时拥有值;先保存调用结果,再清理该方法值,保证报错也不跳过释放。
|
||
let bound = vm.bind_attr(window, "close")?;
|
||
// 保存 Result 以便绑定值始终释放;result? 放在释放之后,避免错误路径留下根。
|
||
let result = match &bound {
|
||
Value::Object(function) => vm.call_with_limits(
|
||
*function,
|
||
&[],
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 1_000_000,
|
||
},
|
||
),
|
||
_ => Err(invalid("Window.close is not callable")),
|
||
};
|
||
vm.release_value(bound)?;
|
||
vm.release_value(result?)?;
|
||
return Ok(());
|
||
}
|
||
// 内置工具打开走同一 VM 的 developerTools 能力,返回值只作临时根使用,不跨到 GUI。
|
||
if let Some(name) = input.kind.strip_prefix("developer-open:") {
|
||
// 只传工具名字,页面工厂和工具状态仍在所属 VM 中创建,GUI 不接收托管返回对象。
|
||
let function = object(Some(&vm.get_attr(self.ui, "developerTools")?))?;
|
||
// 工具工厂返回的页面已由托管展示关系持有,调用结束释放这里只为接收返回值而持有的根。
|
||
let page = vm.call(function, &[Value::Text(name.into())])?;
|
||
vm.release_value(page)?;
|
||
return Ok(());
|
||
}
|
||
if matches!(input.kind.as_str(), "page-pan" | "page-zoom") {
|
||
// 整页相机操作使用 page_input 中的模式/坐标检查,不走普通 Block.click。
|
||
return self.dispatch_page_view(vm, input);
|
||
}
|
||
if matches!(input.kind.as_str(), "viewport-pan" | "viewport-zoom") {
|
||
// 局部 CanvasViewport 的相机不同于整页相机,需要按命中出现位置转换增量。
|
||
return self.dispatch_viewport_view(vm, input);
|
||
}
|
||
if matches!(
|
||
input.kind.as_str(),
|
||
"scene-pan" | "scene-orbit" | "scene-zoom"
|
||
) {
|
||
// 三维交互交给场景控制器,保留源更新方法及投影坐标语义。
|
||
return self.dispatch_scene_view(vm, input);
|
||
}
|
||
if matches!(
|
||
input.kind.as_str(),
|
||
"tab-activate" | "tab-close" | "tab-detach" | "tab-drop"
|
||
) {
|
||
// 标签路径的 input.mount 是 Tab ID,不是 Mount;必须在普通目标解析之前分流。
|
||
return self.dispatch_tab(vm, input);
|
||
}
|
||
// 普通指针输入取同一锁内的版本/目标/完整出现候选快照;随后不持 session 锁执行源 hitTest 或事件回调。
|
||
let (weak, desktop, candidates, viewport, surface) = {
|
||
// 一次锁内拍下同版本信息,随后 hitTest/回调在锁外运行,避免读到窗口选择的半次变化。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
if state.published_version != input.layout_version {
|
||
// 弱对象尚存活不代表几何仍适合旧坐标;版本不符时整次普通输入失效。
|
||
bail!("event targets an obsolete presentation");
|
||
}
|
||
(
|
||
*state
|
||
.mounts
|
||
.active
|
||
.get(&input.mount)
|
||
.ok_or_else(|| anyhow::anyhow!("Mount is no longer active"))?,
|
||
state.desktop,
|
||
state.mounts.ordered(),
|
||
state.layout.viewport,
|
||
state.surface.clone(),
|
||
)
|
||
};
|
||
if input.kind == "scroll" {
|
||
// 升级弱 Mount 获得临时拥有根,保证检查/回调期间不会因源代码删关系而悬空。
|
||
let mount = vm
|
||
.upgrade(weak)?
|
||
.ok_or_else(|| anyhow::anyhow!("scroll Mount expired"))?;
|
||
// 暂存滚动处理错误,以便退出前无条件释放升级得到的 Mount 根。
|
||
let result = (|| {
|
||
// 读取这一具体出现位置的几何,不使用共享 Block 上的全局坐标。
|
||
let data = vm.payload::<crate::mount::Mount>(mount, "Mount")?;
|
||
// scroll 必须有指针位置,才能验证是否仍处于裁剪与页面视口内。
|
||
let point = input
|
||
.point
|
||
.ok_or_else(|| anyhow::anyhow!("scroll has no pointer"))?;
|
||
// scroll 不只校验 Mount 身份,还要指针仍在页面视口和出现裁剪中;相机/面板映射后的增量才可交给 scrollBy。
|
||
if !data.mounted
|
||
|| data.version != input.layout_version
|
||
|| !surface.contains(point, viewport)
|
||
|| !data
|
||
.geometry
|
||
.contains(crate::geometry::Point::new(point[0], point[1]))
|
||
{
|
||
bail!("scroll targets an obsolete or clipped Mount");
|
||
}
|
||
// 屏幕增量还需转换成 Mount 所属空间,不能直接当作内容滚动的逻辑距离。
|
||
let delta = input
|
||
.delta
|
||
.ok_or_else(|| anyhow::anyhow!("scroll has no delta"))?;
|
||
let Some(delta) = data.geometry.logical_delta(input.point, delta) else {
|
||
// 投影不可逆/无法形成有效局部增量时忽略此轮,不伪造一个零或屏幕空间增量。
|
||
return Ok(());
|
||
};
|
||
// 通过托管方法保留滚动协议,调用预算单独有界;方法根与结果根随后各自释放。
|
||
let method = vm.bind_attr(mount, "scrollBy")?;
|
||
// 数量/类型之外的滚动规则交给该方法;临时绑定值在 result 成功/失败两种情况下都释放。
|
||
let result = match method {
|
||
Value::Object(function) => vm.call_with_limits(
|
||
function,
|
||
&[
|
||
Value::Number((delta[0] as f64).into()),
|
||
Value::Number((delta[1] as f64).into()),
|
||
],
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 100_000,
|
||
},
|
||
),
|
||
_ => Err(invalid("scrollBy is not callable")),
|
||
};
|
||
vm.release_value(method)?;
|
||
vm.release_value(result?)?;
|
||
Ok(())
|
||
})();
|
||
// 滚动失败也清理临时根;只有释放后才把保存的 result 交回运行时。
|
||
vm.release(mount)?;
|
||
return result;
|
||
}
|
||
// 坐标点击重新执行源协议选择按下和释放目标,二者同出现才分发;无坐标输入使用已捕获的弱目标。
|
||
let mount = if let (Some(press), Some(release)) = (input.press_point, input.point) {
|
||
if [press, release]
|
||
.iter()
|
||
.any(|point| !surface.contains(*point, viewport))
|
||
{
|
||
// 按下或释放任一点已离开可见页,不能仅凭旧 Mount 身份触发点击。
|
||
return Ok(());
|
||
}
|
||
let Some(mount) = crate::source_hit::resolve(vm, desktop, candidates, press, release)?
|
||
else {
|
||
// resolve 会运行真实源 hitTest;没有共同有效命中目标是正常无事件结果。
|
||
return Ok(());
|
||
};
|
||
mount
|
||
} else {
|
||
// 无坐标输入已有捕获目标,升级弱身份后仍要在下面检查 mounted/interactive。
|
||
vm.upgrade(weak)?
|
||
.ok_or_else(|| anyhow::anyhow!("Mount has expired"))?
|
||
};
|
||
// 同一所有权模式覆盖三维拾取、指针和普通事件:保存任何错误,最后统一 release(mount)。
|
||
let result = (|| {
|
||
// 复制 payload 结束 VM 只读借用,后续分配事件或执行用户方法需要可变借用 VM。
|
||
let data = vm.payload::<crate::mount::Mount>(mount, "Mount")?.clone();
|
||
// 3D 视口点击先生成 RayHit3D 并附到 Event.hit,再沿视口出现传播,不能只按二维外框假定击中某网格。
|
||
if data.geometry.scene3d_viewport
|
||
&& input.kind == "click"
|
||
&& let Some(point) = input.point
|
||
{
|
||
// 用原生 callable 承接 3D 拾取与事件传播;captures 显式保活 Mount 与 desktop。
|
||
let function = vm.allocate(ContextNativeFunction::new(
|
||
"3D pointer click",
|
||
crate::scene3d_model::PointerPick {
|
||
mount,
|
||
desktop,
|
||
point: crate::geometry::Point::new(point[0], point[1]),
|
||
},
|
||
vec![mount, desktop],
|
||
))?;
|
||
// 先暂存结果,释放临时 callable 根后再传播错误,保持失败路径也无额外根残留。
|
||
let result = vm.call_with_limits(
|
||
function,
|
||
&[],
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 1_000_000,
|
||
},
|
||
);
|
||
vm.release(function)?;
|
||
vm.release_value(result?)?;
|
||
return Ok(());
|
||
}
|
||
if !data.mounted {
|
||
// 对象可达但已经卸载,不能把旧事件传给仍被其他地方引用的业务 Block。
|
||
bail!("event targets an obsolete presentation");
|
||
}
|
||
if !data.geometry.interactive {
|
||
// 几何可能是装饰或明确禁交互;不能因为外框命中就默认接受所有输入。
|
||
bail!("Mount does not accept input");
|
||
}
|
||
if input.kind.starts_with("pointer") {
|
||
// 原始指针事件用于图形交互,保留具体 kind 与坐标,不转换为高层 click。
|
||
let p = input
|
||
.point
|
||
.ok_or_else(|| anyhow::anyhow!("pointer needs point"))?;
|
||
// PointerDispatch 在 VM 中执行并显式捕获所有托管依赖,不从 GUI 线程运行源处理器。
|
||
let f = vm.allocate(ContextNativeFunction::new(
|
||
"native pointer event",
|
||
crate::bridge::graphics::PointerDispatch {
|
||
owner: data.block,
|
||
mount,
|
||
desktop,
|
||
kind: input.kind.clone(),
|
||
point: crate::geometry::Point::new(p[0], p[1]),
|
||
},
|
||
vec![data.block, mount, desktop],
|
||
))?;
|
||
// 一次有界的 VM 调用完成指针分派;先释放 callable 根,再传播执行错误。
|
||
let result = vm.call_with_limits(
|
||
f,
|
||
&[],
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 1_000_000,
|
||
},
|
||
);
|
||
vm.release(f)?;
|
||
vm.release_value(result?)?;
|
||
return Ok(());
|
||
}
|
||
// 普通点击/焦点先登记展示焦点请求,再按 kind 调 Block.click 或 Mount.requestFocus;生效仍需后续完整刷新。
|
||
vm.set_attr(
|
||
desktop,
|
||
"focus",
|
||
Value::Number((data.geometry.id as i64).into()),
|
||
)?;
|
||
vm.set_attr(desktop, "focusRequested", Value::Bool(true))?;
|
||
// click 的 receiver 是 Block,Mount 作为出现上下文;focus 的 receiver 则直接是 Mount。
|
||
let (owner, name, args) = match input.kind.as_str() {
|
||
"click" => (data.block, "click", vec![Value::Object(mount)]),
|
||
"focus" => (mount, "requestFocus", vec![]),
|
||
_ => bail!("unsupported native event"),
|
||
};
|
||
// 保留源方法覆盖;临时绑定值需要在调用成功与失败两种情况下都释放。
|
||
let bound = vm.bind_attr(owner, name)?;
|
||
// 临时绑定保持 receiver;错误同样先保存,释放绑定值后才传播。
|
||
let result = match &bound {
|
||
Value::Object(function) => vm.call_with_limits(
|
||
*function,
|
||
&args,
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 1_000_000,
|
||
},
|
||
),
|
||
_ => Err(invalid("event method is not callable")),
|
||
};
|
||
vm.release_value(bound)?;
|
||
vm.release_value(result?)?;
|
||
Ok(())
|
||
})();
|
||
vm.release(mount)?;
|
||
result
|
||
}
|
||
/// endor_host 在执行/清理边界调用,压缩节点弱表和每窗快照缓存;不改写业务对象拥有关系。
|
||
pub fn reclaim(&self, vm: &mut Vm, _roots: &mut Vec<ObjectRef>) -> Result<()> {
|
||
// 先回收托管环,再扫描弱节点表,只保留能升级的身份;render_cache.prune 同步清除死亡业务索引。
|
||
vm.collect_cycles();
|
||
// VM 环回收结束后再压缩原生弱表,避免用回收前仍能升级的状态保留多余缓存索引。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 重建表只保留当前能升级的弱身份;不能把升级得到的临时强根永久留在宿主索引中。
|
||
let mut live = Vec::new();
|
||
for weak in &state.nodes {
|
||
if let Some(object) = vm.upgrade(*weak)? {
|
||
// 升级是有拥有权的 VM 操作,探测结束立即释放,再保存原弱引用。
|
||
vm.release(object)?;
|
||
live.push(*weak);
|
||
}
|
||
}
|
||
state.nodes = live;
|
||
for presentation in state.presentations.values_mut() {
|
||
// 同一业务节点可能缓存于多个窗口,逐窗剪枝,避免只清理 current 的派生状态。
|
||
presentation.render_cache.prune(vm.object_store());
|
||
}
|
||
Ok(())
|
||
}
|
||
/// endor_host 的输入/刷新失败路径调用,修复未发布 pending;不会回滚 REPL 中已成功完成的业务赋值。
|
||
pub fn discard_pending(&self, vm: &mut Vm) -> Result<()> {
|
||
// 修复宿主 pending/注册关系,不运行用户方法;因此持锁逐窗处理不会进入回调重入路径。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
for presentation in state.presentations.values_mut() {
|
||
// 真实 Window 注册表是归属依据,不能单靠失败前的 Presentation.active 恢复页面。
|
||
let window = object(Some(&vm.get_attr(presentation.desktop, "window")?))?;
|
||
// A failed first render must not become visible on the next input
|
||
// merely because show() already registered its Page with the Window.
|
||
// 第一次 render 失败时撤销尚未发布的 Page 登记,避免下一次无关输入把失败页意外显示出来。
|
||
if presentation.active.is_none() && presentation.last_tabs.is_empty() {
|
||
// 从未发布过的窗口不保留失败 render 偶然注册的 Page;拷贝后逐项移除,避免迭代时改集合。
|
||
let unpublished = crate::window::pages(vm.object_store(), window)?.to_vec();
|
||
for page in unpublished {
|
||
// 使用宿主撤销入口,不再执行可能再次失败或产生副作用的源页面方法。
|
||
crate::window::unregister_page_host(vm, window, page)?;
|
||
}
|
||
}
|
||
// 撤销首次失败登记后重新读表;先前成功的 close/move 仍可能已改变现有窗口的拥有关系。
|
||
let pages = crate::window::pages(vm.object_store(), window)?;
|
||
// 失败输入已成功执行的关闭/迁移语句不回滚;重新选择仍在真实窗口注册表中的页,修复 pending 与拥有关系。
|
||
let next = if presentation.closing {
|
||
// 正在关闭的资源不能为了恢复画面而重新挑一页继续展示。
|
||
None
|
||
} else {
|
||
// 原 active 仍登记就保留,否则选择当前真实列表首项;空表自然得到 None。
|
||
presentation
|
||
.active
|
||
.filter(|page| pages.contains(page))
|
||
.or_else(|| pages.first().copied())
|
||
};
|
||
// Earlier successful statements in a failed REPL input remain live.
|
||
// Repair ownership changes before the next publication so a closed or
|
||
// moved Page cannot survive in an old context's mount dictionary.
|
||
presentation.pending = if next != presentation.active || presentation.closing {
|
||
// Some(None) 是明确卸载旧页,不等同于无刷新请求的 None。
|
||
Some(next)
|
||
} else {
|
||
// 无需改变活动页时清掉失败请求,避免后续输入重复展示失败候选。
|
||
None
|
||
};
|
||
vm.set_attr(
|
||
presentation.desktop,
|
||
"pending",
|
||
presentation
|
||
.pending
|
||
.flatten()
|
||
.map(Value::Object)
|
||
.unwrap_or(Value::Nil),
|
||
)?;
|
||
if presentation
|
||
.active
|
||
.is_some_and(|page| vm.object(page).is_err())
|
||
{
|
||
// active 的托管对象已不存在时去除 Rust 借用句柄,不能让下一轮继续拿它提取快照。
|
||
presentation.active = None;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
/// endor_host 在刷新稳定循环中调用,重算失效绑定;回调可继续改变对象,随后 prepare_frame 才排空变化。
|
||
pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> {
|
||
// 只在锁内快照弱节点与宿主桌面集合,实际绑定回调在锁外执行,避免回调访问 UI 时死锁。
|
||
let (nodes, desktops) = {
|
||
// 锁内只复制弱索引与排除集合,锁外 binding::refresh 才真正执行源计算。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
(state.nodes.clone(), state.host_desktops())
|
||
};
|
||
// binding::refresh 在所属 VM 中运行受控计算并返回实际评估次数,不把节点总数当作工作量。
|
||
let evaluated = crate::binding::refresh(vm, &nodes, &desktops)?;
|
||
// 计算回调已返回,重新短暂加锁累计次数,不把会话锁贯穿用户代码。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 诊断计数饱和增长,不让长期会话的统计溢出反过来打断有效程序执行。
|
||
state.binding_evaluations = state.binding_evaluations.saturating_add(evaluated);
|
||
Ok(())
|
||
}
|
||
/// endor_host 每轮刷新调用,广播 VM 变化后为 current 决定无工作、空帧或带视觉根的新一轮布局。
|
||
pub fn prepare_frame(&self, vm: &mut Vm) -> Result<Option<Option<Arc<UiNode>>>> {
|
||
// 一次排空变更必须广播所有 Presentation,不能让先刷新的窗口吃掉其他窗口共享业务数据的失效通知。
|
||
let changes = vm.take_changes();
|
||
// 本锁内广播变化与提取只读快照,保证 current 根与各窗口的失效观察来自同一批更新。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 本轮目标优先 pending,包括显式清空;快照提取返回 None 代表目标无页面,不等于无需刷新。
|
||
let active = state.pending.unwrap_or(state.active);
|
||
// One VM change drain feeds every presentation before selecting a root.
|
||
// Otherwise a window refreshed first could consume another window's work.
|
||
for presentation in state.presentations.values_mut() {
|
||
// 每个窗口都必须先观察同一批变化,即使本轮最后只为 current 提取一个根。
|
||
presentation.observe_changes(vm, &changes);
|
||
}
|
||
// 没页面得到 Ok(None);有页面但快照验证失败则传播 Err,不能伪装成正常空帧。
|
||
let root = active
|
||
.map(|v| state.render_cache.snapshot(vm.object_store(), v))
|
||
.transpose()?;
|
||
// 视觉缓存用 Arc 身份快速判断根未变;即使根相同,焦点、滚动、标签或源依赖仍可能要求重绘。
|
||
let same = match (&root, &state.last_snapshot) {
|
||
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
|
||
(None, None) => true,
|
||
_ => false,
|
||
};
|
||
// 焦点请求是宿主控制状态,视觉树即使复用同一 Arc 也需要一次完整帧处理。
|
||
let focus_changed = matches!(
|
||
vm.get_attr(state.desktop, "focusRequested"),
|
||
Ok(Value::Bool(true))
|
||
);
|
||
// 标签标题/顺序可独立于内容根变化,所以投影后与上次提交结果单独比较。
|
||
let tabs = project_tabs(vm, &mut state)?;
|
||
// 汇总显式展示请求、视觉树、焦点/滚动及强制失效;任一变化都不能用 Arc 相同快速跳过。
|
||
let changed = state.pending.is_some()
|
||
|| !same
|
||
|| focus_changed
|
||
|| matches!(
|
||
vm.get_attr(state.desktop, "scrollRequested"),
|
||
Ok(Value::Bool(true))
|
||
)
|
||
|| state.force_frame
|
||
|| tabs != state.last_tabs;
|
||
if changed {
|
||
// 只在确有新帧工作时记录快照诊断,避免空闲轮询覆盖最后一次有效失效原因。
|
||
if !same {
|
||
// 根身份变化才归因于 snapshot;仅标签/焦点变更不伪造快照失效记录。
|
||
state.last_invalid_dependency =
|
||
format!("snapshot {}", state.render_cache.last_invalidation);
|
||
}
|
||
state.snapshot_visited = state.render_cache.visited;
|
||
state.snapshot_reused = state.render_cache.reused;
|
||
}
|
||
// 双层 Option 三态:外 None 无工作;Some(None) 需提交空帧;Some(Some(root)) 需布局/绘制根。
|
||
Ok(changed.then_some(root))
|
||
}
|
||
/// 布局完成路径调用,提交桥接内部几何和生命周期,供后续 paint 使用;外部发布由 endor_host 稳定循环完成。
|
||
pub fn commit_frame(
|
||
&self,
|
||
vm: &mut Vm,
|
||
snapshot: Option<Arc<UiNode>>,
|
||
frame: &crate::document::Frame,
|
||
) -> Result<()> {
|
||
// 同步 Mount 和更新桥接状态时需要排他访问;调用用户 hooks 前显式 drop(state)。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// pending 的 Some(None) 要清空展示,不能简单 or(active) 又把旧页取回来。
|
||
let page = state.pending.unwrap_or(state.active);
|
||
// synchronize 必须使用本轮选中窗口的私有资源域,而不是 master 的全局注册表。
|
||
let desktop = state.desktop;
|
||
// 同步过程可能升级节点,用弱表快照避免同时借用 Session 的其他可变字段。
|
||
let nodes = state.nodes.clone();
|
||
// 提交几何同步清除临时 layout Context,并产生挂载/卸载序列;提交桥接状态不等于已写 GUI 共享文档。
|
||
let hooks = state
|
||
.mounts
|
||
.synchronize(vm, desktop, page, nodes, frame, false)?;
|
||
// 暂存提交/钩子错误,无论中途哪一步失败都经过下方 hooks 根释放。
|
||
let result = (|| {
|
||
if snapshot.is_none() {
|
||
// 空帧不应残留旧出现几何,否则下一次输入可能命中已经退场的内容。
|
||
state.layout.clear_occurrences();
|
||
}
|
||
if let Some(page) = state.pending.take() {
|
||
// 消费本轮请求;没有请求就沿用 active,不凭快照的 Option 擅自改业务页面身份。
|
||
state.active = page;
|
||
}
|
||
vm.set_attr(state.desktop, "pending", Value::Nil)?;
|
||
vm.set_attr(state.desktop, "focusRequested", Value::Bool(false))?;
|
||
vm.set_attr(state.desktop, "scrollRequested", Value::Bool(false))?;
|
||
state.force_frame = false;
|
||
state.last_snapshot = snapshot;
|
||
state.last_tabs = frame.tabs.clone();
|
||
state.surface = frame.surface.clone();
|
||
// 让随后源回调看到本轮几何与版本,但真正对外原子发布要等整个刷新循环稳定,回调可能继续触发下一轮。
|
||
state.published_version = frame.layout_version;
|
||
// Hooks may call Page/container APIs; never hold the session mutex
|
||
// across user code. Publish only after the refresh loop settles.
|
||
drop(state);
|
||
if let Some(hooks) = hooks {
|
||
// 用户钩子可能再次修改页面,执行后 endor_host 仍会检查是否需要下一轮刷新。
|
||
let value = vm
|
||
.call_with_limits(
|
||
hooks,
|
||
&[],
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 1_000_000,
|
||
},
|
||
)
|
||
.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
|
||
vm.release_value(value)?;
|
||
}
|
||
Ok(())
|
||
})();
|
||
if let Some(hooks) = hooks {
|
||
// 调用成功、VM 报错或提交字段写入失败都释放驱动拥有根,不能依赖 Rust Option 自动清理。
|
||
vm.release(hooks)?;
|
||
}
|
||
result
|
||
}
|
||
|
||
/// endor_host 在 LayoutProgress::Complete 后调用,按帧的出现顺序启动第一片源 paint 工作。
|
||
pub fn begin_paint_frame(
|
||
&self,
|
||
vm: &mut Vm,
|
||
root: Option<Arc<UiNode>>,
|
||
frame: crate::document::Frame,
|
||
) -> Result<crate::source_paint::Progress> {
|
||
let (desktop, mounts) = {
|
||
// 取绘制顺序与弱身份只读 Session,source_paint::start 在锁外执行 VM 协议。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 按最终帧绘制顺序查活动 Mount 弱身份,每条都必须存在;不按 HashMap 顺序调用源 paint。
|
||
let mounts = frame
|
||
.mounts
|
||
.iter()
|
||
.map(|mount| {
|
||
// 以 frame.mounts 的顺序查活动弱身份;缺项代表布局/挂载不一致,必须报错而非漏画。
|
||
state
|
||
.mounts
|
||
.active
|
||
.get(&mount.id)
|
||
.copied()
|
||
.map(|weak| (mount.id, weak))
|
||
.ok_or_else(|| anyhow::anyhow!("paint Mount is unavailable"))
|
||
})
|
||
.collect::<Result<Vec<_>>>()?;
|
||
(state.desktop, mounts)
|
||
};
|
||
// 每片预算 256,由 source_paint 保留未完成续体;此处既可能返回 Draft,也可能返回 Work。
|
||
let progress = crate::source_paint::start(vm, desktop, mounts, root, frame, 256)?;
|
||
self.accept_paint_progress(progress)
|
||
}
|
||
|
||
/// endor_host 保存 PendingPaint 后调用,在同一 VM 上继续最多一片工作,不从头重放 paint 源方法。
|
||
pub fn resume_paint_frame(
|
||
&self,
|
||
vm: &mut Vm,
|
||
work: crate::source_paint::Work,
|
||
) -> Result<crate::source_paint::Progress> {
|
||
// Work 拥有前片续体和草稿,消费后返回新的挂起 Work 或最终完整 Draft。
|
||
let progress = work.resume(vm, 256)?;
|
||
self.accept_paint_progress(progress)
|
||
}
|
||
|
||
/// begin_paint_frame/resume_paint_frame 共用收尾,只有 Complete 才更新展示域的完整 paint 依赖。
|
||
fn accept_paint_progress(
|
||
&self,
|
||
progress: crate::source_paint::Progress,
|
||
) -> Result<crate::source_paint::Progress> {
|
||
if let crate::source_paint::Progress::Complete(draft) = &progress {
|
||
// 借用 Draft 而非消费,调用方仍需把它交回运行时进行验证和文档发布。
|
||
// 只有完整 Draft 写入依赖,Pending 不改变上一轮已提交读取集合。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 完成后过滤所有窗口私有 desktop 读取,再拆对象版本与名称依赖;挂起切片不覆盖上一份完成的 paint 依赖。
|
||
let desktops = state.host_desktops();
|
||
// 私有 desktop 是宿主记账读,排除后才得到源程序真正依赖的属性/名字集合。
|
||
let reads = draft
|
||
.reads
|
||
.iter()
|
||
.filter(|read| !desktops.contains(&read.object()))
|
||
.cloned()
|
||
.collect();
|
||
// 按对象版本读取与命名查找拆分,下一轮 observe_changes 使用各自的失效判断。
|
||
let (reads, names) = crate::presentation::split_reads(reads);
|
||
state.paint_reads = reads;
|
||
state.paint_names = names;
|
||
state.paint_slices = draft.slices;
|
||
}
|
||
Ok(progress)
|
||
}
|
||
|
||
/// endor_host 的 resize/输入替换/退役路径调用,取消所属 VM 的 paint 续体并强制该窗口重新刷新。
|
||
pub fn cancel_paint_frame(
|
||
&self,
|
||
vm: &mut Vm,
|
||
window: u64,
|
||
work: crate::source_paint::Work,
|
||
) -> Result<()> {
|
||
// 先尝试取消 VM 续体,再尝试选原窗口并强制下一帧;前一操作失败也执行必要的宿主失效准备。
|
||
let cancelled = work.cancel(vm);
|
||
// 这一步即使 cancelled 是 Err 也先尝试;否则失败的取消会留下“旧帧看似仍有效”的缓存状态。
|
||
let invalidated = self.select_window(window).and_then(|()| {
|
||
self.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.force_frame = true;
|
||
Ok(())
|
||
});
|
||
// 所有必要清理尝试完成后再按顺序传播错误,不让前一个 ? 跳过后一个失效操作。
|
||
cancelled?;
|
||
invalidated
|
||
}
|
||
|
||
/// endor_host 从 prepare_frame 得到新根后调用,建立出现结构和恢复检查点,再开始有预算的源布局。
|
||
pub fn begin_layout_frame(
|
||
&self,
|
||
vm: &mut Vm,
|
||
root: Option<Arc<UiNode>>,
|
||
) -> Result<LayoutProgress> {
|
||
// 一个 VM 同时只能推进原来的续体;不能在仍只读/挂起时再开另一帧破坏调用与恢复上下文。
|
||
anyhow::ensure!(
|
||
!vm.has_suspended_call() && !vm.object_store().is_read_only(),
|
||
"cannot prepare another frame during suspended layout"
|
||
);
|
||
// 显式空页面无需源布局,清测量/布局读取并推进空帧版本,再同步退役旧 Mount;不是无工作返回。
|
||
let Some(root) = root else {
|
||
// 空帧已没有可复用的测量结果和源布局依赖,分三项清理,不能继续引用旧页读取集合。
|
||
self.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.measure_cache
|
||
.clear();
|
||
self.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.layout_reads
|
||
.clear();
|
||
self.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.layout_names
|
||
.clear();
|
||
// 空帧仍有窗口、标签和新版本:GUI 必须能识别旧出现位置已全部退场。
|
||
let frame = {
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 不复用旧版本,否则旧输入可能被当成对应这个空页面的有效事件。
|
||
state.layout.version = state
|
||
.layout
|
||
.version
|
||
.checked_add(1)
|
||
.ok_or_else(|| anyhow::anyhow!("layout version exhausted"))?;
|
||
crate::document::Frame {
|
||
window: state.current,
|
||
layout_version: state.layout.version,
|
||
tabs: project_tabs(vm, &mut state)?,
|
||
..crate::document::Frame::default()
|
||
}
|
||
};
|
||
// 走共同提交路径退役 Mount/运行卸载钩子,不能仅返回一个默认空 Frame 留下旧生命周期。
|
||
self.commit_frame(vm, None, &frame)?;
|
||
return Ok(LayoutProgress::Complete(Box::new(frame)));
|
||
};
|
||
// 准备私有布局草稿与缓存副本,之后的源调用在锁外运行;正式状态等完成时再写回。
|
||
let (mut layout, cache, desktop, page, nodes) = {
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
(
|
||
state.layout.clone(),
|
||
state.measure_cache.clone(),
|
||
state.desktop,
|
||
state.pending.unwrap_or(state.active),
|
||
state.nodes.clone(),
|
||
)
|
||
};
|
||
// 标签投影会更新稳定 ID 映射,因此短暂可变借用 Session,而非沿用上面的只读锁。
|
||
let tabs = {
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
project_tabs(vm, &mut state)?
|
||
};
|
||
// 记录真实原生页面视口供页面相机 clamp;兼容 Page.mode 只改变根约束,不永久篡改保存的窗口尺寸。
|
||
let full_viewport = layout.viewport;
|
||
// 根未声明特殊表面时采用默认页面模式;保存副本用于完成时添加范围和相机 clamp。
|
||
let surface = root.surface.clone().unwrap_or_default();
|
||
layout.page_mode = root.surface.as_ref().map(|surface| surface.mode);
|
||
{
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 按全会话高水位分配结构出现 ID,取消后也不回收已暴露过的数值身份。
|
||
layout.reserve_identity_floor(state.next_mount);
|
||
}
|
||
// 先建立出现结构与 ID,尚未执行源 measure/arrange;结构供 Mount 准备与协议调用查上下文。
|
||
let mut frame = layout.structure(&root)?;
|
||
{
|
||
// 本轮已暴露的 ID 即使后续取消也不能回收,所以马上推进全会话高水位,而非等最终提交。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
state.next_mount = state.next_mount.max(layout.identity_high_watermark());
|
||
frame.window = state.current;
|
||
}
|
||
// 同步准备阶段可能暂改 Mount;把可恢复检查点、延后钩子及有序弱身份一起交给后续工作。
|
||
let (hooks, mounts, checkpoint) = {
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 在准备态 synchronize 改写 Mount 前保留旧字典与 payload;准备失败立即恢复,不能留下半套父子关系。
|
||
let checkpoint = state.mounts.checkpoint(vm, desktop)?;
|
||
// true 表示准备模式:Mount 关系可供布局读取,但生命周期钩子延后,不在此执行用户代码。
|
||
let hooks = match state
|
||
.mounts
|
||
.synchronize(vm, desktop, page, nodes, &frame, true)
|
||
{
|
||
Ok(hooks) => hooks,
|
||
Err(error) => {
|
||
// 即使结构准备失败也先恢复旧关系;恢复失败本身也必须作为错误报告。
|
||
checkpoint.restore(vm, desktop, &mut state.mounts)?;
|
||
return Err(error.into());
|
||
}
|
||
};
|
||
(hooks, state.mounts.ordered(), checkpoint)
|
||
};
|
||
// 检查点、暂存 hooks、所属窗口与根随源布局 Work 一起保留,跨量子不能只留下 VM 续体。
|
||
let context = LayoutContext {
|
||
surface,
|
||
root: root.clone(),
|
||
tabs,
|
||
full_viewport,
|
||
desktop,
|
||
window: frame.window,
|
||
hooks,
|
||
checkpoint,
|
||
};
|
||
// Draft 携带布局求解用的私有 runtime/cache;reads 从空开始收集本轮实际依赖。
|
||
let draft = crate::source_layout::Draft {
|
||
frame,
|
||
runtime: layout,
|
||
cache,
|
||
reads: Vec::new(),
|
||
};
|
||
// 暂存 Result 而不立即 ?,让 accept_layout_progress 在出错时仍能恢复 context 的检查点。
|
||
let progress = crate::source_layout::start(vm, desktop, mounts, root, draft, 256);
|
||
self.accept_layout_progress(vm, context, progress)
|
||
}
|
||
|
||
/// endor_host 在布局/绘制挂起让出 worker 时调用,累计诊断次数,不改变下一片执行预算。
|
||
pub fn note_refresh_yield(&self) -> Result<()> {
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 统计饱和而非溢出报错,避免长期运行时仅因诊断计数达到上限而中断。
|
||
state.refresh_yields = state.refresh_yields.saturating_add(1);
|
||
Ok(())
|
||
}
|
||
|
||
/// endor_host 重定向刷新时调用,分别记录重启以及本轮是否确实取消 layout/paint,供 stats 诊断。
|
||
pub fn note_refresh_restart(
|
||
&self,
|
||
cancelled_layout: bool,
|
||
cancelled_paint: bool,
|
||
) -> Result<()> {
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 一次重启不一定同时取消两个阶段;布尔转 usize 让各计数只增加实际发生的事件。
|
||
state.refresh_restarts = state.refresh_restarts.saturating_add(1);
|
||
state.layout_cancels = state
|
||
.layout_cancels
|
||
.saturating_add(usize::from(cancelled_layout));
|
||
state.paint_cancels = state
|
||
.paint_cancels
|
||
.saturating_add(usize::from(cancelled_paint));
|
||
Ok(())
|
||
}
|
||
|
||
/// cancel_layout_frame/布局出错路径调用,恢复所属窗口的检查点并释放尚未执行的生命周期驱动。
|
||
fn discard_layout_context(&self, vm: &mut Vm, context: LayoutContext) -> Result<()> {
|
||
// 消费 context 后只提取恢复所需的资源,树/标签等纯 Rust 值自然释放,不执行用户回调。
|
||
let LayoutContext {
|
||
surface: _,
|
||
desktop,
|
||
window,
|
||
hooks,
|
||
checkpoint,
|
||
..
|
||
} = context;
|
||
// 恢复必须在原窗口;若窗口或 session 锁已不可用,至少释放检查点拥有根,不能尝试恢复到另一个窗口。
|
||
let selected = self.select_window(window);
|
||
let restored = if selected.is_ok() {
|
||
// 选窗成功也可能锁损坏,分别处理“可恢复”和“只能释放检查点根”两种情况。
|
||
match self.session.lock() {
|
||
Ok(mut state) => {
|
||
// restore 还原准备阶段暂改的 Mount 字典/payload;随后强制下一帧重新建立几何。
|
||
let result = checkpoint.restore(vm, desktop, &mut state.mounts);
|
||
state.force_frame = true;
|
||
result.map_err(anyhow::Error::from)
|
||
}
|
||
// 锁不可用时仍先释放 VM 拥有根,再报告会话损坏,不能泄漏也不能假装恢复成功。
|
||
Err(_) => checkpoint
|
||
.release(vm)
|
||
.map_err(anyhow::Error::from)
|
||
.and(Err(anyhow::anyhow!("UI session poisoned"))),
|
||
}
|
||
} else {
|
||
// 原窗口已不可选择,不拿 current 的其他窗口去恢复旧检查点,仅清理拥有权。
|
||
checkpoint.release(vm).map_err(anyhow::Error::from)
|
||
};
|
||
// 无论选择/恢复是否失败都先尝试释放延迟 hooks;保存结果后再按优先级传播错误。
|
||
// 不运行待取消的钩子;Option<Result> 转置后保存错误,确保上面的恢复尝试已完成。
|
||
let released = hooks.map(|hooks| vm.release(hooks)).transpose();
|
||
selected?;
|
||
restored?;
|
||
released?;
|
||
Ok(())
|
||
}
|
||
|
||
/// endor_host 取消/重定向刷新时调用,分别取消 owning VM 续体与恢复准备态 Mount,保留外部已发布帧。
|
||
pub fn cancel_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> {
|
||
// Work 和 Context 绑定为一对,不能只取消 VM 调用而丢下临时挂载关系。
|
||
let PendingLayout { work, context } = pending;
|
||
// 取消只读执行与恢复 Mount 是两件事,均先尝试;没有继续执行原续体时也不能漏掉检查点恢复。
|
||
let cancelled = work.cancel(vm);
|
||
let restored = self.discard_layout_context(vm, context);
|
||
cancelled?;
|
||
restored
|
||
}
|
||
|
||
/// endor_host 撤销失败候选布局时调用,与正常取消共用同一清理实现,不另起一轮布局回放源代码。
|
||
pub fn abort_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> {
|
||
self.cancel_layout_frame(vm, pending)
|
||
}
|
||
|
||
/// endor_host 持有 PendingLayout 后调用,在同一 VM、原窗口和原检查点上恢复最多一片布局工作。
|
||
pub fn resume_layout_frame(
|
||
&self,
|
||
vm: &mut Vm,
|
||
pending: PendingLayout,
|
||
) -> Result<LayoutProgress> {
|
||
// 解包只转移拥有权,仍将这一份 context 原样传到完成/错误收尾,不能重建新的检查点。
|
||
let PendingLayout { work, context } = pending;
|
||
// 恢复时原窗口不存在则取消 owning VM 调用并清上下文,不重新建窗口或重放源布局。
|
||
if let Err(error) = self.select_window(context.window) {
|
||
// 先保存原选择错误,取消与恢复均尝试执行,最后按清理结果和原错误顺序返回。
|
||
let cancelled = work.cancel(vm);
|
||
let restored = self.discard_layout_context(vm, context);
|
||
cancelled?;
|
||
restored?;
|
||
return Err(error);
|
||
}
|
||
// 不在这里用 ? 丢掉错误,否则失败续体配套的 context 没有机会恢复/释放。
|
||
let progress = work.resume(vm, 256);
|
||
self.accept_layout_progress(vm, context, progress)
|
||
}
|
||
|
||
/// begin_layout_frame/resume_layout_frame 共用入口,把源布局结果转成桥接进度,集中处理恢复和清理。
|
||
fn accept_layout_progress(
|
||
&self,
|
||
vm: &mut Vm,
|
||
context: LayoutContext,
|
||
progress: Result<crate::source_layout::Progress>,
|
||
) -> Result<LayoutProgress> {
|
||
match progress {
|
||
// Pending 原样捆绑 Work 与 Context;Complete 才投影、提交,Error 则撤销准备态上下文。
|
||
Ok(crate::source_layout::Progress::Pending(work)) => {
|
||
Ok(LayoutProgress::Pending(Box::new(PendingLayout {
|
||
work,
|
||
context,
|
||
})))
|
||
}
|
||
Ok(crate::source_layout::Progress::Complete(draft)) => self
|
||
.finish_layout_frame(vm, context, draft)
|
||
.map(|frame| LayoutProgress::Complete(Box::new(frame))),
|
||
Err(error) => {
|
||
// 源布局失败不能发布 draft;先撤销准备态关系并释放检查点/钩子,再传播执行错误。
|
||
self.discard_layout_context(vm, context)?;
|
||
Err(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// accept_layout_progress 收到 Complete 时调用,投影最终几何、保存依赖和提交 Mount,交回待绘制帧。
|
||
fn finish_layout_frame(
|
||
&self,
|
||
vm: &mut Vm,
|
||
context: LayoutContext,
|
||
draft: crate::source_layout::Draft,
|
||
) -> Result<crate::document::Frame> {
|
||
// 消费开始阶段保存的上下文,所有元数据都来自这一轮,不能临时从另一窗口重取。
|
||
let LayoutContext {
|
||
mut surface,
|
||
root,
|
||
tabs,
|
||
full_viewport,
|
||
desktop,
|
||
window,
|
||
hooks,
|
||
checkpoint,
|
||
} = context;
|
||
// 消费完整求解结果;reads/cache 在生命周期回调前保存,才能发现钩子对布局依赖的新修改。
|
||
let crate::source_layout::Draft {
|
||
mut frame,
|
||
runtime: mut layout,
|
||
mut cache,
|
||
mut reads,
|
||
} = draft;
|
||
// 把全部投影/提交错误保存在闭包结果中,外层始终尝试释放 hooks 和 checkpoint 的拥有根。
|
||
let result: Result<crate::document::Frame> = (|| {
|
||
self.select_window(window)?;
|
||
{
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 先在布局坐标中处理滚动,再叠二维视口/面板投影;绘制与输入共享最终 Frame 几何。
|
||
crate::scroll::project(vm, &state.mounts, &root, &mut frame)?;
|
||
crate::viewport::project(&root, &mut frame)?;
|
||
}
|
||
// Native layout may replace the structural Frame; attach chrome only
|
||
// after either layout path has produced the final page geometry.
|
||
// 小原生快速路径可能返回新的 Frame,标签/窗口与页面相机必须在最终布局之后重新附加。
|
||
frame.tabs = tabs;
|
||
frame.window = window;
|
||
if let Some(root) = frame.mounts.first() {
|
||
// 首个出现是根几何,作为页面内容范围;没有出现时保留已有表面范围。
|
||
surface.extent = crate::layout::Size {
|
||
width: root.width,
|
||
height: root.height,
|
||
};
|
||
}
|
||
if let Some(viewport) = full_viewport {
|
||
// 用真实整页视口约束相机,不能用为特殊 Page.mode 改写过的局部布局约束。
|
||
surface.clamp(viewport);
|
||
}
|
||
frame.surface = surface;
|
||
// Capture versions before lifecycle hooks can change a dependency.
|
||
// 排除所有窗口的宿主私有记账读取;共享原生方法可能捕获主 desktop,不能只排除 current。
|
||
let desktops = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||
.host_desktops();
|
||
// Shared intrinsic functions capture the primary desktop even when
|
||
// invoked from another window. Desktop bookkeeping is private host
|
||
// state; Window, viewport input and business dependencies remain.
|
||
// 源布局实际业务读取版本在生命周期 hooks 前保存,hooks 改写依赖后下一轮才能识别失效并重算。
|
||
reads.retain(|read| !desktops.contains(&read.object()));
|
||
cache.exclude_host_desktops(&desktops);
|
||
// 对象版本读取和命名查找分开存储,供 Presentation.observe_changes 判断不同类别失效。
|
||
let (reads, names) = crate::presentation::split_reads(reads);
|
||
// 焦点输入只接受能表示为非负 u64 的数字,nil/错误类型自然表示没有焦点。
|
||
frame.focus = match vm.get_attr(desktop, "focus") {
|
||
Ok(Value::Number(id)) => id.as_i64().and_then(|n| u64::try_from(n).ok()),
|
||
_ => None,
|
||
};
|
||
// 焦点 ID 仅保留仍存在且可交互的出现,切页/树编辑移除目标后自动清掉失效焦点。
|
||
frame.focus = frame
|
||
.focus
|
||
.filter(|id| frame.mounts.iter().any(|m| m.id == *id && m.interactive));
|
||
{
|
||
// 生命周期回调前先写回本轮计算结果;回调产生的新变化随后由运行时刷新循环再观察。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
layout.viewport = full_viewport;
|
||
state.layout = layout;
|
||
state.measure_cache = cache;
|
||
state.layout_reads = reads;
|
||
state.layout_names = names;
|
||
}
|
||
// 几何先提交到桥接状态,再执行准备阶段延后的挂载 hooks;其变化由全窗口刷新循环再收敛。
|
||
self.commit_frame(vm, Some(root), &frame)?;
|
||
if let Some(hooks) = hooks {
|
||
// 准备阶段暂存的驱动现在才执行;无论返回值业务上是否使用都需释放其 VM 根。
|
||
let value = vm
|
||
.call_with_limits(
|
||
hooks,
|
||
&[],
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 1_000_000,
|
||
},
|
||
)
|
||
.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
|
||
vm.release_value(value)?;
|
||
}
|
||
Ok(frame)
|
||
})();
|
||
// 收尾计算无论成功/失败,都先释放 hooks 与检查点的拥有根;只有所有必要清理通过才返回帧。
|
||
// 两项释放都先求值,不能在 hooks 释放处直接 ? 而跳过 checkpoint 的清理。
|
||
let released_hooks = hooks.map(|hooks| vm.release(hooks)).transpose();
|
||
// 此阶段完成/报错均消费检查点根;这里是释放,不是把已提交几何回滚到准备前。
|
||
let released_checkpoint = checkpoint.release(vm);
|
||
// 所有清理尝试结束后才传播主结果与释放错误,不把失败帧交给绘制/发布调用者。
|
||
let frame = result?;
|
||
released_hooks?;
|
||
released_checkpoint?;
|
||
Ok(frame)
|
||
}
|
||
|
||
/// endor_host::compile 的兼容一次性执行路径调用,读取源程序安排展示的 Page 并提取不可变视觉树。
|
||
pub fn rendered(&self, vm: &Vm) -> Result<Option<Arc<UiNode>>> {
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||
// 此兼容提取接口只读取待展示 Page,不回退 active;持久增量路径使用 prepare_frame 的 active/pending 选择。
|
||
state
|
||
.pending
|
||
.flatten()
|
||
.map(|v| block::snapshot(vm.object_store(), v))
|
||
.transpose()
|
||
.map_err(Into::into)
|
||
}
|
||
}
|
||
|
||
/// calls 创建状态/协议结果时调用,从当前会话取 desktop,再给对象附上指定的名义类型。
|
||
fn attach_named_type(
|
||
cx: &mut NativeContext<'_>,
|
||
object: ObjectRef,
|
||
name: &str,
|
||
session: &Shared,
|
||
) -> Result<(), VmError> {
|
||
// 仅复制资源域身份,释放锁后才操作 VM;不会把 Session 锁延伸到类型附加路径。
|
||
let desktop = session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
attach_desktop_type(cx, object, name, desktop)
|
||
}
|
||
|
||
/// bridge/model/runtime 的原生分配器调用,给已分配 payload 附上同一 desktop 注册的语言类型。
|
||
pub fn attach_desktop_type(
|
||
cx: &mut NativeContext<'_>,
|
||
instance: ObjectRef,
|
||
name: &str,
|
||
desktop: ObjectRef,
|
||
) -> Result<(), VmError> {
|
||
// 缺少类型条目直接返回错误,不能自动创建同名 Type 让实例与已有契约不兼容。
|
||
let definition = desktop_type(cx.store(), name, desktop)?;
|
||
cx.attach_type(instance, definition)
|
||
}
|
||
|
||
/// 签名构造、能力检查和各分配器调用,查询 bootstrap 注册的精确 Type 身份,不按名字新建类型。
|
||
pub(crate) fn desktop_type(
|
||
store: &endor_core::ObjectStore,
|
||
name: &str,
|
||
desktop: ObjectRef,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 查当前 desktop 的实际字典对象和精确类型条目;不是按名字创建新类型,缺条目沿 object 解码报错。
|
||
let types = object(Some(&store.get_attr(desktop, "types")?))?;
|
||
object(store.dict_get(types, &Value::Text(name.into()))?.as_ref())
|
||
}
|
||
|
||
/// endor_host::compile 在 main 未通过页面 API 自行展示时调用,把 main 返回的 Block 提取成不可变根。
|
||
pub fn snapshot(vm: &Vm, value: &Value) -> Result<Arc<UiNode>> {
|
||
// 兼容 main 必须返回真实对象;标量值的 ui.open 回退规则不在这个直接快照接口中执行。
|
||
let Value::Object(root) = value else {
|
||
bail!("main must return a Block");
|
||
};
|
||
// block::snapshot 继续检查 payload/树语义;仅是 Value::Object 还不足以证明它可展示。
|
||
block::snapshot(vm.object_store(), *root).map_err(Into::into)
|
||
}
|