2243 lines
89 KiB
Rust
2243 lines
89 KiB
Rust
//! 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)
|
||
}
|