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

2243 lines
89 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 推进。挂起工作必须恢复或显式取消;失败不能把未完成的几何和生命周期状态留作已发布结果。
//!
//! Tessera's native visual protocols over Aster's managed object graph.
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,
};
pub const MAX_UI_NODES: usize = 4096;
pub const MAX_PAGES: usize = 64;
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",
];
pub const QUERY_NAMES: &[&str] = &[
"isInstance",
"isSubtype",
"implement",
"implements",
"supports",
];
pub use tree::{UiKind, UiNode};
#[derive(Debug)]
/// 整个 VM 的桥接会话。节点索引跨窗口共享,presentations 按窗口隔离派生状态;current 决定方法当前访问哪份展示上下文。
struct Session {
// Weak visual-object registry belongs to the VM, not a particular window.
nodes: Vec<WeakObjectRef>,
presentations: std::collections::BTreeMap<u64, crate::presentation::Presentation>,
current: u64,
master: ObjectRef,
next_window: u64,
// 跨窗口共用 Mount 高水位,避免某窗口取消/重建后与其他窗口已暴露出现 ID 重合。
next_mount: u64,
// 正常关闭副窗口后暂存尚待发布的 Close 等命令;Presentation 已移除也不能丢掉最后的原生退役请求。
retired_commands: Vec<crate::window::Command>,
refresh_yields: usize,
refresh_restarts: usize,
layout_cancels: usize,
paint_cancels: usize,
paint_slices: usize,
binding_evaluations: usize,
exiting: bool,
}
impl Session {
/// 收集所有窗口的私有 desktop 身份,用于排除宿主记账读取造成的无关布局失效。
fn host_desktops(&self) -> std::collections::HashSet<ObjectRef> {
self.presentations
.values()
.map(|view| view.desktop)
.collect()
}
}
impl std::ops::Deref for Session {
type Target = crate::presentation::Presentation;
/// 将包装类型的只读访问转交给内部当前上下文。
fn deref(&self) -> &Self::Target {
// Session 解引用始终指向 current Presentation;调用源回调前切换窗口,结束后恢复主窗口,不能把它当全局唯一展示。
self.presentations
.get(&self.current)
.expect("selected presentation")
}
}
impl std::ops::DerefMut for Session {
/// 将包装类型的可变访问转交给内部当前上下文。
fn deref_mut(&mut self) -> &mut Self::Target {
self.presentations
.get_mut(&self.current)
.expect("selected presentation")
}
}
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 {
/// 返回该操作显式参数数量范围;这里不计接收者、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),
Stats | Viewport | Window | PageWindow | Mounts => (0, 0),
State | StateSet => (1, 1),
Increment => (0, 0),
TextOf => (2, 4),
Scroll => (3, 3),
CanvasViewport => (3, 3),
Viewport3D => (3, 3),
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),
Row | Column | Overlay => (2, MAX_UI_NODES + 2),
Button => (1, 1),
Insert | Replace | Move => (2, 2),
Page | Unmount | Close | Activate | Back | Forward | Children | NewWindow
| CloseWindow | Detach => (0, 0),
Open => (0, 1),
Click | Invalidate => (0, 1),
On => (2, 3),
Dispatch => (1, 2),
OnEvent | OnMount | OnUnmount => (1, 1),
};
endor_core::CallArity::range(minimum, maximum)
}
}
#[derive(Debug)]
/// 将一个宿主 Operation 及会话环境封装成 VM 可调用对象。
struct Function {
signature: Option<CallSignature>,
operation: Operation,
owner: Option<ObjectRef>,
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 {
signature: CallSignature,
operation: Operation,
expected: ObjectRef,
session: Shared,
}
/// 判断方法是否仍为宿主提供的原生事件实现,区分它与用户源覆盖。
pub(crate) fn is_intrinsic_event(
store: &endor_core::ObjectStore,
function: ObjectRef,
) -> Result<bool, VmError> {
let Some(function) = store
.object(function)?
.payload()
.as_any()
.downcast_ref::<ContextNativeFunction>()
else {
return Ok(false);
};
Ok(function
.implementation::<TypeMethod>()
.is_some_and(|method| matches!(method.operation, Operation::OnEvent)))
}
impl NativeCallable for TypeMethod {
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
fn signature(&self) -> Option<CallSignature> {
Some(self.signature.clone())
}
/// 在 Operation 的显式参数范围两端各加一,计入 VM 传入的隐式接收者。
fn arity(&self) -> Option<endor_core::CallArity> {
let arity = self.operation.arity();
Some(endor_core::CallArity::range(
arity.required + 1,
arity.maximum + 1,
))
}
/// 校验首参是预期名义类型实例,再剥离接收者,复用 Function 分派执行同一 Operation。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
// 名义方法统一存在类型上,VM 把 receiver 放首参;校验后剥离 receiver 再交 Function 的显式参数分派。
let owner = object(args.first())?;
if !cx.store().is_instance(owner, self.expected)? {
return Err(invalid("incorrect receiver type for visual method"));
}
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 {
signature: Option<CallSignature>,
options: Option<CallSignature>,
operation: Operation,
session: Shared,
}
impl NativeCallable for NativeFactory {
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
fn signature(&self) -> Option<CallSignature> {
self.signature.clone()
}
/// 要求原生必填参数再加一个请求 Type;上界暂开放,子类字段与原生选项在 invoke 内分别校验。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::range(
self.operation.arity().required + 1,
usize::MAX,
))
}
/// 把请求类型的字段后缀与原生选项前缀分开验证,分配真实原生子类并初始化字段,避免非法字段先触发构造副作用。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
let requested = object(args.first())?;
let fields = cx
.store()
.payload::<EndorTypeObject>(requested, "Type")?
.fields()
.to_vec();
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],
)?;
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 {
return Err(invalid("native constructor did not return an instance"));
};
// 分配器必须返回精确 requested 类型,不能只返回基类实例;之后才初始化子类字段。
if cx.store().type_of(instance)? != Some(requested) {
return Err(invalid(
"native constructor returned an incorrect nominal type",
));
}
for (field, value) in fields.iter().zip(&values[native_count..]) {
cx.set_attr(instance, &field.name, value.clone())?;
}
Ok(NativeResult::Return(Value::Object(instance)))
}
}
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
fn invalid(message: &'static str) -> VmError {
VmError::InvalidArgument(message)
}
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
fn object(value: Option<&Value>) -> Result<ObjectRef, VmError> {
match value {
Some(Value::Object(value)) => Ok(*value),
_ => Err(invalid("expected an object")),
}
}
/// 把当前源值或文本选择解码成此调用需要的文本表示。
fn text(value: Option<&Value>) -> Result<String, VmError> {
match value {
Some(Value::Text(value)) => Ok(value.clone()),
_ => Err(invalid("expected text")),
}
}
/// 读取整数参数,拒绝非整数或无法在目标范围表示的值。
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> {
let value = value
.map(|v| text(Some(v)))
.transpose()?
.unwrap_or_else(|| default.into());
parse_color(&value).map_err(|_| invalid("color must use #RRGGBB or #RRGGBBAA"))?;
Ok(value)
}
/// 读取布局尺寸参数,检查非负、有限和最大尺寸限制。
fn dimension(value: Option<&Value>) -> Result<f32, VmError> {
let Some(Value::Number(number)) = value else {
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"))?;
Ok(value as f32)
}
/// 为一个托管对象安装绑定到当前会话的原生方法,声明调用名与具体 Operation。
fn bind(
cx: &mut NativeContext<'_>,
owner: ObjectRef,
name: &'static str,
operation: Operation,
session: &Shared,
) -> Result<(), VmError> {
let desktop = session
.lock()
.map_err(|_| invalid("UI session poisoned"))?
.desktop;
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],
))?;
attach_desktop_type(cx, function, "Function", desktop)?;
cx.set_attr(owner, name, Value::Object(function))
}
/// 使用默认原生类型创建 Block;需要源子类身份的构造走 new_typed_block。
fn new_block(
cx: &mut NativeContext<'_>,
data: Data,
session: &Shared,
) -> Result<ObjectRef, VmError> {
new_typed_block(cx, data, session, None)
}
/// 分配有名义类型和默认位置/缩放的独立相机,供新视口拥有自己的导航状态。
fn new_camera2d(
cx: &mut NativeContext<'_>,
session: &Shared,
requested: Option<ObjectRef>,
args: &[Value],
) -> Result<ObjectRef, VmError> {
let desktop = session
.lock()
.map_err(|_| invalid("UI session poisoned"))?
.desktop;
let definition = requested.unwrap_or(desktop_type(cx.store(), "Camera2D", desktop)?);
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)] {
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)
}
/// 创建原生 Block 并附加请求的 Endor 类型;先验证继承关系,再登记到会话节点索引。
fn new_typed_block(
cx: &mut NativeContext<'_>,
data: Data,
session: &Shared,
requested: Option<ObjectRef>,
) -> Result<ObjectRef, VmError> {
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",
};
let page = matches!(data.kind, Kind::Page);
let desktop = session
.lock()
.map_err(|_| invalid("UI session poisoned"))?
.desktop;
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"));
}
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 {
let mut pages = 0;
for weak in &state.nodes {
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)
}
/// 在当前 VM 中建立窗口、desktop 和独立 Presentation,实际 OS 窗口等命令发布后再创建。
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 {
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"));
}
let id = state
.next_window
.checked_add(1)
.filter(|id| *id <= i64::MAX as u64)
.ok_or(invalid("window identities exhausted"))?;
let (window, desktop, tabs) = crate::window::create(cx, state.master, id, definition)?;
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)
}
/// 从页面登记信息取得所属窗口,保持 Page 的归属与原生资源身份一致。
fn page_window(
store: &endor_core::ObjectStore,
state: &Session,
page: ObjectRef,
) -> Result<Option<u64>, VmError> {
for (id, view) in &state.presentations {
let window = object(Some(&store.get_attr(view.desktop, "window")?))?;
if crate::window::pages(store, window)?.contains(&page) {
return Ok(Some(*id));
}
}
Ok(None)
}
/// 撤销页面在展示会话中的登记,配合模型层处理活动页面与生命周期。
fn remove_page(
cx: &mut NativeContext<'_>,
view: &mut crate::presentation::Presentation,
page: ObjectRef,
) -> Result<(), VmError> {
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) {
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(())
}
/// 在指定窗口安排页面展示,并维护目标 Presentation 的活动/待发布状态。
fn show_in(
cx: &mut NativeContext<'_>,
page: ObjectRef,
session: &Shared,
target: Option<ObjectRef>,
) -> Result<(), VmError> {
let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?;
let old = page_window(cx.store(), &state, page)?;
let id = match target {
// 显式目标窗口既要未关闭,也要对应本会话 Presentation 的同一 desktop,拒绝其他 VM 的窗口身份。
Some(window) => {
let data = cx
.store()
.payload::<crate::window::ManagedWindow>(window, "Window")?;
if data.closed {
return Err(invalid("native window is closed"));
}
let id = data.id;
if state
.presentations
.get(&id)
.is_none_or(|view| view.desktop != data.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,
)?;
}
let view = state
.presentations
.get_mut(&id)
.ok_or(invalid("missing target window"))?;
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(())
}
/// 在当前窗口安排页面展示,实际几何和画面由后续刷新提交。
fn show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result<(), VmError> {
show_in(cx, page, session, None)
}
#[derive(Debug)]
/// 接收源 present 结果并将其转换成宿主可展示内容的原生续接步骤。
struct PresentResult {
open: bool,
session: Shared,
}
impl NativeCallable for PresentResult {
/// 接收 present 委托返回的唯一视觉对象,作为后续展示处理的输入。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
/// 验证 present 结果为 Block;open 模式必要时包装 Page 并展示,否则只返回转换得到的视觉对象。
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"));
};
let is_page = cx.store().payload::<block::Block>(*root, "Block")?.page;
// present 委托返回值必须为 Block;open=true 必要时包 Page 并展示,open=false 只返回转换结果。
let result = if self.open {
let page = if is_page {
*root
} else {
new_block(
cx,
Data::new(Kind::Page, vec![*root], 0.0, 0.0),
&self.session,
)?
};
show(cx, page, &self.session)?;
page
} else {
*root
};
Ok(NativeResult::Return(Value::Object(result)))
}
}
/// 返回已有 Block,或把其他值的可读表示包装成 TextBlock;Presentable 委托由外层调用分派负责。
fn present_value(
cx: &mut NativeContext<'_>,
value: &Value,
session: &Shared,
) -> Result<ObjectRef, VmError> {
if let Value::Object(block) = value
&& cx
.store()
.object(*block)?
.payload()
.as_any()
.is::<block::Block>()
{
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 {
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 {
Pending(Box<PendingLayout>),
Complete(Box<crate::document::Frame>),
}
/// 一次未提交布局的上下文:目标窗口、根、布局/缓存、延后钩子以及用于失败恢复的检查点。
struct LayoutContext {
surface: crate::page::Surface,
root: Arc<UiNode>,
tabs: Vec<crate::tabs::Tab>,
full_viewport: Option<crate::layout::Size>,
desktop: ObjectRef,
window: u64,
// 布局同步返回的生命周期序列拥有根,暂留到安全提交边界执行/释放,取消时也必须清理。
hooks: Option<ObjectRef>,
// 布局准备可能暂改 Mount payload 与 desktop.mounts,检查点保证失败或尺寸重定向能恢复未发布关系。
checkpoint: crate::mount::Checkpoint,
}
/// 挂起布局的拥有者。续体与上下文必须一起保留并最终恢复或取消。
pub struct PendingLayout {
work: crate::source_layout::Work,
context: LayoutContext,
}
/// 运行时持有的桥接能力句柄,通过共享 Session 协调托管对象与每窗口展示。
pub struct UiBridge {
pub ui: ObjectRef,
session: Shared,
tasks: crate::tasks::Scheduler,
}
/// 解析对象的方法并在同一 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)?;
let result = match &bound {
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)),
_ => Err(anyhow::anyhow!("object method is not callable")),
};
vm.release_value(bound)?;
result
}
/// 将托管页面注册表投影成稳定标签 ID、标题与活动状态,不把托管引用交给原生侧栏。
fn project_tabs(
vm: &Vm,
state: &mut crate::presentation::Presentation,
) -> Result<Vec<crate::tabs::Tab>> {
let window = object(Some(&vm.get_attr(state.desktop, "window")?))?;
let pages = crate::window::pages(vm.object_store(), window)?;
// 标签选中反映待提交页面(若有请求),否则沿用 active;页面标题和顺序仍从真实窗口注册表提取。
let active = state.pending.unwrap_or(state.active);
let mut live = Vec::new();
let mut tabs = Vec::new();
for (index, page) in pages.iter().copied().enumerate() {
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 {
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),
};
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),
});
}
state.tab_ids = live;
Ok(tabs)
}
impl UiBridge {
/// Detach all occurrences without running layout/paint. All windows are
/// already closed before source unmount hooks run, so they cannot remount.
/// 按窗口和展示生命周期执行有界退出清理,允许调用方根据故障状态决定是否执行回调。
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();
let mut mounts = 0;
let prepared = (|| -> Result<usize> {
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
state.exiting = true;
let ids: Vec<_> = state.presentations.keys().copied().collect();
for id in &ids {
let view = state.presentations.get_mut(id).expect("known window");
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,
)? {
drivers.push((*id, hooks));
}
} else {
view.mounts.retire(vm, desktop)?;
}
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())
})();
let mut result = prepared.map(|windows| (windows, mounts, tasks));
// 若之前清理/回调失败,余下 hooks 不再执行,但所有驱动根仍逐项 release,避免失败路径泄漏。
for (id, hooks) in drivers {
if result.is_ok() {
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());
}
}
self.select_window(crate::window::MAIN_WINDOW)?;
result
}
/// 查询任务系统是否仍有可推进的协作工作,供 worker 选择等待策略。
pub fn has_cooperative_tasks(&self) -> bool {
self.tasks.has_cooperative_work()
}
/// 把当前 VM 交给任务调度器推进,限制一轮交付数量。
pub fn poll_tasks(&self, vm: &mut Vm, limit: usize) -> Result<usize> {
self.tasks.poll(vm, limit)
}
/// 取消当前域中的任务,撤销关闭/失败后不应再发生的交付。
pub fn cancel_tasks(&self, vm: &mut Vm) -> Result<usize> {
self.tasks.cancel_all(vm)
}
/// 返回当前仍有效的展示窗口身份,用于稳定遍历一次全窗口刷新。
pub fn window_ids(&self) -> Result<Vec<u64>> {
let state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
let mut ids: Vec<_> = state.presentations.keys().copied().collect();
// 优先处理切离旧 active 页的窗口,使跨窗口迁移先退旧出现再挂新出现;同优先级按窗口 ID 稳定排序。
ids.sort_by_key(|id| {
let view = &state.presentations[id];
(
if view.active.is_some()
&& view.pending.is_some()
&& view.pending.flatten() != view.active
{
0
} else {
1
},
*id,
)
});
Ok(ids)
}
/// 判断窗口是否仍登记且没有进入关闭状态。
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)
})
}
/// 切换当前 Presentation 上下文;不会创建新窗口或复制 VM。
pub fn select_window(&self, id: u64) -> Result<()> {
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
if !state.presentations.contains_key(&id) {
bail!("window is unavailable");
}
state.current = id;
Ok(())
}
/// 从托管展示域撤销窗口,退役 Mount 并清理该窗口的私有对象关系。
pub fn retire_window(&self, vm: &mut Vm, id: u64) -> Result<()> {
if id == crate::window::MAIN_WINDOW {
bail!("the primary shell is closed by the OS window control");
}
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
let Some(view) = state.presentations.get_mut(&id) else {
return Ok(());
};
let desktop = view.desktop;
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(())
}
/// 收尾已经关闭的展示资源,避免它们参与下一次窗口遍历。
pub fn finish_closed(&self, vm: &mut Vm) -> Result<()> {
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();
let windows = object(Some(&vm.get_attr(state.master, "windows")?))?;
for id in ids {
let desktop = state.presentations[&id].desktop;
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(())
}
/// 从各托管窗口收集待发布命令;原生执行时还需校验 session。
pub fn take_window_commands(&self, vm: &Vm) -> Result<Vec<crate::window::Command>> {
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)
}
/// 校验并更新所属窗口的逻辑尺寸输入;尺寸不变时不制造新布局,改变时标记需要刷新。
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))
{
bail!("invalid window viewport");
}
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
// 尺寸比较在分配新输入对象前完成,避免相同 resize 通知改变 VM 版本而触发自我失效。
if state.layout.viewport == viewport {
return Ok(false);
}
if let Some(size) = viewport {
let input = vm.new_object()?;
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 {
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)
}
/// 将 ui 入口与兼容名称安装到本次程序的全局字典,保持所有调用处在同一个 VM 域。
pub fn install_globals(&self, vm: &mut Vm, globals: ObjectRef) -> Result<()> {
vm.dict_set(globals, Value::Text("ui".into()), Value::Object(self.ui))?;
// Compatibility spelling used by the original module examples.
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) {
let value = vm.get_attr(self.ui, name)?;
vm.dict_set(globals, Value::Text((*name).into()), value)?;
}
Ok(())
}
/// 检查当前会话是否已经安排或拥有可展示页面。
pub fn has_page(&self) -> Result<bool> {
Ok(self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.presentations
.values()
.any(|view| view.pending.flatten().is_some() || view.active.is_some()))
}
/// 验证标签拖放仍指向有效页面和窗口关系,避免旧拖动操作跨域生效。
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 {
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() {
return false;
}
}
true
}
/// 通过展示协议打开源值,必要时创建或选择承载它的页面。
pub fn open_value(&self, vm: &mut Vm, value: &Value) -> Result<()> {
let function = object(Some(&vm.get_attr(self.ui, "open")?))?;
let page = vm.call(function, std::slice::from_ref(value))?;
vm.release_value(page)?;
Ok(())
}
/// 处理页面激活、关闭、移动和拆分等标签操作,保持窗口所有权与展示状态一致。
fn dispatch_tab(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
let page = {
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");
}
// 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"))?;
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 {
let window = {
let state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
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.
let destination = {
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)?;
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");
}
let value = invoke_member(
vm,
page,
"moveTab",
&[Value::Number((position as i64).into())],
300_000,
)?;
vm.release_value(value)?;
let value = invoke_member(vm, page, "activate", &[], 300_000)?;
vm.release_value(value)?;
}
return Ok(());
}
let method = match input.kind.as_str() {
"tab-close" => "close",
"tab-detach" => "detachToWindow",
_ => "activate",
};
let value = invoke_member(vm, page, method, &[], 1_000_000)?;
// detach 返回值可能被源覆盖成其他值,仅确为 Window 且带屏幕位置才调用 moveTo;最后统一释放原返回值。
let placed = (|| -> Result<()> {
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()
{
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(())
})();
vm.release_value(value)?;
placed?;
Ok(())
}
/// 在所属窗口的展示上下文中处理输入,区分页面管理、滚动、焦点、相机与普通事件。
pub fn dispatch(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
input
.validate()
.map_err(|message| anyhow::anyhow!(message))?;
if self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.current
!= input.window
{
bail!("event belongs to another window");
}
// 窗口关闭通过名义 close 方法,保留源覆盖语义;直接调用宿主原生退役只用于故障清理路径。
if input.kind == "window-close" {
let desktop = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.desktop;
let window = object(Some(&vm.get_attr(desktop, "window")?))?;
let bound = vm.bind_attr(window, "close")?;
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:") {
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") {
return self.dispatch_page_view(vm, input);
}
if matches!(input.kind.as_str(), "viewport-pan" | "viewport-zoom") {
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"
) {
return self.dispatch_tab(vm, input);
}
// 普通指针输入取同一锁内的版本/目标/完整出现候选快照;随后不持 session 锁执行源 hitTest 或事件回调。
let (weak, desktop, candidates, viewport, surface) = {
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" {
let mount = vm
.upgrade(weak)?
.ok_or_else(|| anyhow::anyhow!("scroll Mount expired"))?;
let result = (|| {
let data = vm.payload::<crate::mount::Mount>(mount, "Mount")?;
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");
}
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")?;
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(())
})();
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))
{
return Ok(());
}
let Some(mount) = crate::source_hit::resolve(vm, desktop, candidates, press, release)?
else {
return Ok(());
};
mount
} else {
vm.upgrade(weak)?
.ok_or_else(|| anyhow::anyhow!("Mount has expired"))?
};
let result = (|| {
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
{
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],
))?;
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 {
bail!("event targets an obsolete presentation");
}
if !data.geometry.interactive {
bail!("Mount does not accept input");
}
// 普通点击/焦点先登记展示焦点请求,再按 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))?;
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)?;
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
}
/// 整理不再有效的弱索引与展示根,并配合 VM 回收,避免派生缓存延长业务对象生命周期。
pub fn reclaim(&self, vm: &mut Vm, _roots: &mut Vec<ObjectRef>) -> Result<()> {
// 先回收托管环,再扫描弱节点表,只保留能升级的身份;render_cache.prune 同步清除死亡业务索引。
vm.collect_cycles();
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.release(object)?;
live.push(*weak);
}
}
state.nodes = live;
for presentation in state.presentations.values_mut() {
presentation.render_cache.prune(vm.object_store());
}
Ok(())
}
/// 丢弃未成功发布的展示请求,恢复后续输入可以使用的会话状态。
pub fn discard_pending(&self, vm: &mut Vm) -> Result<()> {
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
for presentation in state.presentations.values_mut() {
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() {
let unpublished = crate::window::pages(vm.object_store(), window)?.to_vec();
for page in unpublished {
crate::window::unregister_page_host(vm, window, page)?;
}
}
let pages = crate::window::pages(vm.object_store(), window)?;
// 失败输入已成功执行的关闭/迁移语句不回滚;重新选择仍在真实窗口注册表中的页,修复 pending 与拥有关系。
let next = if presentation.closing {
None
} else {
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(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())
{
presentation.active = None;
}
}
Ok(())
}
/// 重新评估已失效的属性绑定,并把结果计入会话统计。
pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> {
// 只在锁内快照弱节点与宿主桌面集合,实际绑定回调在锁外执行,避免回调访问 UI 时死锁。
let (nodes, desktops) = {
let state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
(state.nodes.clone(), state.host_desktops())
};
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(())
}
/// 排空一次 VM 变化记录并通知各窗口,提取活动页面快照,判断本次是否需要布局与发布。
pub fn prepare_frame(&self, vm: &mut Vm) -> Result<Option<Option<Arc<UiNode>>>> {
// 一次排空变更必须广播所有 Presentation,不能让先刷新的窗口吃掉其他窗口共享业务数据的失效通知。
let changes = vm.take_changes();
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() {
presentation.observe_changes(vm, &changes);
}
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,
};
let focus_changed = matches!(
vm.get_attr(state.desktop, "focusRequested"),
Ok(Value::Bool(true))
);
let tabs = project_tabs(vm, &mut state)?;
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 {
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))
}
/// 提交已经完成的出现几何与快照,并处理展示生命周期;不把挂起或失败的布局当成已发布帧。
pub fn commit_frame(
&self,
vm: &mut Vm,
snapshot: Option<Arc<UiNode>>,
frame: &crate::document::Frame,
) -> Result<()> {
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
let page = state.pending.unwrap_or(state.active);
let desktop = state.desktop;
let nodes = state.nodes.clone();
// 提交几何同步清除临时 layout Context,并产生挂载/卸载序列;提交桥接状态不等于已写 GUI 共享文档。
let hooks = state
.mounts
.synchronize(vm, desktop, page, nodes, frame, false)?;
let result = (|| {
if snapshot.is_none() {
state.layout.clear_occurrences();
}
if let Some(page) = state.pending.take() {
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 {
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.release(hooks)?;
}
result
}
/// 在已完成的几何上启动绘制协议,保留绘制任务所需的宿主上下文。
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) = {
let state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
// 按最终帧绘制顺序查活动 Mount 弱身份,每条都必须存在;不按 HashMap 顺序调用源 paint。
let mounts = frame
.mounts
.iter()
.map(|mount| {
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)
};
let progress = crate::source_paint::start(vm, desktop, mounts, root, frame, 256)?;
self.accept_paint_progress(progress)
}
/// 恢复同一次源绘制调用,把完成的 Draft 交回发布流程。
pub fn resume_paint_frame(
&self,
vm: &mut Vm,
work: crate::source_paint::Work,
) -> Result<crate::source_paint::Progress> {
let progress = work.resume(vm, 256)?;
self.accept_paint_progress(progress)
}
/// 区分挂起与完成的绘制进度;完成时保存真实读取依赖并接回发布流程。
fn accept_paint_progress(
&self,
progress: crate::source_paint::Progress,
) -> Result<crate::source_paint::Progress> {
if let crate::source_paint::Progress::Complete(draft) = &progress {
let mut state = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
// 完成后过滤所有窗口私有 desktop 读取,再拆对象版本与名称依赖;挂起切片不覆盖上一份完成的 paint 依赖。
let desktops = state.host_desktops();
let reads = draft
.reads
.iter()
.filter(|read| !desktops.contains(&read.object()))
.cloned()
.collect();
let (reads, names) = crate::presentation::split_reads(reads);
state.paint_reads = reads;
state.paint_names = names;
state.paint_slices = draft.slices;
}
Ok(progress)
}
/// 取消尚未完成的源绘制,释放其续体和未发布上下文。
pub fn cancel_paint_frame(
&self,
vm: &mut Vm,
window: u64,
work: crate::source_paint::Work,
) -> Result<()> {
// 先尝试取消 VM 续体,再尝试选原窗口并强制下一帧;前一操作失败也执行必要的宿主失效准备。
let cancelled = work.cancel(vm);
let invalidated = self.select_window(window).and_then(|()| {
self.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.force_frame = true;
Ok(())
});
cancelled?;
invalidated
}
/// 为目标快照建立 Mount、检查点和布局上下文,再启动可挂起的只读布局求解。
pub fn begin_layout_frame(
&self,
vm: &mut Vm,
root: Option<Arc<UiNode>>,
) -> Result<LayoutProgress> {
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();
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()
}
};
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(),
)
};
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;
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);
}
let mut frame = layout.structure(&root)?;
{
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;
}
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)?;
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,
};
let draft = crate::source_layout::Draft {
frame,
runtime: layout,
cache,
reads: Vec::new(),
};
let progress = crate::source_layout::start(vm, desktop, mounts, root, draft, 256);
self.accept_layout_progress(vm, context, progress)
}
/// 记录本次刷新主动让出 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(())
}
/// 记录视口目标替换导致的刷新重启,便于区分正常挂起与取消重试。
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"))?;
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(())
}
/// 撤销未完成布局所保留的窗口、挂载与生命周期上下文。
fn discard_layout_context(&self, vm: &mut Vm, context: LayoutContext) -> Result<()> {
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) => {
let result = checkpoint.restore(vm, desktop, &mut state.mounts);
state.force_frame = true;
result.map_err(anyhow::Error::from)
}
Err(_) => checkpoint
.release(vm)
.map_err(anyhow::Error::from)
.and(Err(anyhow::anyhow!("UI session poisoned"))),
}
} else {
checkpoint.release(vm).map_err(anyhow::Error::from)
};
// 无论选择/恢复是否失败都先尝试释放延迟 hooks;保存结果后再按优先级传播错误。
let released = hooks.map(|hooks| vm.release(hooks)).transpose();
selected?;
restored?;
released?;
Ok(())
}
/// 取消源布局续体并清理对应上下文,保持已发布帧可用。
pub fn cancel_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> {
let PendingLayout { work, context } = pending;
// 取消只读执行与恢复 Mount 是两件事,均先尝试;没有继续执行原续体时也不能漏掉检查点恢复。
let cancelled = work.cancel(vm);
let restored = self.discard_layout_context(vm, context);
cancelled?;
restored
}
/// 关闭失败布局的挂起工作并恢复检查点;供错误或候选撤销路径使用。
pub fn abort_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> {
self.cancel_layout_frame(vm, pending)
}
/// 继续同一次尚未发布的布局任务;完成时沿原上下文收尾,不能新建一轮来重放副作用。
pub fn resume_layout_frame(
&self,
vm: &mut Vm,
pending: PendingLayout,
) -> Result<LayoutProgress> {
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);
}
let progress = work.resume(vm, 256);
self.accept_layout_progress(vm, context, progress)
}
/// 接收布局的 Pending/Complete 结果;保留上下文或执行最终投影和提交。
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) => {
self.discard_layout_context(vm, context)?;
Err(error)
}
}
}
/// 依次投影滚动/视口、补页面元数据、保存读取版本、提交 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;
let crate::source_layout::Draft {
mut frame,
runtime: mut layout,
mut cache,
mut reads,
} = draft;
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 {
surface.clamp(viewport);
}
frame.surface = surface;
// Capture versions before lifecycle hooks can change a dependency.
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);
let (reads, names) = crate::presentation::split_reads(reads);
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 {
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 与检查点的拥有根;只有所有必要清理通过才返回帧。
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)
}
/// 读取当前源程序已经选择的展示内容,为编译入口生成最终文档提供根。
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)
}
}
/// 从会话的类型注册表取得指定类型,再给原生分配的对象附加托管类型身份。
fn attach_named_type(
cx: &mut NativeContext<'_>,
object: ObjectRef,
name: &str,
session: &Shared,
) -> Result<(), VmError> {
let desktop = session
.lock()
.map_err(|_| invalid("UI session poisoned"))?
.desktop;
attach_desktop_type(cx, object, name, desktop)
}
/// 按给定 desktop 查找类型并附加,支持不同窗口使用自己的展示上下文。
pub fn attach_desktop_type(
cx: &mut NativeContext<'_>,
instance: ObjectRef,
name: &str,
desktop: ObjectRef,
) -> Result<(), VmError> {
let definition = desktop_type(cx.store(), name, desktop)?;
cx.attach_type(instance, definition)
}
/// 读取 desktop 的托管类型注册表;缺失或类型错误时返回 VM 错误。
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())
}
/// 将当前托管或可变状态提取为后续阶段使用的派生表示。
pub fn snapshot(vm: &Vm, value: &Value) -> Result<Arc<UiNode>> {
let Value::Object(root) = value else {
bail!("main must return a Block");
};
block::snapshot(vm.object_store(), *root).map_err(Into::into)
}