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

1093 lines
62 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 把 Endor 方法调用分派到对应宿主操作。这里处理构造、树修改、绑定、状态与页面能力。
//! 先检查参数和只读阶段限制,再修改托管状态;对象引用的生命周期仍交给 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))
}
}