Files
endorphin-tessera-gui/src/bridge/ui_bridge.rs
T

2623 lines
132 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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)
}