1093 lines
62 KiB
Rust
1093 lines
62 KiB
Rust
//! 把 Endor 方法调用分派到对应宿主操作。这里处理构造、树修改、绑定、状态与页面能力。
|
||
//! 先检查参数和只读阶段限制,再修改托管状态;对象引用的生命周期仍交给 NativeContext 和 Aster 管理。
|
||
//!
|
||
//! 调用来源:VM 直接执行 bootstrap 注册的 ui 自由函数,或由 TypeMethod/NativeFactory 剥离接收者/Type 后转交。
|
||
//! Function.owner 保存绑定对象,requested_type 保存原生子类构造请求;本文件 args 只包含显式原生参数。
|
||
//! 简单分支同步返回 NativeResult::Return;需要执行源协议/事件处理器的分支返回 Invoke,让 VM 接着推进调用栈。
|
||
//! NativeContext 追踪本次分配和 upgrade 的临时根,返回/委托时提升输出引用,其余在域退出时释放。
|
||
use super::*;
|
||
|
||
impl NativeCallable for Function {
|
||
/// VM 在调用边界查询 bootstrap/bind 写入的签名;由外层已校验的临时转交 Function 可返回 None。
|
||
fn signature(&self) -> Option<CallSignature> {
|
||
self.signature.clone()
|
||
}
|
||
/// VM 查询自由函数的实参数量,或供转交适配器使用;这里不再给 receiver/Type 首参加一。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(self.operation.arity())
|
||
}
|
||
|
||
/// VM 或外层原生调用适配器进入的实际业务分派,返回借用结果或委托;不负责 GUI 帧的原子发布。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
use Operation::*;
|
||
// 外层 TypeMethod/NativeFactory 会直接调用本方法,因此这里仍需防御性检查参数数量。
|
||
let arity = self.operation.arity();
|
||
// 显式区间不包含已剥离的 receiver/Type,后面 args[0] 等索引依赖这次验证。
|
||
let (minimum, maximum) = (arity.required, arity.maximum);
|
||
// Function 接收已剥离 receiver 的显式参数;先校验数量,后面索引 args 才安全。
|
||
if !(minimum..=maximum).contains(&args.len()) {
|
||
// 在任何分配或模型修改之前拒绝数量不符,避免越界或带半次副作用的失败。
|
||
return Err(invalid("incorrect UI method argument count"));
|
||
}
|
||
if matches!(self.operation, Camera2D) {
|
||
// 独立相机不是 Block,不进入下方的视觉 Data.kind 构造;requested_type 保留源子类。
|
||
let camera = new_camera2d(cx, &self.session, self.requested_type, args)?;
|
||
return Ok(NativeResult::Return(Value::Object(camera)));
|
||
}
|
||
// 3D 构造与普通 Block 分开,requested_type 保留源子类名义身份,否则从本 desktop 取默认类型。
|
||
if let SceneObject(kind) = self.operation {
|
||
// 只复制当前类型域身份,放锁后进入三维分配器;该分配器校验 kind 的实参与子类约束。
|
||
let desktop = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
// 类型构造保留请求身份,自由工厂选择当前 desktop 中 kind.name 对应的默认类型。
|
||
let definition =
|
||
self.requested_type
|
||
.unwrap_or(desktop_type(cx.store(), kind.name(), desktop)?);
|
||
// model::scene3d 建立托管几何/场景对象,此刻不投影、不光栅化也不建立 OS 资源。
|
||
let object = crate::scene3d_model::allocate(cx, desktop, definition, kind, args)?;
|
||
return Ok(NativeResult::Return(Value::Object(object)));
|
||
}
|
||
if matches!(self.operation, CameraControl) {
|
||
// receiver 是控制器,两个显式参数是相机与手势;实际状态更新由 scene3d_model::control 解释。
|
||
crate::scene3d_model::control(
|
||
cx,
|
||
self.owner
|
||
.ok_or(invalid("controller requires a receiver"))?,
|
||
object(args.first())?,
|
||
object(args.get(1))?,
|
||
)?;
|
||
return Ok(NativeResult::Return(Value::Nil));
|
||
}
|
||
if matches!(self.operation, Translate3D | Rotate3D) {
|
||
// 三分量实参共用转换入口,末尾布尔值区分旋转/位移;对象缺少 owner 时直接报错。
|
||
crate::scene3d_model::transform(
|
||
cx,
|
||
self.owner
|
||
.ok_or(invalid("3D transform requires a receiver"))?,
|
||
args,
|
||
matches!(self.operation, Rotate3D),
|
||
)?;
|
||
return Ok(NativeResult::Return(Value::Nil));
|
||
}
|
||
// implement 修改显式能力关系;implements 查类型,supports 查对象实际能力,不靠同名字段鸭子类型推断。
|
||
if matches!(self.operation, Implement | Implements | Supports) {
|
||
// 第二实参必须是托管 Trait,注册和查询共用身份,但标量 supports 仍需先验证 Trait 本身。
|
||
let capability = object(args.get(1))?;
|
||
// 此分支计算语言返回值,随后统一包 Return;只有 Implement 真正修改类型关系。
|
||
let value = match self.operation {
|
||
Implement => {
|
||
// 第一参数是 Type,VM 的 implement_trait 检查能力契约并建立托管引用关系。
|
||
cx.implement_trait(object(args.first())?, capability)?;
|
||
Value::Nil
|
||
}
|
||
Implements => Value::Bool(
|
||
cx.store()
|
||
.implements_trait(object(args.first())?, capability)?,
|
||
),
|
||
Supports => {
|
||
// 即使第一参数是标量也先验证第二参数真为 Trait;不能用 false 掩盖错误能力对象。
|
||
cx.store()
|
||
.payload::<endor_core::EndorTraitObject>(capability, "Trait")?;
|
||
Value::Bool(match args[0] {
|
||
// 对象按实际能力契约查询,标量没有这里的宿主对象能力而返回 false。
|
||
Value::Object(value) => cx.store().supports(value, capability)?,
|
||
_ => false,
|
||
})
|
||
}
|
||
// 外层 matches 已限定三种操作,其他枚举值到这里表示分派实现内部不一致。
|
||
_ => unreachable!(),
|
||
};
|
||
return Ok(NativeResult::Return(value));
|
||
}
|
||
if matches!(self.operation, IsInstance | IsSubtype) {
|
||
// 当前实现要求两者都解码成对象:签名中的 Any 不等于可在这里传 nil 后获得 false。
|
||
let candidate = object(args.first())?;
|
||
// expected 必须属于此 VM 的真实类型体系;关系查询不按类型名字字符串比较。
|
||
let expected = object(args.get(1))?;
|
||
// 实例关系检查对象的类型,子类型关系检查两个 Type 的继承关系,不能混为同一查询。
|
||
let value = if matches!(self.operation, IsInstance) {
|
||
cx.store().is_instance(candidate, expected)?
|
||
} else {
|
||
// isSubtype 用于类型间关系,不把 candidate 当实例读取其 type_of。
|
||
cx.store().is_subtype(candidate, expected)?
|
||
};
|
||
return Ok(NativeResult::Return(Value::Bool(value)));
|
||
}
|
||
// 显式失效属于模型变更,只读阶段拒绝;paint/layout/content 精度不同,后续分别清理对应缓存。
|
||
if matches!(self.operation, Invalidate) {
|
||
if cx.store().is_read_only() {
|
||
// measure/paint 等只读域不能靠显式失效反复制造下一轮更新反馈。
|
||
return Err(invalid(
|
||
"invalidation is forbidden during read-only rendering",
|
||
));
|
||
}
|
||
// 仅省略参数才默认 layout,错误类型不回退;transpose 保留显式解析失败。
|
||
let reason = args
|
||
.first()
|
||
.map(|value| text(Some(value)))
|
||
.transpose()?
|
||
.unwrap_or_else(|| "layout".into());
|
||
if !matches!(reason.as_str(), "paint" | "layout" | "content") {
|
||
// 精度标签必须已知,否则不能可靠决定清哪些缓存和依赖集合。
|
||
return Err(invalid(
|
||
"invalidation reason must be paint, layout or content",
|
||
));
|
||
}
|
||
// TypeMethod 已把 receiver 移入 owner,没有绑定对象的自由调用不能执行此方法。
|
||
let owner = self.owner.ok_or(invalid("invalidation requires a Block"))?;
|
||
// 以解析后的共享 Data 身份归并别名,不能只让调用者这一个包装对象失效。
|
||
let (data, _) = block::resolve(cx.store(), owner)?;
|
||
// 锁内只遍历弱节点并更新派生失效状态,不在这里调用用户布局/绘制方法。
|
||
let mut session = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// 去重后的 Block 身份集合供每个窗口判断是否包含同一份受影响的共享业务状态。
|
||
let mut affected = std::collections::HashSet::new();
|
||
// 找出解析到同一底层 Data 的所有别名,失效共享数据时不能只刷新调用者包装身份。
|
||
for weak in &session.nodes {
|
||
if let Some(block) = cx.upgrade(*weak)?
|
||
&& block::resolve(cx.store(), block)?.0 == data
|
||
{
|
||
// NativeContext 持有升级临时根直到本次调用结束,不为弱索引永久添加拥有关系。
|
||
affected.insert(block);
|
||
}
|
||
}
|
||
for presentation in session.presentations.values_mut() {
|
||
// 同一 Data 可以出现在多个窗口,失效通知必须广播,不能仅清 current 的缓存。
|
||
presentation.invalidate(cx, &affected, &reason)?;
|
||
}
|
||
return Ok(NativeResult::Return(Value::Nil));
|
||
}
|
||
if matches!(self.operation, Mounts) {
|
||
// 返回的是这个 Block 的具体展示出现,不能把底层 Data 相同的其他别名也算到 owner 上。
|
||
let owner = self.owner.ok_or(invalid("mount query requires a Block"))?;
|
||
// 查全部窗口,锁内只读取原生 Mount 表与托管 payload,不执行源回调。
|
||
let session = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// 暂留窗口/出现 ID 作为排序键,最后列表只暴露 Mount 对象,不暴露宿主元组。
|
||
let mut positions = Vec::new();
|
||
for (window, presentation) in &session.presentations {
|
||
// 每窗活动表只有当前仍展示的出现,死亡弱项和其他 Block 的出现都不加入结果。
|
||
for (id, weak) in &presentation.mounts.active {
|
||
if let Some(object) = cx.upgrade(*weak)?
|
||
&& cx
|
||
.store()
|
||
.payload::<crate::mount::Mount>(object, "Mount")?
|
||
.block
|
||
== owner
|
||
{
|
||
positions.push((*window, *id, object));
|
||
}
|
||
}
|
||
}
|
||
// 跨窗口 Mount 查询按窗口/出现 ID 稳定排序,不依赖 HashMap 遍历顺序;返回每个具体出现而非几何副本。
|
||
positions.sort_unstable_by_key(|(window, id, _)| (*window, *id));
|
||
// 用专用托管集合保留结果引用和只读访问契约,不把 Rust Vec 直接当源值返回。
|
||
let set = crate::mount::collection(
|
||
cx,
|
||
positions.into_iter().map(|(_, _, object)| object).collect(),
|
||
session.desktop,
|
||
)?;
|
||
return Ok(NativeResult::Return(Value::Object(set)));
|
||
}
|
||
if matches!(self.operation, NewWindow) {
|
||
// 持锁分配新的窗口 ID 并建立 Presentation,create_window 还会检查只读/退出/数量限制。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// Window 只能通过 Type 构造入口创建,缺少 requested_type 说明不是合法的类型调用。
|
||
let definition = self
|
||
.requested_type
|
||
.ok_or(invalid("Window constructor requires a type"))?;
|
||
// 返回托管资源对象,原生窗口由随后发布的帧/命令驱动 GUI 创建。
|
||
let window = create_window(cx, &mut state, definition)?;
|
||
return Ok(NativeResult::Return(Value::Object(window)));
|
||
}
|
||
if matches!(self.operation, CloseWindow) {
|
||
// 资源对象来自 bound receiver,不用 current 猜要关闭哪个窗口。
|
||
let window = self
|
||
.owner
|
||
.ok_or(invalid("window close requires a receiver"))?;
|
||
// 用 payload 的资源 ID 定位 Presentation;关闭状态由 window::close 在模型中维护。
|
||
let id = cx
|
||
.store()
|
||
.payload::<crate::window::ManagedWindow>(window, "Window")?
|
||
.id;
|
||
crate::window::close(cx, window)?;
|
||
// model 关闭成功后才改宿主展示状态,关闭失败不能提前标记为已退场。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// 关闭托管窗口后为 Presentation 排空 pending 并标 closing,后续刷新才安排 Mount 退役与空帧。
|
||
if let Some(view) = state.presentations.get_mut(&id) {
|
||
cx.set_attr(view.desktop, "pending", Value::Nil)?;
|
||
view.pending = Some(None);
|
||
view.force_frame = true;
|
||
view.closing = true;
|
||
}
|
||
return Ok(NativeResult::Return(Value::Nil));
|
||
}
|
||
if matches!(self.operation, Window) {
|
||
// ui.window() 查询当前选择窗口的资源对象;和 Window() 分配新窗口的路径不同。
|
||
let desktop = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
return Ok(NativeResult::Return(
|
||
cx.store().get_attr(desktop, "window")?,
|
||
));
|
||
}
|
||
if matches!(self.operation, Viewport) {
|
||
// ui.viewport() 从当前窗口的输入对象生成结果,不直接读取 winit/OS 尺寸。
|
||
let desktop = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
// 运行时还没提供尺寸时属性为 nil,明确报不可用,不把它伪装成零宽高。
|
||
let Value::Object(input) = cx.store().get_attr(desktop, "viewport")? else {
|
||
return Err(invalid("window viewport is unavailable"));
|
||
};
|
||
// 两个普通属性读取参与 VM 依赖跟踪,尺寸变化可让调用方的测量失效。
|
||
let width = cx.store().get_attr(input, "width")?;
|
||
// 返回 MeasureResult 协议值而非把内部输入对象本身暴露给源程序修改。
|
||
let height = cx.store().get_attr(input, "height")?;
|
||
return Ok(NativeResult::Return(Value::Object(
|
||
crate::native_visual::record(
|
||
cx,
|
||
desktop,
|
||
"MeasureResult",
|
||
vec![("width", width), ("height", height)],
|
||
)?,
|
||
)));
|
||
}
|
||
// 诊断会读对象总数与宿主变化统计,禁止在只读渲染中观察并形成反馈依赖。
|
||
if matches!(self.operation, Stats) {
|
||
if cx.store().is_read_only() {
|
||
// 即使签名标 External,这里仍防御直接适配器调用,防止布局自观察计数形成反馈。
|
||
return Err(invalid(
|
||
"runtime statistics are unavailable during read-only rendering",
|
||
));
|
||
}
|
||
// 各窗口共享的计数与当前展示派生计数一起投影,锁内不推进刷新。
|
||
let session = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// blocks/pages 数的是成功升级的存活业务对象,而不是弱表历史长度或 Mount 出现数。
|
||
let mut blocks = 0_i64;
|
||
// 页面是 Block 的子集,同一页跨窗口迁移不会增加业务页面计数。
|
||
let mut pages = 0_i64;
|
||
for weak in &session.nodes {
|
||
if let Some(object) = cx.upgrade(*weak)? {
|
||
blocks += 1;
|
||
if cx.store().payload::<block::Block>(object, "Block")?.page {
|
||
pages += 1;
|
||
}
|
||
}
|
||
}
|
||
// 统计对象总数在分配 stats 结果之前采样,避免把本次诊断结果计入自己。
|
||
let objects = cx.store().object_count() as i64;
|
||
// session 的 Deref 指向 current,tabs/mounts 与布局计数属于当前窗口,而非跨窗求和。
|
||
let tabs = cx.store().dict_items(session.tabs)?.len() as i64;
|
||
// 新普通对象只装原生数值/文本,不把 VM store 或 Session 句柄暴露给程序。
|
||
let stats = cx.new_object()?;
|
||
for (name, value) in [
|
||
("objects", objects),
|
||
("blocks", blocks),
|
||
("pages", pages),
|
||
("tabs", tabs),
|
||
("mounts", session.mounts.active.len() as i64),
|
||
("snapshotVisited", session.snapshot_visited as i64),
|
||
("snapshotReused", session.snapshot_reused as i64),
|
||
("measured", session.layout.measured as i64),
|
||
("sourceMeasureHits", session.measure_cache.hits as i64),
|
||
("sourceMeasureMisses", session.measure_cache.misses as i64),
|
||
("arranged", session.layout.arranged as i64),
|
||
("layoutPasses", session.layout.settle_passes as i64),
|
||
("layoutSlices", session.layout.slices as i64),
|
||
("refreshYields", session.refresh_yields as i64),
|
||
("refreshRestarts", session.refresh_restarts as i64),
|
||
("layoutCancels", session.layout_cancels as i64),
|
||
("paintCancels", session.paint_cancels as i64),
|
||
("paintSlices", session.paint_slices as i64),
|
||
("bindingEvaluations", session.binding_evaluations as i64),
|
||
(
|
||
"dependencyInvalidations",
|
||
session.dependency_invalidations as i64,
|
||
),
|
||
] {
|
||
// 统一转成语言整数,字段名字是 ui.stats 的观察接口,不影响调度逻辑。
|
||
cx.set_attr(stats, name, Value::Number(value.into()))?;
|
||
}
|
||
// 单独保留文本失效原因,数值计数不能解释究竟哪条源属性触发了重算。
|
||
cx.set_attr(
|
||
stats,
|
||
"lastInvalidDependency",
|
||
Value::Text(session.last_invalid_dependency.clone()),
|
||
)?;
|
||
cx.set_attr(
|
||
stats,
|
||
"windowDependencyDiagnostics",
|
||
Value::Text(
|
||
session
|
||
.presentations
|
||
.iter()
|
||
.filter(|(_, view)| !view.last_invalid_dependency.is_empty())
|
||
.map(|(id, view)| format!("{id}: {}", view.last_invalid_dependency))
|
||
.collect::<Vec<_>>()
|
||
.join("; "),
|
||
),
|
||
)?;
|
||
return Ok(NativeResult::Return(Value::Object(stats)));
|
||
}
|
||
// 状态是普通名义 Object,value 属性经 VM 追踪,set/increment 方法捕获同一对象,不额外建立原生状态副本。
|
||
if matches!(self.operation, State) {
|
||
// value 及绑定方法都是普通托管属性,GC 能追踪回调和状态之间的真实边。
|
||
let state = cx.new_object()?;
|
||
attach_named_type(cx, state, "Object", &self.session)?;
|
||
cx.set_attr(state, "value", args[0].clone())?;
|
||
bind(cx, state, "set", StateSet, &self.session)?;
|
||
bind(cx, state, "increment", Increment, &self.session)?;
|
||
return Ok(NativeResult::Return(Value::Object(state)));
|
||
}
|
||
if matches!(self.operation, StateSet | Increment) {
|
||
// 状态对象由 bind 捕获,不能把 args[0] 当 receiver 再切一次参数。
|
||
let owner = self.owner.ok_or(invalid("state method has no owner"))?;
|
||
// set 接受任意新值;increment 则对当前 value 做精确整数读取与溢出检查。
|
||
let value = if matches!(self.operation, StateSet) {
|
||
args[0].clone()
|
||
} else {
|
||
// 先限制 Number 表示,再从中取可表示 i64 的整数;不截断十进制或二进制小数。
|
||
let Value::Number(value) = cx.store().get_attr(owner, "value")? else {
|
||
return Err(invalid("increment requires an integer state"));
|
||
};
|
||
// increment 要求可表示为 i64 的整数且加一不溢出,不能把任意浮点状态强转整数。
|
||
let next = value
|
||
.as_i64()
|
||
.and_then(|n| n.checked_add(1))
|
||
.ok_or(invalid("integer state increment exceeds its range"))?;
|
||
Value::Number(next.into())
|
||
};
|
||
// 通过统一属性写入口更新版本,textOf/绑定的依赖失效因此能被下一轮刷新观察。
|
||
cx.set_attr(owner, "value", value)?;
|
||
return Ok(NativeResult::Return(Value::Nil));
|
||
}
|
||
if matches!(self.operation, TextOf) {
|
||
// 先保存被观察对象和属性名;这里不是把当前值立即格式化成一个永不更新的字符串。
|
||
let source = object(args.first())?;
|
||
// 属性名必须是真字符串,不能从任意对象 repr 推导不稳定的名字。
|
||
let field = text(args.get(1))?;
|
||
// textOf 建立前确认字段可读取,具体字符串化与依赖记录发生在快照提取时。
|
||
cx.store().get_attr(source, &field)?;
|
||
// 字号可选但必须是等级整数,缺省为 1;颜色在后面按独立可选参数解析。
|
||
let size = args
|
||
.get(2)
|
||
.map(|v| integer(Some(v)))
|
||
.transpose()?
|
||
.unwrap_or(1);
|
||
if !(1..=8).contains(&size) {
|
||
// 数字能转为整数不代表是受支持等级,在分配 TextBlock 之前拒绝越界。
|
||
return Err(invalid("text size must be 1 through 8"));
|
||
}
|
||
// 初始文字为空,真正内容留给快照读取 source.field;构造不冻结当前属性值。
|
||
let mut data = Data::new(
|
||
Kind::Text {
|
||
value: String::new(),
|
||
size: size as u32,
|
||
color: color(args.get(3), "#242422")?,
|
||
},
|
||
vec![],
|
||
0.0,
|
||
0.0,
|
||
);
|
||
// 保留源对象强边与字段名,不在创建时复制一个静态字符串;后续属性变化会让文字快照重算。
|
||
data.text_binding = Some((source, field));
|
||
return Ok(NativeResult::Return(Value::Object(new_block(
|
||
cx,
|
||
data,
|
||
&self.session,
|
||
)?)));
|
||
}
|
||
// 普通组件先组装 Data,再统一由 new_typed_block 安装 Cell、属性和名义类型;不直接写原生窗口。
|
||
let constructor = match self.operation {
|
||
// 滚动视口有限宽高和唯一内容 Block;滚动轴/偏移由属性和 Mount 后续处理。
|
||
Scroll => Some(Data::new(
|
||
Kind::Scroll {
|
||
width: dimension(args.first())?,
|
||
height: dimension(args.get(1))?,
|
||
},
|
||
vec![object(args.get(2))?],
|
||
0.0,
|
||
0.0,
|
||
)),
|
||
// 文本是叶节点,size 的 u64→u32 转换和下面统一 1..=8 检查分别限制表示与语义。
|
||
Text => Some(Data::new(
|
||
Kind::Text {
|
||
value: text(args.first())?,
|
||
size: args
|
||
.get(1)
|
||
.map(|v| integer(Some(v)))
|
||
.transpose()?
|
||
.unwrap_or(1)
|
||
.try_into()
|
||
.map_err(|_| invalid("text size exceeds limits"))?,
|
||
color: color(args.get(2), "#242422")?,
|
||
},
|
||
vec![],
|
||
0.0,
|
||
0.0,
|
||
)),
|
||
// 宽高必填,圆角缺省零,颜色缺省浅蓝;每个尺寸都先经有限非负检查。
|
||
Rect => Some(Data::new(
|
||
Kind::Rect {
|
||
width: dimension(args.first())?,
|
||
height: dimension(args.get(1))?,
|
||
radius: args
|
||
.get(2)
|
||
.map(|v| dimension(Some(v)))
|
||
.transpose()?
|
||
.unwrap_or(0.0),
|
||
color: color(args.get(3), "#DCE8FF")?,
|
||
},
|
||
vec![],
|
||
0.0,
|
||
0.0,
|
||
)),
|
||
// 空白只提供主轴占位,不带孩子或绘图颜色。
|
||
Spacer => Some(Data::new(
|
||
Kind::Spacer(dimension(args.first())?),
|
||
vec![],
|
||
0.0,
|
||
0.0,
|
||
)),
|
||
// 三种容器共用参数解码,Kind 决定后续横排/纵排/叠放,不在这里计算子项坐标。
|
||
Row | Column | Overlay => Some(Data::new(
|
||
if matches!(self.operation, Row) {
|
||
// 行布局随后沿水平主轴累计孩子位置。
|
||
Kind::Row
|
||
} else if matches!(self.operation, Overlay) {
|
||
// 叠放布局共享内容原点,不采用线性 gap 累加位置。
|
||
Kind::Overlay
|
||
} else {
|
||
// 此入口仅剩 Column,后续沿纵向排列。
|
||
Kind::Column
|
||
},
|
||
// 线性容器固定前两参为 padding/gap,后续都是孩子对象;组成合法性在 Block 分配前统一检查。
|
||
args[2..]
|
||
.iter()
|
||
.map(|v| object(Some(v)))
|
||
.collect::<Result<_, _>>()?,
|
||
dimension(args.first())?,
|
||
dimension(args.get(1))?,
|
||
)),
|
||
// 空 Page 尚未登记到任何窗口,直到 render/open 才安排展示。
|
||
Page => Some(Data::new(Kind::Page, vec![], 0.0, 0.0)),
|
||
// 二维场景只有孩子对象序列;各对象的 x/y 等世界属性在快照提取时读取。
|
||
Scene2D => Some(Data::new(
|
||
Kind::Scene2D,
|
||
args.iter()
|
||
.map(|value| object(Some(value)))
|
||
.collect::<Result<_, _>>()?,
|
||
0.0,
|
||
0.0,
|
||
)),
|
||
CanvasViewport => {
|
||
// 视口自身参加有限布局,第三实参保存 Scene2D 引用,不把场景孩子展开成行列内容。
|
||
let mut data = Data::new(
|
||
Kind::CanvasViewport {
|
||
width: dimension(args.first())?,
|
||
height: dimension(args.get(1))?,
|
||
},
|
||
vec![object(args.get(2))?],
|
||
0.0,
|
||
0.0,
|
||
);
|
||
// 每个新二维视口默认新建独立 Camera2D;显式属性共享相机才会联动其他视口。
|
||
data.camera = Some(new_camera2d(cx, &self.session, None, &[])?);
|
||
Some(data)
|
||
}
|
||
Viewport3D => {
|
||
// 当前 desktop 提供相机/控制器的名义类型,所有默认资源在同一 VM 内创建。
|
||
let desktop = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
// 默认相机使用透视投影;这里分配模型对象,不立即生成场景渲染快照。
|
||
let definition = desktop_type(cx.store(), "PerspectiveCamera", desktop)?;
|
||
// 空参数选择场景模块的默认姿态,之后源属性仍可显式换相机或改参数。
|
||
let camera = crate::scene3d_model::allocate(
|
||
cx,
|
||
desktop,
|
||
definition,
|
||
crate::scene3d_model::Constructor::Perspective,
|
||
&[],
|
||
)?;
|
||
// 普通 children 先为空,世界场景独立存于 scene3d 字段;面板 UI 会在提取时派生。
|
||
let mut data = Data::new(
|
||
Kind::Viewport3D {
|
||
width: dimension(args.first())?,
|
||
height: dimension(args.get(1))?,
|
||
},
|
||
vec![],
|
||
0.,
|
||
0.,
|
||
);
|
||
data.camera = Some(camera);
|
||
data.scene3d = Some(object(args.get(2))?);
|
||
// 每个新视口独立控制器,避免两个不同相机的输入共享原生控制状态。
|
||
let definition = desktop_type(cx.store(), "OrbitController", desktop)?;
|
||
// 三维视口默认独立透视相机及 OrbitController,Scene3D 引用则来自调用者,可显式共享世界。
|
||
data.controller = Some(crate::scene3d_model::allocate(
|
||
cx,
|
||
desktop,
|
||
definition,
|
||
crate::scene3d_model::Constructor::Orbit,
|
||
&[],
|
||
)?);
|
||
Some(data)
|
||
}
|
||
// 按钮标签也是普通 TextBlock 孩子,布局和生命周期沿同一组成树,不是原生按钮硬编码文字。
|
||
Button => {
|
||
// 先创建真实文字孩子,让按钮尺寸和文本测量、字体缓存、子树生命周期保持一致。
|
||
let label = new_block(
|
||
cx,
|
||
Data::new(
|
||
Kind::Text {
|
||
value: text(args.first())?,
|
||
size: 1,
|
||
color: "#243047".into(),
|
||
},
|
||
vec![],
|
||
0.0,
|
||
0.0,
|
||
),
|
||
&self.session,
|
||
)?;
|
||
Some(Data::new(Kind::Button, vec![label], 10.0, 0.0))
|
||
}
|
||
// 非构造操作继续进入页面/实例方法分派;None 不是失败,也不创建占位对象。
|
||
_ => None,
|
||
};
|
||
// 统一检查文本等级和 UTF-8 字节量,再按 requested_type 生成实际对象,保留子类工厂身份。
|
||
if let Some(data) = constructor {
|
||
if let Kind::Text { size, value, .. } = &data.kind
|
||
&& (!(1..=8).contains(size) || value.len() > 16_384)
|
||
{
|
||
// 两条文本工厂共用语义限制;防止类型工厂参数能转 u32 却超出实际支持字号等级。
|
||
return Err(invalid("text exceeds size limits"));
|
||
}
|
||
return Ok(NativeResult::Return(Value::Object(new_typed_block(
|
||
cx,
|
||
data,
|
||
&self.session,
|
||
self.requested_type,
|
||
)?)));
|
||
}
|
||
if matches!(self.operation, OpenValue | Present)
|
||
&& let Value::Object(value) = args[0]
|
||
{
|
||
// 对象值可能实现 Presentable;标量不可能走这一协议路径,稍后按普通文字转换处理。
|
||
let (desktop, current) = {
|
||
// master 用于共享能力类型,current 用于传给源 present 的窗口资源,二者不能混用。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
(state.master, state.desktop)
|
||
};
|
||
// 非 Block 对象若显式支持 Presentable,调用其 present;已有 Block 直接进入下面的展示转换。
|
||
let capability = desktop_type(cx.store(), "Presentable", desktop)?;
|
||
if !cx
|
||
.store()
|
||
.object(value)?
|
||
.payload()
|
||
.as_any()
|
||
.is::<block::Block>()
|
||
&& cx.store().supports(value, capability)?
|
||
{
|
||
// 只有非 Block 且显式实现能力的对象才执行源 present,不能凭同名属性鸭子类型推断。
|
||
// 能力实现依附于类型,需从该类型的方法表取 present,而非随意的实例属性。
|
||
let definition = cx
|
||
.store()
|
||
.type_of(value)?
|
||
.ok_or(invalid("untyped Presentable"))?;
|
||
// lookup_method 处理继承规则,缺少实现即能力契约错误。
|
||
let function = cx
|
||
.store()
|
||
.lookup_method(definition, "present")?
|
||
.ok_or(invalid("missing present method"))?;
|
||
// 源 present 接收实际目标窗口,不能总把主窗口传给副窗口中的调用。
|
||
let window = object(Some(&cx.store().get_attr(current, "window")?))?;
|
||
// 源方法返回后由 PresentResult 检查真实 Block,并按 open 标志决定是否包装 Page。
|
||
let complete = cx.allocate(ContextNativeFunction::new(
|
||
"validate presentation",
|
||
PresentResult {
|
||
open: matches!(self.operation, OpenValue),
|
||
session: self.session.clone(),
|
||
},
|
||
vec![desktop],
|
||
))?;
|
||
// 小驱动先调用 present(value, window),再把结果交 PresentResult 验证/可选打开,所有委托共用当前 VM 预算。
|
||
let instructions = [
|
||
endor_core::Instruction::new(endor_core::Opcode::LoadConst, 0),
|
||
endor_core::Instruction::simple(endor_core::Opcode::PushNull),
|
||
endor_core::Instruction::new(endor_core::Opcode::LoadConst, 1),
|
||
endor_core::Instruction::simple(endor_core::Opcode::PushNull),
|
||
endor_core::Instruction::new(endor_core::Opcode::LoadConst, 2),
|
||
endor_core::Instruction::new(endor_core::Opcode::LoadConst, 3),
|
||
endor_core::Instruction::new(endor_core::Opcode::Call, 2),
|
||
endor_core::Instruction::new(endor_core::Opcode::Call, 1),
|
||
endor_core::Instruction::simple(endor_core::Opcode::ReturnValue),
|
||
];
|
||
// 常量表按字节码的 0..3 索引依次提供续接、源方法、receiver 和 Window。
|
||
let code = endor_core::EndorCode::from_instructions(
|
||
&instructions,
|
||
vec![
|
||
Value::Object(complete),
|
||
Value::Object(function),
|
||
Value::Object(value),
|
||
Value::Object(window),
|
||
],
|
||
endor_core::CodeLayout::default(),
|
||
)?
|
||
.with_location("Presentable.present", "<tessera>", 1);
|
||
// 模板包装成托管代码后,驱动函数的强边负责维持常量/代码存活。
|
||
let code = cx.allocate(endor_core::EndorCodeObject::new(code))?;
|
||
// 驱动只访问常量,不需要用户命名空间,私有空字典避免污染全局。
|
||
let globals = cx.new_dict()?;
|
||
// 返回 Invoke 把驱动接入同一 VM 帧循环,不在 NativeContext 内递归进入 VM。
|
||
let driver = cx.new_function("Presentable.present", code, globals)?;
|
||
attach_desktop_type(cx, driver, "Function", desktop)?;
|
||
return Ok(NativeResult::Invoke {
|
||
function: driver,
|
||
arguments: vec![],
|
||
});
|
||
}
|
||
}
|
||
// ui.open 把普通结果转换为根组件,再在必要时包一层 Page 并展示;ui.present 仅转换,不自动打开页面。
|
||
if matches!(self.operation, OpenValue) {
|
||
// 协议对象已在上面返回 Invoke;这里仅处理已有 Block 或普通值的文字回退。
|
||
let root = present_value(cx, &args[0], &self.session)?;
|
||
// Page 可直接保留,普通 Block 外包一层新 Page,以便加入窗口的页面注册表。
|
||
let page = if cx.store().payload::<block::Block>(root, "Block")?.page {
|
||
root
|
||
} else {
|
||
// 包装持有原 root,不复制业务对象,因此源对象/状态仍可继续被其他位置展示。
|
||
new_block(
|
||
cx,
|
||
Data::new(Kind::Page, vec![root], 0.0, 0.0),
|
||
&self.session,
|
||
)?
|
||
};
|
||
show(cx, page, &self.session)?;
|
||
return Ok(NativeResult::Return(Value::Object(page)));
|
||
}
|
||
if matches!(self.operation, Present) {
|
||
// 只做值到视觉的转换,不调用 show,不改变活动页或窗口归属。
|
||
return Ok(NativeResult::Return(Value::Object(present_value(
|
||
cx,
|
||
&args[0],
|
||
&self.session,
|
||
)?)));
|
||
}
|
||
// 从这里起都是需要接收者的 Block/Page 方法,未绑定 owner 时不能继续修改任意全局对象。
|
||
let owner = self.owner.ok_or(invalid("method has no owner"))?;
|
||
if matches!(self.operation, Bind) {
|
||
// 属性名与计算函数分开解码;connect 建立依赖和可取消 Subscription,不立即变成静态值。
|
||
let field = text(args.first())?;
|
||
// 函数对象的签名/效果由绑定模块检查,不能只因它是 Object 就当作合法计算闭包。
|
||
let compute = object(args.get(1))?;
|
||
return Ok(NativeResult::Return(Value::Object(
|
||
crate::binding::connect(
|
||
cx,
|
||
owner,
|
||
&field,
|
||
compute,
|
||
self.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.master,
|
||
)?,
|
||
)));
|
||
}
|
||
// moveTab 重排同窗口页面索引并强制下一帧更新标签;移动的是原 Page,非新页面副本。
|
||
if matches!(self.operation, MoveTab) {
|
||
// 页面位置是当前顺序下标,不是 Tab ID 或子组成 slot,先做平台 usize 表示检查。
|
||
let position = usize::try_from(integer(args.first())?)
|
||
.map_err(|_| invalid("tab position out of range"))?;
|
||
// 同锁内找所属窗口并重排,不能只用 current:源代码可持有其他窗口的 Page。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// 未登记的 Page 没有可重排标签,不能自动打开它再进行移动。
|
||
let id = page_window(cx.store(), &state, owner)?.ok_or(invalid("Page is not open"))?;
|
||
// 对真实所属窗口设置强制帧,确保即使内容根没变,标签顺序仍会发布。
|
||
let view = state
|
||
.presentations
|
||
.get_mut(&id)
|
||
.ok_or(invalid("missing window"))?;
|
||
// model/window 维护实际顺序,Presentation 只负责其派生投影和刷新标记。
|
||
let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?;
|
||
crate::window::move_page(cx, window, owner, position)?;
|
||
view.force_frame = true;
|
||
return Ok(NativeResult::Return(Value::Object(owner)));
|
||
}
|
||
// 接收者可能是共享转发别名,解析到最终 Data 后修改副本,最后用一次 replace_payload 写入结构边。
|
||
let (data_ref, data) = block::resolve(cx.store(), owner)?;
|
||
// 在副本上编辑结构,再经末尾 replace_payload 更新强边与版本,避免直接篡改借用 payload。
|
||
let mut data = data.clone();
|
||
// 默认返回 Unit/Nil,append/insert/订阅等分支会改成自己的业务结果;查询分支直接返回。
|
||
let mut result = Value::Nil;
|
||
match self.operation {
|
||
SetText => match &mut data.kind {
|
||
Kind::Text { value, .. } => {
|
||
// 新文字必须为真实字符串且受 UTF-8 字节预算约束,再替换 payload/普通属性。
|
||
let next = text(args.first())?;
|
||
if next.len() > 16_384 {
|
||
// 检查在写属性前,错误输入不会先清掉旧 text_binding 或部分改文字。
|
||
return Err(invalid("text exceeds 16 KiB"));
|
||
}
|
||
*value = next;
|
||
// 直接 setText 取代旧 textOf 推导;同时更新普通 text 属性,使快照属性读取与 payload 描述保持一致。
|
||
data.text_binding = None;
|
||
cx.set_attr(owner, "text", args[0].clone())?;
|
||
}
|
||
// receiver 的名义类型校验之外再验证 payload,避免对不兼容原生种类写文本。
|
||
_ => return Err(invalid("setText requires a text Block")),
|
||
},
|
||
SetColor => match &mut data.kind {
|
||
Kind::Text { color: value, .. } | Kind::Rect { color: value, .. } => {
|
||
// 颜色格式先校验,随后把普通 color 属性与本地 payload 副本保持一致。
|
||
*value = color(args.first(), "#242422")?;
|
||
cx.set_attr(owner, "color", Value::Text(value.clone()))?;
|
||
}
|
||
// 容器/空白不支持这一颜色修改接口,不能往无关 payload 中补出虚构颜色字段。
|
||
_ => return Err(invalid("this Block has no color")),
|
||
},
|
||
Append | Insert | Remove | Replace | Move => {
|
||
if !data.is_container() {
|
||
// 只允许有组成语义的容器;叶 Block 不因调用 append 而隐式变成 Column。
|
||
return Err(invalid("expected a container Block"));
|
||
}
|
||
if matches!(self.operation, Append | Insert) {
|
||
// 新增边需要新 slot;append 取末尾,insert 的首参是可插入缝位置。
|
||
let (position, argument) = if matches!(self.operation, Insert) {
|
||
// 平台索引转换和容器长度检查分开,len 本身允许表示“插到末尾”。
|
||
let position = usize::try_from(integer(args.first())?)
|
||
.map_err(|_| invalid("child position out of range"))?;
|
||
if position > data.children.len() {
|
||
return Err(invalid("child position out of range"));
|
||
}
|
||
(position, args.get(1))
|
||
} else {
|
||
// append 不接位置,唯一实参就是新孩子;不会误把它解析成索引。
|
||
(data.children.len(), args.first())
|
||
};
|
||
// 统一孩子对象解码;validate 再检查真实 Block 和无环组成。
|
||
let child = object(argument)?;
|
||
// 插入前禁止孩子图回到当前 Data,避免循环组成;position 是插入位置,返回值却是稳定 slot。
|
||
block::validate(cx.store(), child, Some(data_ref))?;
|
||
// 保存当前高水位作为返回身份,再推进 next_slot,删除旧边也不回退该计数。
|
||
let slot = data.next_slot;
|
||
// slot 单调增加且限制到 i64 正范围,不复用已删除身份;前后插入/重排不会把其他孩子 slot 改号。
|
||
data.next_slot = slot
|
||
.checked_add(1)
|
||
.filter(|next| *next <= i64::MAX as u64)
|
||
.ok_or(VmError::IdentityExhausted)?;
|
||
data.children.insert(position, Child { slot, block: child });
|
||
result = Value::Number((slot as i64).into());
|
||
} else {
|
||
// remove/replace/move 首参为稳定 slot 而非当前数组下标;只有 move 的第二参才是目标位置。
|
||
let slot = integer(args.first())?;
|
||
// 在当前顺序中查该稳定身份的位置,不能直接把 slot 转 usize 当数组下标。
|
||
let index = data
|
||
.children
|
||
.iter()
|
||
.position(|v| v.slot == slot)
|
||
.ok_or(invalid("child slot does not exist"))?;
|
||
match self.operation {
|
||
Remove => {
|
||
// 删除这一稳定边,不重新编号其余孩子;是否销毁 Block 由托管引用关系决定。
|
||
data.children.remove(index);
|
||
}
|
||
Replace => {
|
||
// 新孩子先过无环/类型校验,再替换旧边对象;边 slot 保持不变。
|
||
let child = object(args.get(1))?;
|
||
block::validate(cx.store(), child, Some(data_ref))?;
|
||
// replace 保留该组成边 slot,但换实际 Block;出现键还含 Block 身份,因此可能产生新的 Mount 上下文。
|
||
data.children[index].block = child;
|
||
}
|
||
Move => {
|
||
// 第二参是目标顺序下标,首参仍是稳定 slot;目标必须是已有序列内位置。
|
||
let to = usize::try_from(integer(args.get(1))?)
|
||
.map_err(|_| invalid("child position out of range"))?;
|
||
if to >= data.children.len() {
|
||
// move 不新增孩子,因此 len 是越界,不同于 insert 允许插到末尾的 len。
|
||
return Err(invalid("child position out of range"));
|
||
}
|
||
// 移动完整 Child 值,slot 与对象身份都保持,不分配新的出现组成键。
|
||
let child = data.children.remove(index);
|
||
data.children.insert(to, child);
|
||
}
|
||
// 外层已排除 append/insert,此处只可能是 remove/replace/move。
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
}
|
||
// children 查询返回 slot文本->Block 的托管字典,不把当前顺序假装成稳定 slot 连号数组。
|
||
Children => {
|
||
// 查询创建新字典但不改原容器;key 是 slot 文本,迭代顺序不作为下标身份保证。
|
||
let result = cx.new_dict()?;
|
||
for child in data.children {
|
||
// VM 字典赋值记录真实 Block 强边,调用结果可安全独立持有孩子而非引用 Rust 临时切片。
|
||
cx.dict_set(
|
||
result,
|
||
Value::Text(child.slot.to_string()),
|
||
Value::Object(child.block),
|
||
)?;
|
||
}
|
||
return Ok(NativeResult::Return(Value::Object(result)));
|
||
}
|
||
Render | Navigate => {
|
||
if !matches!(data.kind, Kind::Page) {
|
||
// 只有 Page 有根替换与导航历史,不能对普通 ContainerBlock 偷用页面语义。
|
||
return Err(invalid("expected a Page"));
|
||
}
|
||
// 新内容须真实 Block 且不能包含当前 Page 的共享 Data,防止出现循环组成。
|
||
let root = object(args.first())?;
|
||
block::validate(cx.store(), root, Some(data_ref))?;
|
||
if matches!(self.operation, Navigate) {
|
||
// 首次 navigate 把当前根纳入历史,之后新导航先截断前进分支,再追加新根,保持浏览历史语义。
|
||
if data.history.is_empty() {
|
||
if let Some(old) = data.children.first() {
|
||
// 首次导航才把现有根作为历史起点;空 Page 没有可保留的起始根。
|
||
data.history.push(old.block);
|
||
}
|
||
} else {
|
||
// 从后退位置发起新导航会丢弃原前进分支,不能保留两条互相矛盾的线性历史。
|
||
data.history.truncate(data.history_index + 1);
|
||
}
|
||
data.history.push(root);
|
||
data.history_index = data.history.len() - 1;
|
||
}
|
||
// 页面内容统一保留根 slot0,render/navigate 改的是根对象;show 只记录待展示请求,后续刷新再布局。
|
||
data.children = vec![Child {
|
||
slot: 0,
|
||
block: root,
|
||
}];
|
||
data.next_slot = data.next_slot.max(1);
|
||
show(cx, owner, &self.session)?;
|
||
}
|
||
Back | Forward => {
|
||
if data.history.is_empty() {
|
||
// 无历史是正常无动作,不把索引减法做在空列表上,也不制造一次空展示请求。
|
||
return Ok(NativeResult::Return(Value::Nil));
|
||
}
|
||
// 后退/前进钳到历史首尾,复用已保留根对象而非重新执行旧源码。
|
||
let next = if matches!(self.operation, Back) {
|
||
// saturating_sub 在首项停住,不绕回列表尾部。
|
||
data.history_index.saturating_sub(1)
|
||
} else {
|
||
// 前进上界为最后已有项,不为不存在的未来页分配对象。
|
||
(data.history_index + 1).min(data.history.len() - 1)
|
||
};
|
||
data.history_index = next;
|
||
data.children = vec![Child {
|
||
slot: 0,
|
||
block: data.history[next],
|
||
}];
|
||
show(cx, owner, &self.session)?;
|
||
}
|
||
// 分离先创建新 Window,再把同一 Page 展示到它;跨窗口出现上下文由后续展示同步重建。
|
||
Detach => {
|
||
// 短锁中只创建资源,锁释放后再 show_in,避免内部二次锁定同一个 Session。
|
||
let window = {
|
||
// 新窗口编号和 Presentation 同步建立,使用主注册域的 Window 默认类型。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// detach 创建默认 Window,不继承 Page 的源类型作为窗口类型。
|
||
let definition = desktop_type(cx.store(), "Window", state.master)?;
|
||
create_window(cx, &mut state, definition)?
|
||
};
|
||
show_in(cx, owner, &self.session, Some(window))?;
|
||
return Ok(NativeResult::Return(Value::Object(window)));
|
||
}
|
||
PageWindow => {
|
||
// 这是查询:按真实注册表找 owner 的归属,不能直接返回当前窗口冒充其宿主。
|
||
let state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
// 未打开/已关闭 Page 正常返回 nil,签名为 nullable(Window)。
|
||
let value = if let Some(id) = page_window(cx.store(), &state, owner)? {
|
||
cx.store()
|
||
.get_attr(state.presentations[&id].desktop, "window")?
|
||
} else {
|
||
// 仍存活的 Page 可以暂时没有窗口归属,不因此新建窗口或报错。
|
||
Value::Nil
|
||
};
|
||
return Ok(NativeResult::Return(value));
|
||
}
|
||
Open | Activate | MoveWindow => {
|
||
// moveToWindow 必须明确目标;open/activate 的缺省则保留原归属或使用当前窗口。
|
||
if matches!(self.operation, MoveWindow)
|
||
&& matches!(args.first(), None | Some(Value::Nil))
|
||
{
|
||
return Err(invalid("Page.moveToWindow requires a Window"));
|
||
}
|
||
// None 与显式 nil 在 open 路径等价;非空实参先解码对象,show_in 再检查 Window payload/域。
|
||
let target = match args.first() {
|
||
None | Some(Value::Nil) => None,
|
||
Some(value) => Some(object(Some(value))?),
|
||
};
|
||
show_in(cx, owner, &self.session, target)?;
|
||
return Ok(NativeResult::Return(Value::Object(owner)));
|
||
}
|
||
// unmount 清内容但不关闭 Page 标签,也不直接等同于销毁整个业务对象。
|
||
Unmount => {
|
||
// 内容强边在统一 replace_payload 时撤销,Page 自己仍在标签注册表中可再次 render。
|
||
data.children.clear();
|
||
}
|
||
Close => {
|
||
// 关闭 Page 当场取消页面和所属 Mount 任务,即使同一输入稍后又打开它,也不能复活旧异步工作。
|
||
crate::tasks::cancel_owner(cx, owner)?;
|
||
// 找出现位置并撤登记的过程只操作宿主表/任务,不在锁内执行用户生命周期钩子。
|
||
let mut state = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?;
|
||
if let Some(id) = page_window(cx.store(), &state, owner)? {
|
||
// 关闭即终止旧出现的任务归属,不等待下一次布局实际卸载才取消。
|
||
// 弱 Page 身份用于精确筛选这个页面的 Mount,不能取消整个窗口其他页面任务。
|
||
let page = cx.store().downgrade(owner)?;
|
||
for weak in state.presentations[&id].mounts.ordered() {
|
||
// 升级成功且 Mount.page 匹配才取消;不会把共享 Block 的其他出现一并取消。
|
||
if let Some(mount) = cx.upgrade(weak)?
|
||
&& cx
|
||
.store()
|
||
.payload::<crate::mount::Mount>(mount, "Mount")?
|
||
.page
|
||
== Some(page)
|
||
{
|
||
// 取消 owner/key 调度资格,旧 I/O 即使稍后完成也不能再向此出现交付。
|
||
crate::tasks::cancel_owner(cx, mount)?;
|
||
}
|
||
}
|
||
// 撤注册后为原窗口选择替代页或安排清空,生命周期卸载仍交后续刷新处理。
|
||
remove_page(
|
||
cx,
|
||
state
|
||
.presentations
|
||
.get_mut(&id)
|
||
.ok_or(invalid("missing window"))?,
|
||
owner,
|
||
)?;
|
||
}
|
||
return Ok(NativeResult::Return(Value::Nil));
|
||
}
|
||
OnClick => {
|
||
// 简便 onClick 保存单个处理器,validate_handler 检查真实 callable 契约而非仅有对象身份。
|
||
let handler = object(args.first())?;
|
||
crate::event_dispatch::validate_handler(cx.store(), handler)?;
|
||
data.click = Some(handler);
|
||
result = Value::Object(owner);
|
||
}
|
||
On => {
|
||
// 字符串事件名用于传播匹配,按 UTF-8 字节限制长度,不允许空事件名。
|
||
let kind = text(args.first())?;
|
||
if kind.is_empty() || kind.len() > 128 {
|
||
return Err(invalid("invalid event kind"));
|
||
}
|
||
// 先校验回调再建立 Connection,避免非法处理器留下不可取消的半条订阅。
|
||
let handler = object(args.get(1))?;
|
||
crate::event_dispatch::validate_handler(cx.store(), handler)?;
|
||
// 订阅默认非捕获;可用 Bool 或 capture/bubble 文本明确阶段,其他值拒绝而不做真值转换。
|
||
let capture = match args.get(2) {
|
||
None | Some(Value::Bool(false)) => false,
|
||
Some(Value::Bool(true)) => true,
|
||
Some(Value::Text(phase)) if phase == "capture" => true,
|
||
Some(Value::Text(phase)) if phase == "bubble" => false,
|
||
_ => return Err(invalid("phase must be capture or bubble")),
|
||
};
|
||
// 清掉已断开连接后再计算容量,保留仍有效订阅顺序;新 Connection 拥有 handler,返回 token 仅弱引用连接。
|
||
let mut active = Vec::new();
|
||
for token in &data.subscriptions {
|
||
// data.subscriptions 实际保存 Connection 身份,已断开者 handler=None 不再占容量。
|
||
if cx
|
||
.store()
|
||
.payload::<crate::event_dispatch::Connection>(*token, "Connection")?
|
||
.handler
|
||
.is_some()
|
||
{
|
||
// 按原序保留仍有效连接,事件路由中的监听顺序因此不因清理而改变。
|
||
active.push(*token);
|
||
}
|
||
}
|
||
if active.len() >= MAX_UI_NODES {
|
||
// 清理后仍满才拒绝,避免死亡连接永久占满一个 Block 的订阅预算。
|
||
return Err(invalid("too many event subscriptions"));
|
||
}
|
||
// Connection 强持 handler,由 Block 的 subscriptions 拥有;任务和事件生命周期可据此断开。
|
||
let connection = cx.allocate(crate::event_dispatch::Connection {
|
||
kind,
|
||
handler: Some(handler),
|
||
capture,
|
||
})?;
|
||
// 返回的 Subscription 仅弱指连接,不应因用户保留令牌而让整个已卸载组件和回调常驻。
|
||
let token = cx.allocate(crate::event_dispatch::Subscription {
|
||
connection: cx.store().downgrade(connection)?,
|
||
})?;
|
||
attach_named_type(cx, token, "Subscription", &self.session)?;
|
||
active.push(connection);
|
||
data.subscriptions = active;
|
||
result = Value::Object(token);
|
||
}
|
||
// 基类事件/挂载回调默认空实现,子类可覆盖;不在这条默认路径生成额外状态。
|
||
OnEvent | OnMount | OnUnmount => return Ok(NativeResult::Return(Value::Nil)),
|
||
Click | Dispatch => {
|
||
// 事件/订阅类型来自当前展示域,传播上下文仍由明确 owner/可选 Mount 指定。
|
||
let desktop = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.desktop;
|
||
// click 新建种类为 click 的 Event,dispatch 使用调用者提供事件;两者都将可选具体 Mount 交传播模块验证。
|
||
let (event, mount) = if matches!(self.operation, Click) {
|
||
// click 总创建一份新 Event,调用方只可补具体 Mount;不会把旧事件对象重复分发。
|
||
let definition = desktop_type(cx.store(), "Event", desktop)?;
|
||
(
|
||
crate::event_dispatch::new_event(cx, definition, "click")?,
|
||
args.first(),
|
||
)
|
||
} else {
|
||
// dispatch 接现成事件,第二实参才是可选出现;传播模块继续验证两者语义。
|
||
(object(args.first())?, args.get(1))
|
||
};
|
||
// None 保留“未指定出现”含义,显式非法值传播错误;不从任意当前 Mount 猜目标。
|
||
let mount = mount.map(|value| object(Some(value))).transpose()?;
|
||
// 返回 Invoke/Return 由事件模块决定,源捕获/目标/冒泡回调仍走同一 VM 帧循环。
|
||
return crate::event_dispatch::dispatch(cx, owner, event, mount, desktop);
|
||
}
|
||
// 其余操作应在前面的专用分支处理;漏分派必须显式报错,不能悄悄返回成功 Unit。
|
||
_ => return Err(invalid("unsupported Block operation")),
|
||
}
|
||
// 非提前返回的结构编辑/订阅操作在末尾统一经过写屏障,触发拥有边更新和变更日志。
|
||
cx.replace_payload(data_ref, data)?;
|
||
Ok(NativeResult::Return(result))
|
||
}
|
||
}
|