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

1083 lines
56 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.
//! 每次展示的托管上下文:同一个 Block 出现在两个位置时,有两个独立 Mount。
//! Mount 保存父子关系、出现几何和布局期间的查询上下文;MountTable 用弱索引定位对象,不靠索引强行保活业务树。
//! 同步会安排 onMount/onUnmount。布局取消时使用 Checkpoint 恢复未发布状态,避免半完成的出现关系影响下一次刷新。
//!
//! 调用路径:UiBridge 在布局前 checkpoint/synchronize(preparing=true),完成后提交几何,取消则 restore。
//! 源 measure/arrange/事件通过 Method 调用 Mount API;生命周期返回 vm_sequence 驱动,调用方安排实际执行。
//! 活动 Mount 由 desktop.mounts 强持有;MountTable 与反向层次关系仅保存弱索引。
//!
//! Managed presentation contexts. The active desktop owns the mount dictionary;
//! reverse links and the host registry are weak, so shared content cannot retain a Page.
use crate::document::{Frame, MountGeometry};
use endor_core::{
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef,
Value, Vm, VmError, WeakObjectRef,
};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
#[derive(Debug, Clone)]
/// 一个组件在一个展示位置上的上下文。强持有 Block,反向页面/父子链接使用弱身份,避免共享组件把已关闭页面保活。
pub struct Mount {
// 组件是强引用;页面、父节点和兄弟链接是弱引用,防止反向链接形成保活环。
pub block: ObjectRef,
// 所属 Page 弱身份,用于区分换页后的出现上下文,不让共享内容反向保活页面。
pub page: Option<WeakObjectRef>,
// 父出现弱身份,源 parent() 在目标已消失时返回 Nil。
pub parent: Option<WeakObjectRef>,
// 首子出现弱身份,后续兄弟沿 next_sibling 查找。
pub first_child: Option<WeakObjectRef>,
// 同级下一出现弱身份,几何帧中的 ID 链在同步第二遍解析为此链接。
pub next_sibling: Option<WeakObjectRef>,
// 桥接同步的几何与每次出现的滚动位置分开保存;准备期可暂存结构几何,不能据此认定 GUI 已发布。
pub geometry: MountGeometry,
// 此出现的用户请求偏移;帧中 geometry.scroll 才是钳制后的有效位置与上限。
pub scroll_offset: [f32; 2],
// 最后同步的布局版本,Bounds 查询会一并返回;准备同步也会写本轮版本,不等于 GUI 已接收该版本。
pub version: u64,
// false 后禁止依赖当前几何的操作;外部即使还持有 Mount 对象也不能把它当活动出现。
pub mounted: bool,
// Mount 可以拥有任务;退役后任务交付还要检查 owner 是否仍有效。
pub tasks: Option<ObjectRef>,
// Private derived scratch, installed before the read-only layout call and
// cleared by geometry commit. It contains no managed references or roots.
// 布局只读协议使用的临时派生上下文;不拥有托管根,提交或退役时清空。
pub layout: Option<Arc<Mutex<crate::layout::Context>>>,
}
impl ObjectPayload for Mount {
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
fn reference_assignment_only(&self) -> bool {
true
}
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
fn as_any(&self) -> &dyn std::any::Any {
self
}
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
fn kind_name(&self) -> &'static str {
"tessera-mount"
}
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
fn allows_attributes(&self) -> bool {
true
}
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
// 仅 block 和 tasks 是 payload 的强拥有边;page/parent/child/sibling 是弱边,不参与 trace。
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
visit(self.block);
// Aster GC 只额外追踪存在的任务作用域,弱页面/层次链不参与强引用追踪。
if let Some(tasks) = self.tasks {
visit(tasks);
}
}
}
#[derive(Debug, Clone, Copy)]
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
enum Query {
// 读取本出现已提交或排列期可用的 Bounds;测量阶段不可提前读几何。
Bounds,
// 设置此出现的绝对滚动请求,实际范围由滚动层钳制。
ScrollTo,
// 在此出现请求偏移上累加位移,不写共享 Block 的滚动状态。
ScrollBy,
// 查询有效滚动位置和最大值,不直接返回未钳制请求。
ScrollPosition,
// 联动滚动祖先,把此出现送入有效可见区域。
ScrollIntoView,
// 用此视口有限尺寸和场景边界拟合相机。
FitView,
// 从此三维视口查询射线交点和原托管 mesh。
Pick3D,
// 本出现局部坐标经二维/面板投影变为页面视图坐标。
ToView,
// 页面视图点逆投影到本出现局部坐标,失败可返回 Nil。
ToLocal,
// 弱查询父 Mount,不制造新的父拥有关系。
Parent,
// 弱查询首子 Mount,空或失效均返回 Nil。
FirstChild,
// 弱查询下一同级出现,不按相同业务对象去重。
NextSibling,
// 弱查询所属页面,退役上下文可能仍有页面弱观察。
Page,
// 请求滚入可见并取得焦点,真正窗口发布在后续阶段完成。
Focus,
// 只查询此 Mount 的提交几何包含关系,不执行源 Block.hitTest。
HitTest,
}
#[derive(Debug)]
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
struct Method {
// 本桌面名义类型构造的绑定方法契约,Bounds 返回类型也从此提取。
signature: endor_core::CallSignature,
// 方法实际绑定的 Mount,由 ContextNativeFunction captures 保活。
owner: ObjectRef,
// 方法对象拥有 Mount,但不能经桌面反向保活整个窗口;需要桌面能力时再 upgrade。
desktop: WeakObjectRef,
// 操作分派枚举,同时决定显式 arity、效果和返回契约。
query: Query,
}
impl NativeCallable for Method {
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
/// 适配器已绑定 Mount;坐标测试/转换与scrollTo/By接受两个数,其余层次/几何/焦点查询零参。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(
// 这些坐标/滚动方法需要两个 Number,其余操作无显式实参,receiver 已捕获。
if matches!(
self.query,
Query::HitTest
| Query::Pick3D
| Query::ToView
| Query::ToLocal
| Query::ScrollTo
| Query::ScrollBy
) {
2
} else {
0
},
))
}
/// 读取具体出现位置,先检查仍挂载与几何可用性,再执行层次、坐标、焦点、滚动或拾取操作,避免使用已退役上下文。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
// VM 调用绑定方法后先复制当前 payload,避免分配返回对象或改堆时持有旧存储借用。
let mount = cx.store().payload::<Mount>(self.owner, "Mount")?.clone();
// 层次查询允许观察已经脱离的对象(通常得到 Nil);几何、焦点、滚动、拾取等必须仍挂接。
if !mount.mounted
&& matches!(
self.query,
Query::Bounds
| Query::Focus
| Query::HitTest
| Query::ScrollTo
| Query::ScrollBy
| Query::ScrollIntoView
| Query::FitView
| Query::Pick3D
| Query::ToView
| Query::ToLocal
| Query::ScrollPosition
)
{
return Err(VmError::InvalidArgument("Mount is no longer attached"));
}
// 除 VM 元数据检查外入口再核对实际参数数目;二维坐标/位移操作取两个 Number,其余零参。
let arity = if matches!(
self.query,
Query::HitTest
| Query::Pick3D
| Query::ToView
| Query::ToLocal
| Query::ScrollTo
| Query::ScrollBy
) {
2
} else {
0
};
// 入口再次核对实际参数数目,防止绕过元数据路径导致 args 下标越界。
if args.len() != arity {
return Err(VmError::InvalidArgument("incorrect Mount method arity"));
}
// 所有分支统一产生 Value,再交给 NativeResult::Return 的 VM 拥有关系协议。
let value = match self.query {
// 坐标互转都要求已经提交的几何,不能混用本轮布局临时 bounds 与旧相机映射。
Query::ToView | Query::ToLocal => {
// 布局期间几何尚未提交,坐标转换不能把临时 bounds 与上帧投影混用。
if mount.layout.is_some() {
return Err(VmError::InvalidArgument(
"coordinate conversion requires committed geometry",
));
}
// 显式输入是本地/视图坐标,按 coordinate 规则拒绝非法数值后再做空间转换。
let point = crate::geometry::Point::new(
crate::scroll::coordinate(&args[0])?,
crate::scroll::coordinate(&args[1])?,
);
// 根据查询方向决定正向或逆向映射;Option 保留不可投影/遮挡的失败结果。
let point =
// 查询方向决定先正投影局部点还是逆投影视图点;不能仅交换输出字段代替逆变换。
if matches!(self.query, Query::ToView) {
// ToView 先将局部点加节点原点,再套二维 view;若在世界面板内,还继续向外投影到页面。
// 先加本节点布局原点再套累计二维 view,之后如有 panel 再投影到外层页面。
let point = mount.geometry.view.unwrap_or_default().point(
crate::geometry::Point::new(
point.x + mount.geometry.x,
point.y + mount.geometry.y,
),
);
mount
.geometry
.panel
.as_ref()
.map_or(Some(point), |panel| panel.project_point(point))
} else {
// ToLocal 先撤销面板和视图投影,再减节点几何原点;不可逆时返回 Nil,而非虚构坐标。
mount.geometry.logical_point(point).map(|point| {
crate::geometry::Point::new(
point.x - mount.geometry.x,
point.y - mount.geometry.y,
)
})
};
// 转换成功才分配名义 Point;失败返回 Nil,不把无效数值写进结果属性。
if let Some(point) = point {
// 成功后创建名义 Point 的字段存储,不把转换结果作为未标类型的近似对象返回。
let result = cx.new_object()?;
// 需升级方法捕获的弱桌面以取得 Point 类型,桌面失效时明确报错。
let desktop = cx.upgrade(self.desktop)?.ok_or(VmError::InvalidArgument(
"coordinate conversion desktop expired",
))?;
crate::ui_bridge::attach_desktop_type(cx, result, "Point", desktop)?;
cx.set_attr(result, "x", crate::native_visual::value(point.x))?;
cx.set_attr(result, "y", crate::native_visual::value(point.y))?;
Value::Object(result)
} else {
Value::Nil
}
}
// 同一滚动入口区分绝对位置和相对增量,钳制、挂接状态与写屏障交由 scroll::set 统一执行。
Query::ScrollTo | Query::ScrollBy => {
crate::scroll::set(
cx,
self.owner,
self.desktop,
[
crate::scroll::coordinate(&args[0])?,
crate::scroll::coordinate(&args[1])?,
],
matches!(self.query, Query::ScrollBy),
)?;
Value::Nil
}
// 滚动层负责嵌套祖先、裁剪与偏移夹取,此适配器只提供具体出现身份。
Query::ScrollIntoView => {
crate::scroll::into_view(cx, self.owner, self.desktop)?;
Value::Nil
}
// 拟合以 Mount 的有限视口尺寸为准;不是全局 Page 相机重置。
Query::FitView => {
crate::viewport::fit(cx, &mount)?;
Value::Nil
}
// 射线拾取需要所属桌面构造名义命中值,先确认弱桌面仍有效。
Query::Pick3D => {
// 升级拾取所需桌面,不能通过旧 Mount 在已回收窗口环境里构造类型。
let desktop = cx
.upgrade(self.desktop)?
.ok_or(VmError::InvalidArgument("viewport desktop expired"))?;
crate::scene3d_model::pick(
cx,
desktop,
&mount,
crate::geometry::Point::new(
crate::scroll::coordinate(&args[0])?,
crate::scroll::coordinate(&args[1])?,
),
)?
}
// 返回帧中已钳制的有效位置与最大偏移,不直接暴露可能尚未投影的 scroll_offset 原值。
Query::ScrollPosition => {
// 只有 geometry.scroll 存在的出现才可查询滚动状态,普通节点明确报错。
let p = mount
.geometry
.scroll
.ok_or(VmError::InvalidArgument("Mount is not scrollable"))?;
// 滚动查询返回 x/y/maxX/maxY 的新记录,字段源于帧有效投影。
let result = cx.new_object()?;
// 逐项把有效偏移与上限写入返回记录,不暴露内部可变数组。
for (name, value) in [("x", p[0]), ("y", p[1]), ("maxX", p[2]), ("maxY", p[3])] {
cx.set_attr(result, name, Value::Number((value as f64).into()))?;
}
Value::Object(result)
}
Query::Bounds => {
// source_layout::Step 在调用 arrange 前填临时 Context.bounds;measure 阶段保持 None,避免读取尚未决定的分配。
// 布局只读调用中可查询当前安排阶段的临时 bounds;测量时尚无 bounds,明确报错。
// 非布局期则读取最后提交的 MountGeometry。
let geometry = if let Some(layout) = &mount.layout {
layout
.lock()
.map_err(|_| VmError::InvalidArgument("layout context poisoned"))?
.bounds
.ok_or(VmError::InvalidArgument(
"Mount geometry is unavailable during measurement",
))?
} else {
[
mount.geometry.x,
mount.geometry.y,
mount.geometry.width,
mount.geometry.height,
]
};
// 创建 Bounds 结果对象;名义定义由本方法签名保证与当前桌面一致。
let bounds = cx.new_object()?;
// 从本方法返回签名取得 Bounds 的名义类型,避免生成一个仅字段相似的普通对象。
let endor_core::CallType::Single(endor_core::AnnotationType::Nominal(definition)) =
&self.signature.returns
else {
return Err(VmError::InvalidArgument("invalid Mount bounds signature"));
};
cx.attach_type(bounds, *definition)?;
// 按 x/y/width/height 写入所选几何,另附 mount.version 作为 layoutVersion。
for (name, value) in [
("x", geometry[0]),
("y", geometry[1]),
("width", geometry[2]),
("height", geometry[3]),
] {
cx.set_attr(bounds, name, Value::Number((value as f64).into()))?;
}
cx.set_attr(
bounds,
"layoutVersion",
Value::Number((mount.version as i64).into()),
)?;
Value::Object(bounds)
}
// 请求焦点先让出现进入可见范围,再检查可聚焦资格并写入桌面请求;后续发布负责实际生效。
Query::Focus => {
crate::scroll::into_view(cx, self.owner, self.desktop)?;
// 只有可交互出现可以聚焦;滚入可见请求先由滚动层处理,焦点资格不由字符串类型名决定。
if !mount.geometry.interactive {
return Err(VmError::InvalidArgument("Mount is not focusable"));
}
// 焦点写入当前桌面的待处理请求,弱桌面过期则不能再设置。
let desktop = cx
.upgrade(self.desktop)?
.ok_or(VmError::InvalidArgument("Mount window has expired"))?;
cx.set_attr(
desktop,
"focus",
Value::Number((mount.geometry.id as i64).into()),
)?;
cx.set_attr(desktop, "focusRequested", Value::Bool(true))?;
Value::Nil
}
// 这里是 Mount 已提交几何的 contains 查询;源 Block 的 hitTest 覆盖由 source_hit 驱动另行执行。
Query::HitTest => {
// 布局临时上下文存在时 contains 不保证对应最终投影,必须拒绝源 Mount.hitTest。
if mount.layout.is_some() {
return Err(VmError::InvalidArgument(
"Mount hitTest is unavailable during layout",
));
}
// 命中坐标只要求 Number 可转有限 f32;与 scroll::coordinate 不同,此处不限制绝对值至一百万。
// hitTest 的输入只需有限 f32;此闭包刻意不用带一百万绝对值上限的滚动坐标解析。
let number = |value: &Value| -> Result<f32, VmError> {
// 只接受 Number,不把字符串或其他值隐式转为坐标。
let Value::Number(number) = value else {
return Err(VmError::InvalidArgument(
"point coordinates must be numbers",
));
};
// 解析到实际几何使用的 f32,溢出为无穷仍由下一步拒绝。
let number = number
.to_string()
.parse::<f32>()
.map_err(|_| VmError::InvalidArgument("invalid point coordinate"))?;
// NaN/无穷没有有效包含关系,不能送入 Rect 或面板射线计算。
if !number.is_finite() {
return Err(VmError::InvalidArgument("invalid point coordinate"));
}
Ok(number)
};
Value::Bool(mount.geometry.contains(crate::geometry::Point::new(
number(&args[0])?,
number(&args[1])?,
)))
}
// 其余枚举仅为弱层次查询,允许观察已脱离上下文;穷尽匹配保证不会把几何操作落到此处。
query => {
// 选择对应弱边再尝试升级,不额外保存或强连父/子/页面。
let weak = match query {
Query::Parent => mount.parent,
Query::FirstChild => mount.first_child,
Query::NextSibling => mount.next_sibling,
Query::Page => mount.page,
_ => unreachable!(),
};
// 弱链接不存在或目标已回收都返回 Nil;升级本身失败才传播 VM 错误。
weak.map(|weak| cx.upgrade(weak))
.transpose()?
.flatten()
.map(Value::Object)
.unwrap_or(Value::Nil)
}
};
Ok(NativeResult::Return(value))
}
}
/// 声明 Mount 查询方法的源类型契约,区分几何、层次与坐标操作。
fn mount_signature(
store: &endor_core::ObjectStore,
desktop: ObjectRef,
query: Query,
) -> Result<endor_core::CallSignature, VmError> {
use endor_core::{AnnotationType as T, CallEffect, CallSignature, CallType};
// 每个返回名义类型从该桌面 types 获取,不能拿另一 VM 同名类型拼接契约。
let nominal = |name| crate::ui_bridge::desktop_type(store, name, desktop).map(T::Nominal);
// 层次、拾取和坐标转换可能没有结果,使用 nullable 名义类型;变更操作返回 Unit。
let returns = match query {
// bounds 总返回 Bounds,测量期不可用时走错误而非 nullable。
Query::Bounds => nominal("Bounds")?.into(),
// 层次弱边可能为空或目标已回收,因此结果允许 Nil。
Query::Parent | Query::FirstChild | Query::NextSibling => {
CallType::nullable(nominal("Mount")?)
}
Query::Page => CallType::nullable(nominal("Page")?),
Query::HitTest => T::Bool.into(),
Query::Pick3D => CallType::nullable(nominal("RayHit3D")?),
Query::ToView | Query::ToLocal => CallType::nullable(nominal("Point")?),
// 滚动位置目前返回普通记录,契约 Any 与实现字段记录保持一致。
Query::ScrollPosition => T::Any.into(),
_ => T::Unit.into(),
};
// 滚动、相机拟合和焦点会写业务/桌面属性,声明 Mutating;其余查询允许在只读协议中调用。
let effect = if matches!(
query,
Query::ScrollTo | Query::ScrollBy | Query::ScrollIntoView | Query::FitView | Query::Focus
) {
CallEffect::Mutating
} else {
CallEffect::ReadOnly
};
// 显式参数契约与 Method.arity 保持相同分组,receiver 不计入已绑定实参。
let parameters = if matches!(
query,
Query::HitTest
| Query::Pick3D
| Query::ToView
| Query::ToLocal
| Query::ScrollTo
| Query::ScrollBy
) {
vec![T::Number, T::Number]
} else {
vec![]
};
// 先构造参数与效果,再替换精确返回契约,保留 nullable 名义类型信息。
let mut signature = CallSignature::exact(parameters, T::Any, effect);
signature.returns = returns;
Ok(signature)
}
#[derive(Debug, Default, Clone)]
/// 活动出现的弱索引。真实托管拥有关系在 desktop 的挂载字典中,索引不能充当隐藏 GC 根。
pub struct MountTable {
// Mount ID 到托管弱身份,真实强拥有者是 desktop.mounts 字典;order 单独保存原布局顺序。
pub active: HashMap<u64, WeakObjectRef>,
// 原布局先序 ID 列表,弱 HashMap 本身无序,生命周期退役需要这个确定顺序。
order: Vec<u64>,
}
#[derive(Debug)]
/// 同步出现关系时的共享临时工作状态,保存本轮重用、建立和退役安排。
/// MountTable::synchronize 包装它为零参 callable,让全部分配与写屏障经过 NativeContext;用户生命周期回调另由返回驱动执行。
struct Sync {
// 本次同步所属桌面,由同步函数 captures 显式保活,持有活动挂载字典。
desktop: ObjectRef,
// 本次活动 Page,可为空;同步内部转弱身份写入 Mount,避免反向保活。
page: Option<ObjectRef>,
// 业务 Block 弱注册表,用它把发布帧中的身份文本解析为现存对象。
nodes: Vec<WeakObjectRef>,
// 上一活动 Mount 弱表,用于按出现 ID 尝试复用。
old: HashMap<u64, WeakObjectRef>,
// 上一帧先序出现顺序,逆序退役用于保证孩子先于父卸载。
old_order: Vec<u64>,
// 本轮拥有的原生几何帧,数据不依赖外部借用生命周期。
frame: Frame,
// true 为布局准备阶段,安装临时 Context 并保留已有滚动偏移;false 为几何提交,清 Context。
preparing: bool,
// 同步成功后交回新弱索引,宿主函数调用结束后由 MountTable 移出。
output: Arc<Mutex<HashMap<u64, WeakObjectRef>>>,
}
#[derive(Debug)]
/// 将一个生命周期回调及参数接入同一 VM 调用序列。
struct LifecycleCall {
// 已经按 Block 名义类型解析到的生命周期源/原生方法。
function: ObjectRef,
// 调用时显式 receiver,包装 captures 保持它到序列实际执行。
block: ObjectRef,
// 当前具体出现上下文,作为第二参数传入同一个业务 Block 的钩子。
mount: ObjectRef,
// 卸载包装先取消 owner 任务;挂载包装不做取消。
unmount: bool,
}
impl NativeCallable for LifecycleCall {
/// Block、Mount 与用户钩子都已捕获,生命周期序列无参调用此适配器。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 卸载时先取消 Mount 所有任务,再以 Block/Mount 为实参请求 VM 执行用户 onMount/onUnmount,不在 Rust 中直接运行源体。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// 用户 onUnmount 运行前先取消该出现拥有的任务,防止退役后继续交付旧工作。
if self.unmount {
crate::tasks::cancel_owner(cx, self.mount)?;
}
Ok(NativeResult::Invoke {
function: self.function,
arguments: vec![Value::Object(self.block), Value::Object(self.mount)],
})
}
}
/// 构造生命周期调用所需的托管函数与参数,使回调遵循同一 VM 的引用和执行约束。
fn lifecycle_call(
cx: &mut NativeContext<'_>,
mount: ObjectRef,
name: &'static str,
) -> Result<ObjectRef, VmError> {
// Sync 为每个新增/退役出现建立包装,从 Mount 找到实际业务 receiver。
let block = cx.store().payload::<Mount>(mount, "Mount")?.block;
// 生命周期通过名义类型查找,不能被普通实例同名字段绕开协议。
let definition = cx
.store()
.type_of(block)?
.ok_or(VmError::InvalidArgument("untyped mounted Block"))?;
// 按 onMount/onUnmount 查真实方法,缺失类型/方法都表示协议未安装完整。
let function = cx
.store()
.lookup_method(definition, name)?
.ok_or(VmError::InvalidArgument("Block has no lifecycle method"))?;
// 生命周期包装显式捕获函数、Block、Mount,使序列执行到回调前这些对象不会被回收。
cx.allocate(ContextNativeFunction::new(
name,
LifecycleCall {
function,
block,
mount,
unmount: name == "onUnmount",
},
vec![function, block, mount],
))
}
impl NativeCallable for Sync {
/// 展示同步步骤从捕获的帧与弱节点表读取输入,无需外部实参。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 把弱业务身份升级为本轮对象映射,再同步帧中出现位置到托管 Mount,建立结构/几何与待通知生命周期。
fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result<NativeResult, VmError> {
// 帧只含 Block 字符串身份,先将弱节点表升级为本次同步可用的实际对象映射。
let mut blocks = HashMap::new();
// 弱注册表不保活隐藏业务树;只升级当前仍存活的 Block 为本次调用临时根。
for weak in &self.nodes {
// 失效弱节点直接忽略,帧若仍引用其身份则在下面明确拒绝。
if let Some(block) = cx.upgrade(*weak)? {
blocks.insert(block.to_string(), block);
}
}
// 把捕获的活动 Page 转为弱身份写进各 Mount,用于上下文比较和源码 page()。
let page = self
.page
.map(|page| cx.store().downgrade(page))
.transpose()?;
// 新帧 ID→托管 Mount 临时映射,第一遍全部建立后再填关系。
let mut objects = HashMap::new();
// 记录真实新建出现的先序次序,最后仅这些对象安排 onMount。
let mut created = Vec::new();
// 第一遍按帧顺序为每个几何找业务 Block 并复用/分配 Mount。
for geometry in &self.frame.mounts {
// 帧 block_id 只能对应仍可升级的业务对象,不能凭字符串新建一个替代身份。
let block = *blocks
.get(&geometry.block_id)
.ok_or(VmError::InvalidArgument(
"frame references an unavailable Block",
))?;
// 先按同一 Mount ID 找旧弱对象,失效项不具备复用资格。
let old = self
.old
.get(&geometry.id)
.map(|weak| cx.upgrade(*weak))
.transpose()?
.flatten();
// Mount ID 相同仍不足以复用:必须仍挂接、页面相同、真实 Block 相同,防止身份误归属。
let reusable = if let Some(old) = old {
// 检查旧 Mount 的真实 payload,页面/业务对象不一致时即使数值 ID 相同也必须新建。
let mount = cx.store().payload::<Mount>(old, "Mount")?;
(mount.mounted && mount.page == page && mount.block == block).then_some(old)
} else {
None
};
// 通过全部资格检查才复用,保留已有任务作用域和用户滚动偏移。
let object = if let Some(old) = reusable {
old
} else {
// 新出现从无关系、零偏移、无任务和无临时 layout 开始,随后安装类型与方法。
let object = cx.allocate(Mount {
block,
page,
parent: None,
first_child: None,
next_sibling: None,
geometry: geometry.clone(),
scroll_offset: [0.0, 0.0],
version: self.frame.layout_version,
mounted: true,
tasks: None,
layout: None,
})?;
crate::ui_bridge::attach_desktop_type(cx, object, "Mount", self.desktop)?;
// 每个新 Mount 安装捕获自身的原生方法;captures 中只放 Mount,桌面仍保留弱身份。
for (name, query) in [
("bounds", Query::Bounds),
("scrollTo", Query::ScrollTo),
("scrollBy", Query::ScrollBy),
("scrollPosition", Query::ScrollPosition),
("scrollIntoView", Query::ScrollIntoView),
("fitView", Query::FitView),
("pick3D", Query::Pick3D),
("toView", Query::ToView),
("toLocal", Query::ToLocal),
("parent", Query::Parent),
("firstChild", Query::FirstChild),
("nextSibling", Query::NextSibling),
("page", Query::Page),
("requestFocus", Query::Focus),
("hitTest", Query::HitTest),
] {
// 方法函数只捕获本 Mount;弱桌面用于按需获取能力,避免方法反向保活整个窗口。
let function = cx.allocate(ContextNativeFunction::new(
name,
Method {
signature: mount_signature(cx.store(), self.desktop, query)?,
owner: object,
desktop: cx.store().downgrade(self.desktop)?,
query,
},
vec![object],
))?;
cx.set_attr(object, name, Value::Object(function))?;
}
cx.set_attr(object, "id", Value::Number((geometry.id as i64).into()))?;
cx.set_attr(object, "block", Value::Object(block))?;
created.push(object);
object
};
objects.insert(geometry.id, object);
}
// 先建立全部 Mount,再第二遍补链接,保证 parent/child/sibling 无论先后都能找到目标身份。
// 第二遍建立新的强拥有字典,桌面切换到此字典后才能释放旧出现集合。
let root = cx.new_dict()?;
// 第二遍补齐所有弱层次关系和本轮几何,并将每个活动出现强持有在新字典中。
for geometry in &self.frame.mounts {
// 所有 Mount 已先创建,parent/child/sibling 可指向帧中任意先后位置。
let object = objects[&geometry.id];
// 帧链接 ID 在完整临时映射中转弱对象身份;不让父子边额外形成强引用环。
let link = |id: Option<u64>| -> Result<Option<WeakObjectRef>, VmError> {
id.map(|id| cx.store().downgrade(objects[&id])).transpose()
};
// 组装最终本轮 payload,任务作用域保留,关系/几何/版本使用本帧数据。
let mount = Mount {
tasks: cx.store().payload::<Mount>(object, "Mount")?.tasks,
block: blocks[&geometry.block_id],
page,
parent: link(geometry.parent)?,
first_child: link(geometry.first_child)?,
next_sibling: link(geometry.next_sibling)?,
geometry: geometry.clone(),
// 准备阶段沿用用户滚动状态;提交阶段以帧中有效偏移为准,非滚动容器归零。
scroll_offset: if self.preparing {
cx.store().payload::<Mount>(object, "Mount")?.scroll_offset
} else {
geometry.scroll.map(|s| [s[0], s[1]]).unwrap_or([0.0, 0.0])
},
version: self.frame.layout_version,
mounted: true,
// 临时 Context 只有原生派生数据,无托管根;提交时置 None,表示几何可供外部查询。
layout: if self.preparing {
Some(Arc::new(Mutex::new(crate::layout::Context::default())))
} else {
None
},
};
// 与旧 payload 比较避免无变化 replace_payload 造成大量 Contents 日志。
let old = cx.store().payload::<Mount>(object, "Mount")?;
// 只在上下文、几何、版本、链接或公开挂接属性确实需更新时替换 payload,减少无关变更日志。
if self.preparing
|| old.layout.is_some()
|| !old.mounted
|| old.version != mount.version
|| old.geometry != mount.geometry
|| old.page != mount.page
|| old.parent != mount.parent
|| old.first_child != mount.first_child
|| old.next_sibling != mount.next_sibling
|| matches!(
cx.store().get_attr(object, "mounted"),
Err(VmError::AttributeNotFound(_))
)
{
cx.replace_payload(object, mount)?;
cx.set_attr(object, "mounted", Value::Bool(true))?;
cx.set_attr(
object,
"layoutVersion",
Value::Number((self.frame.layout_version as i64).into()),
)?;
}
// 新字典逐项拥有活动 Mount;弱链接本身不负责让整棵出现树存活。
cx.dict_set(
root,
Value::Text(geometry.id.to_string()),
Value::Object(object),
)?;
}
// 生命周期包装按“全部卸载→全部新挂载”顺序收集,实际源执行留给返回 driver。
let mut hooks = Vec::new();
// Layout records are preorder; reversing guarantees children unmount
// before their parents. A Page change creates fresh Mount contexts.
// 旧帧按父先子后记录,逆序退役保证孩子 onUnmount 在父之前;复用对象不退役。
for id in self.old_order.iter().rev() {
// 从保存的确定顺序取旧身份,不依赖 HashMap 随机迭代安排卸载。
let weak = self.old[id];
// 对象仍存活且没有作为同一 ID 的新对象复用时才退役,避免重复调用 onUnmount。
if let Some(object) = cx.upgrade(weak)?
&& objects.get(id) != Some(&object)
{
// 退役副本先撤去挂接及结构链接,随后才安装卸载包装,旧句柄不能继续用于几何操作。
let mut mount = cx.store().payload::<Mount>(object, "Mount")?.clone();
// 先撤销挂接和全部关系,再安排回调;onUnmount 不能继续使用失效几何或沿旧链操作。
mount.mounted = false;
mount.layout = None;
mount.parent = None;
mount.first_child = None;
mount.next_sibling = None;
cx.replace_payload(object, mount)?;
cx.set_attr(object, "mounted", Value::Bool(false))?;
hooks.push(lifecycle_call(cx, object, "onUnmount")?);
}
}
// 新建顺序沿帧先序,onMount 默认父先子后;全部旧退役回调排在新挂载回调之前。
// preparing=true 也会生成这些包装,但 UiBridge 保存驱动到完成时才执行;取消准备时只释放驱动,不触发钩子。
// 按新帧先序安排挂载回调,父先于子;复用出现不重复通知。
for object in created {
hooks.push(lifecycle_call(cx, object, "onMount")?);
}
// 同步只返回生命周期序列,不在此处立即执行用户回调;调用方决定在后续阶段何时运行。
// 无钩子返回 Nil,有钩子构造 vm_sequence;同步本身不直接递归执行源函数。
let driver = if hooks.is_empty() {
None
} else {
Some(crate::vm_sequence::build(
cx,
hooks,
"<mount lifecycle>",
self.desktop,
)?)
};
// 新宿主索引仅保存弱引用,字典和生命周期包装才是实际托管拥有者。
let weak = objects
.into_iter()
.map(|(id, object)| Ok((id, cx.store().downgrade(object)?)))
.collect::<Result<HashMap<_, _>, VmError>>()?;
// 先让桌面拥有新字典,再公布原生弱索引;返回的驱动保持待退役对象直到回调结束。
cx.set_attr(self.desktop, "mounts", Value::Object(root))?;
*self
.output
.lock()
.map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))? = weak;
Ok(NativeResult::Return(
driver.map(Value::Object).unwrap_or(Value::Nil),
))
}
}
/// Explicitly owned roots protect the old presentation while preparation
/// temporarily replaces its desktop dictionary and Mount payloads.
/// 布局准备前的挂载快照与临时保留,用于取消恢复;成功提交后必须释放其保留引用。
/// UiBridge 在 source_layout 尚未完成的错误/取消路径调用 restore,进入完成投影/提交路径后调用 release。
/// 只覆盖挂载字典、旧 Mount payload 和两项公开属性,不保存 Page/业务字段、窗口命令或任务内部状态的完整事务快照。
pub struct Checkpoint {
// 旧弱表与顺序副本,restore 成功后替换回宿主表。
table: MountTable,
// 原 desktop.mounts 字典身份;由 roots 显式保活,None 表示原先没有该属性。
dictionary: Option<ObjectRef>,
// 保存 payload 与 mounted/layoutVersion 两项公开属性;None 表示原先没有属性,回滚要删除。
states: Vec<(ObjectRef, Mount, Option<Value>, Option<Value>)>,
// 检查点实际 retain/upgrade 得到的拥有引用,成功或失败退出都必须 release。
roots: Vec<ObjectRef>,
}
impl Checkpoint {
/// 释放检查点为回滚临时保留的托管根,成功提交后不再需要这些引用。
pub fn release(self, vm: &mut Vm) -> Result<(), VmError> {
// 聚合释放错误,单个根释放失败不阻止清理其他已保留根。
let mut result = Ok(());
// 逆序释放所有临时根,单项失败仍继续尝试余下根,最终返回最后一次释放错误。
for root in self.roots.into_iter().rev() {
// 保留最后一次释放错误,调用方仍知道资源清理没有完全成功。
if let Err(error) = vm.release(root) {
result = Err(error);
}
}
result
}
/// 恢复检查点中尚未发布的出现关系与布局上下文,并释放临时保留。
pub fn restore(
self,
vm: &mut Vm,
desktop: ObjectRef,
table: &mut MountTable,
) -> Result<(), VmError> {
// 先暂存恢复错误,让下方无论成败都尝试释放检查点拥有根。
// 不调用 onUnmount/onMount;准备态尚未执行这些通知,恢复只修复宿主派生关系,避免制造一轮虚假生命周期。
let result = (|| {
// 回滚先识别本轮新增的出现并将其脱离,再恢复原有 Mount payload、公开属性和桌面拥有字典。
let old: std::collections::HashSet<_> =
self.states.iter().map(|state| state.0).collect();
// 遍历准备阶段当前出现,识别不属于旧检查点的临时新建 Mount。
for weak in table.ordered() {
// 弱升级提供临时强根,处理结果暂存后必须释放再传播错误。
if let Some(object) = vm.upgrade(weak)? {
// 捕获单个对象退役过程的结果,确保后面的 release 仍执行。
let result = (|| {
// 只把准备阶段新出现脱离,旧出现将由保存 payload 原样恢复。
if !old.contains(&object) {
// 新对象即使仍被外部源引用,也必须标为未挂接并清除临时几何上下文与弱层次。
let mut mount = vm.payload::<Mount>(object, "Mount")?.clone();
mount.mounted = false;
mount.layout = None;
mount.parent = None;
mount.first_child = None;
mount.next_sibling = None;
vm.replace_payload(object, mount)?;
vm.set_attr(object, "mounted", Value::Bool(false))?;
}
Ok::<_, VmError>(())
})();
// 保存处理结果后先释放 upgrade 根,再传播处理错误,避免失败路径遗留临时强引用。
vm.release(object)?;
result?;
}
}
// 恢复每个旧 payload 和两项公开属性,不能只改宿主索引而留下 VM 状态不一致。
for (object, mount, mounted, version) in &self.states {
vm.replace_payload(*object, mount.clone())?;
// 原属性“缺失”和“存在且为 Nil”等值状态不同,逐项按 Option 恢复。
for (name, value) in [("mounted", mounted), ("layoutVersion", version)] {
// 原属性存在则恢复旧值,不存在则 del_attr;已缺失可忽略,其余删除错误仍要传播。
if let Some(value) = value {
vm.set_attr(*object, name, value.clone())?;
} else {
// 原本不存在的属性应删除;已经缺失视为已恢复,其他错误仍传播。
match vm.del_attr(*object, name) {
Ok(()) | Err(VmError::AttributeNotFound(_)) => {}
Err(error) => return Err(error),
}
}
}
}
// 恢复旧桌面字典的强拥有关系;原来没有字典时必须删去准备阶段新字典。
if let Some(dictionary) = self.dictionary {
vm.set_attr(desktop, "mounts", Value::Object(dictionary))?;
} else {
// 缺失可忽略,其他删除失败意味着恢复未完成,不能直接报告成功。
match vm.del_attr(desktop, "mounts") {
Ok(()) | Err(VmError::AttributeNotFound(_)) => {}
Err(error) => return Err(error),
}
}
*table = self.table.clone();
Ok(())
})();
// 恢复过程失败也要释放检查点根;先尝试两项操作,再按恢复错误优先的顺序返回。
// 先尝试释放全部检查点根,再优先返回恢复错误;不能因恢复失败跳过根清理。
let released = self.release(vm);
result?;
released
}
}
impl MountTable {
/// 保留布局准备前的 Mount 状态,供失败或取消路径恢复。
pub fn checkpoint(&self, vm: &mut Vm, desktop: ObjectRef) -> Result<Checkpoint, VmError> {
// 布局准备入口读取原挂载字典,保留“属性不存在”状态;非对象现值是非法内部状态。
let dictionary = match vm.get_attr(desktop, "mounts") {
Ok(Value::Object(dictionary)) => Some(dictionary),
Err(VmError::AttributeNotFound(_)) => None,
Ok(_) => return Err(VmError::InvalidArgument("invalid mount dictionary")),
Err(error) => return Err(error),
};
// 记录每次显式 retain/upgrade 的拥有引用,部分构造失败时也可统一清理。
let mut roots = Vec::new();
// 保留旧字典,避免准备阶段 desktop.mounts 换成新字典后旧出现提前回收,导致无法恢复。
if let Some(dictionary) = dictionary {
vm.retain(dictionary)?;
roots.push(dictionary);
}
// 先保存表/字典,逐项累积状态和根,不能在全部成功前交给调用方。
let mut checkpoint = Checkpoint {
table: self.clone(),
dictionary,
states: Vec::new(),
roots,
};
// 把中途读取/保留错误暂存,创建失败时仍释放已经取得的全部拥有根。
let result = (|| {
// 按旧展示顺序保存活动对象,便于恢复一致的上下文而不受 HashMap 顺序影响。
for weak in self.ordered() {
// 只有仍存活的 Mount 可保存,弱升级本身提供一个检查点拥有根。
if let Some(object) = vm.upgrade(weak)? {
// upgrade 已提供一个拥有根,直接记录待释放列表,不重复 retain;随后读取失败也会统一清理。
checkpoint.roots.push(object);
// 复制当前出现 payload,恢复时连同滚动请求、任务作用域及临时布局状态一起还原。
let mount = vm.payload::<Mount>(object, "Mount")?.clone();
// 保存 mounted/layoutVersion 两项独立源字段,不能假设它们与 payload 一定同步或类型固定。
let mut attributes = Vec::new();
// 逐项区分有值/缺失,其他读取错误不应被误当作缺失。
for name in ["mounted", "layoutVersion"] {
// 读取返回对象 Value 时还需额外 retain,Rust Value 副本本身不增加 VM 根。
let value = match vm.get_attr(object, name) {
Ok(value) => Some(value),
Err(VmError::AttributeNotFound(_)) => None,
Err(error) => return Err(error),
};
// 公开属性理论上可能持有对象,快照保存 Value 本身不会自动增加 VM 根,须额外 retain。
if let Some(Value::Object(object)) = &value {
vm.retain(*object)?;
checkpoint.roots.push(*object);
}
attributes.push(value);
}
checkpoint.states.push((
object,
mount,
attributes.remove(0),
attributes.remove(0),
));
}
}
Ok::<_, VmError>(())
})();
// 检查点创建到一半失败时释放已经取得的根,不返回一个不完整且可能泄漏的检查点。
if let Err(error) = result {
checkpoint.release(vm)?;
return Err(error);
}
Ok(checkpoint)
}
/// A faulted domain cannot run user onUnmount callbacks. Still detach every
/// context and release its owning dictionary through the normal heap barrier.
/// 退役当前出现表,撤销挂载关系并准备不再展示的生命周期状态。
pub fn retire(&mut self, vm: &mut Vm, desktop: ObjectRef) -> Result<(), VmError> {
// 故障域退役按孩子先父后清除出现,但绝不调用任何源 onUnmount。
for weak in self.ordered().into_iter().rev() {
// 当前仍存活的旧句柄要置为 detached;每次升级的强根处理后立即释放。
if let Some(object) = vm.upgrade(weak)? {
// 暂存 payload/公开属性更新结果,让故障路径也能清理弱升级拥有根。
let result = (|| {
// 清挂接、临时 layout 与父子兄弟链接,旧源句柄不得继续操作关闭的展示上下文。
let mut mount = vm.payload::<Mount>(object, "Mount")?.clone();
mount.mounted = false;
mount.layout = None;
mount.parent = None;
mount.first_child = None;
mount.next_sibling = None;
vm.replace_payload(object, mount)?;
vm.set_attr(object, "mounted", Value::Bool(false))
})();
vm.release(object)?;
result?;
}
}
// 故障退役不执行用户回调;将 desktop.mounts 换成空字典,再清除原生索引,撤销桌面拥有关系。
// 通过正常 VM 写屏障将 desktop.mounts 换为空字典,撤销实际托管强拥有边。
let dictionary = vm.new_dict()?;
// 先保存写入结果,再释放新字典的局部分配根;桌面属性成功后持有字典。
let result = vm.set_attr(desktop, "mounts", Value::Object(dictionary));
vm.release(dictionary)?;
result?;
self.active.clear();
self.order.clear();
Ok(())
}
/// 按原始展示顺序返回弱 Mount 身份,供事件和协议驱动器保持稳定顺序。
pub fn ordered(&self) -> Vec<WeakObjectRef> {
// 过滤仍在 active 的 ID 并保留旧帧顺序,事件/生命周期调用方不直接遍历 HashMap。
self.order
.iter()
.filter_map(|id| self.active.get(id).copied())
.collect()
}
/// 根据新结构复用或建立每次出现的 Mount,连接父子关系并准备生命周期调用。
pub fn synchronize(
&mut self,
vm: &mut Vm,
desktop: ObjectRef,
page: Option<ObjectRef>,
nodes: Vec<WeakObjectRef>,
frame: &Frame,
preparing: bool,
) -> Result<Option<ObjectRef>, VmError> {
// 同步函数与宿主表之间共享一次性输出弱表;Mutex 只传递索引,不承担托管根。
// 本函数不自行创建恢复点;准备调用方须先 checkpoint。调用失败时已有 VM 写入不由 self.active 的保留自动撤销。
let output = Arc::new(Mutex::new(HashMap::new()));
// 同步入口显式保活桌面与可选页面;旧表和节点表本身仍是弱索引,不增加隐式根。
let mut captures = vec![desktop];
captures.extend(page);
// 构造无参 native 同步入口,显式 captures 保活桌面和可选页面,其他输入为原生值/弱观察。
let function = vm.allocate(ContextNativeFunction::new(
"mount-sync",
Sync {
desktop,
page,
nodes,
old: self.active.clone(),
old_order: self.order.clone(),
frame: frame.clone(),
preparing,
output: output.clone(),
},
captures,
))?;
// 先保存调用结果再释放同步入口,错误时同样清理;只有成功后才替换活动索引与顺序。
// 让所有分配/写入在所属 VM 的 NativeContext 中执行,返回值可能是待执行生命周期驱动。
let result = vm.call(function, &[]);
vm.release(function)?;
// 先释放入口函数后再传播调用错误;失败不安装半完成的宿主 active/order。
let result = result?;
// 从共享输出移动弱表而不是克隆;生命周期驱动若存在则作为拥有结果交给调用方释放。
self.active = std::mem::take(
&mut *output
.lock()
.map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))?,
);
self.order = frame.mounts.iter().map(|mount| mount.id).collect();
// 仅 Object 驱动或 Nil 是合法同步返回;其他值先释放再报告内部协议错误。
match result {
Value::Object(hooks) => Ok(Some(hooks)),
Value::Nil => Ok(None),
// 非预期返回值也统一经 release_value 清理,再报告同步返回协议错误。
value => {
vm.release_value(value)?;
Err(VmError::InvalidArgument("invalid lifecycle batch"))
}
}
}
}
// 兼容旧 crate::mount::collection 调用路径,实际对象列表拥有/索引实现集中在 object_list 模块。
pub use crate::object_list::collection;