docs: explain UI bridge callers, control flow and VM ownership
This commit is contained in:
+427
-74
File diff suppressed because it is too large
Load Diff
@@ -1,42 +1,63 @@
|
||||
//! 安装一次 VM 域需要的宿主类型、对象和方法。类型注册表是源代码调用原生能力的入口。
|
||||
//! 先建立类型与桌面基础设施,再绑定构造器、视觉协议和开发工具工厂;返回的根负责保持这些对象存活。
|
||||
//!
|
||||
//! Allocate and connect the managed desktop, type tables and host globals.
|
||||
//! 调用链:`endor_host::compile` / `ReplSession::new` 创建候选 VM,
|
||||
//! 随后调用本模块的 `install`;运行时再通过 install_globals 注入源程序使用的 UI 值。
|
||||
//! 本文件只建立宿主环境。源程序稍后调用构造器或实例方法时,才会进入 `calls::Function::invoke`。
|
||||
//! 安装返回的 UiBridge 由运行时保存;传入的 roots 由候选运行时持有,并在失败或退役时统一释放。
|
||||
use super::*;
|
||||
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
/// 为一次候选程序/REPL 会话安装 Tessera 宿主环境,供运行时后续注入全局值和执行源程序。
|
||||
///
|
||||
/// 类型要先创建,签名才能引用它们;因此先分配没有签名的函数,再原地补齐 payload。
|
||||
/// 所有 `?` 都把安装失败交还候选运行时,不会在这里把半成品替换成活跃程序。
|
||||
/// `roots` 保存明确的 VM 拥有根;Rust 的 ObjectRef 数值和 Arc<Mutex<Session>> 均不能替代它们。
|
||||
pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
// 先建立 desktop/ui/tabs 并加入显式 roots,后面初始化失败时由上层运行时统一释放,避免依赖 Rust 句柄保活。
|
||||
// desktop 是此桥接域的类型注册表和窗口资源入口;它不是操作系统桌面或原生窗口。
|
||||
let desktop = vm.new_object()?;
|
||||
roots.push(desktop);
|
||||
// 安装时尚无窗口尺寸;运行时收到 viewport 后再填充,避免在 VM 引导期间依赖 GUI 状态。
|
||||
vm.set_attr(desktop, "viewport", Value::Nil)?;
|
||||
// tabs 是主窗口的托管页面注册表,Presentation 和 window::install 会使用同一字典身份。
|
||||
let tabs = vm.new_dict()?;
|
||||
roots.push(tabs);
|
||||
vm.set_attr(desktop, "tabs", Value::Object(tabs))?;
|
||||
// ui 是对源代码公开的命名空间;desktop 保存内部记账,二者用途不能混同。
|
||||
let ui = vm.new_object()?;
|
||||
roots.push(ui);
|
||||
// 原生 Session 借用托管身份,主窗口先拥有独立 Presentation;业务对象仍全部位于传入的单一 VM。
|
||||
let session = Arc::new(Mutex::new(Session {
|
||||
// 节点弱索引用来检查数量与回收失效记录,不应让已关闭的业务对象继续存活。
|
||||
nodes: Vec::new(),
|
||||
// 先登记主窗口上下文;后续窗口通过 create_window 建立各自的 Presentation。
|
||||
presentations: std::collections::BTreeMap::from([(
|
||||
crate::window::MAIN_WINDOW,
|
||||
crate::presentation::Presentation::new(desktop, tabs),
|
||||
)]),
|
||||
// Session 的 Deref 按 current 选择布局/挂载状态;初始所有操作都落在主窗口。
|
||||
current: crate::window::MAIN_WINDOW,
|
||||
// master 始终指向主 desktop,副窗口仍借用同一 VM 的类型和资源域。
|
||||
master: desktop,
|
||||
// 分配器从现有最高窗口 ID 继续增长;Mount 则尚无任何已分配出现位置。
|
||||
next_window: crate::window::MAIN_WINDOW,
|
||||
next_mount: 0,
|
||||
// 关闭后尚未交给 GUI 的原生命令暂存在这里,不能随 Presentation 删除而丢失。
|
||||
retired_commands: Vec::new(),
|
||||
// 累计诊断计数由刷新流程更新,供 ui.stats/开发工具观察本会话的实际工作量。
|
||||
refresh_yields: 0,
|
||||
refresh_restarts: 0,
|
||||
layout_cancels: 0,
|
||||
paint_cancels: 0,
|
||||
paint_slices: 0,
|
||||
binding_evaluations: 0,
|
||||
// 清理阶段会置位,create_window 等操作据此拒绝在退出期间重新创建资源。
|
||||
exiting: false,
|
||||
}));
|
||||
// 先分配函数占位再补签名,因为签名会引用尚未建立的名义类型;保存列表用于第二阶段替换 payload。
|
||||
// 保存 ui.name 的函数身份及 Operation,等全部类型注册后再为这些函数补齐精确签名。
|
||||
let mut public_functions = Vec::new();
|
||||
// 另外记录 TypeConstructor 所用的工厂;它们多接收一个 Type 首参,不能套用自由函数签名。
|
||||
let mut factories = Vec::new();
|
||||
for (name, operation) in [
|
||||
("implement", Operation::Implement),
|
||||
@@ -63,6 +84,8 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
("viewport", Operation::Viewport),
|
||||
("window", Operation::Window),
|
||||
] {
|
||||
// 每个名字得到自己的 VM callable,实际操作由 Function 根据 operation 分派。
|
||||
// desktop 放在 captures 中,确保函数仍可达时它所依赖的类型注册表也能被 GC 追踪。
|
||||
let function = vm.allocate(ContextNativeFunction::new(
|
||||
name,
|
||||
Function {
|
||||
@@ -75,6 +98,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
vec![desktop],
|
||||
))?;
|
||||
roots.push(function);
|
||||
// 属性供源程序的 ui.text/ui.present 等查询;列表供本函数第二阶段补签名,职责不同。
|
||||
vm.set_attr(ui, name, Value::Object(function))?;
|
||||
public_functions.push((name, function, operation));
|
||||
}
|
||||
@@ -83,6 +107,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
roots.extend([root_types.object, root_types.type_type]);
|
||||
vm.set_attr(ui, "Object", Value::Object(root_types.object))?;
|
||||
vm.set_attr(ui, "Type", Value::Object(root_types.type_type))?;
|
||||
// definitions 汇集全部类型,稍后写入 desktop.types,供 desktop_type 按名称查精确身份。
|
||||
let mut definitions = vec![
|
||||
("Object", root_types.object),
|
||||
("Type", root_types.type_type),
|
||||
@@ -127,10 +152,13 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
("Task", "Object", None),
|
||||
("ObjectList", "Object", Some("object-list")),
|
||||
] {
|
||||
// 依照表中基类名取得当前 VM 的真实类型,不能用另一个 VM 中的同名 Type 代替。
|
||||
let base = object(Some(&vm.get_attr(ui, base_name)?))?;
|
||||
// 根据构造规则选择专用工厂、通用工厂或抽象类型;这里只注册,不分配业务实例。
|
||||
let constructor = match constructor_name {
|
||||
// ObjectList 与 Event 有特殊的“类型首参+子类字段”构造规则,单独原生适配;其他类型共用 NativeFactory。
|
||||
Some("object-list") => {
|
||||
// ObjectList 构造要保持托管元素身份与字段规则,由 object_list::Constructor 处理。
|
||||
let factory = vm.allocate(ContextNativeFunction::new(
|
||||
"object-list",
|
||||
crate::object_list::Constructor {
|
||||
@@ -140,17 +168,20 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
vec![desktop],
|
||||
))?;
|
||||
roots.push(factory);
|
||||
// 工厂本身也是语言中的 Function 实例,挂类型后才能通过名义函数检查。
|
||||
let function_type = object(Some(&vm.get_attr(ui, "Function")?))?;
|
||||
vm.attach_type(factory, function_type)?;
|
||||
TypeConstructor::NativeTyped(factory)
|
||||
}
|
||||
Some("event") => {
|
||||
// Event 另有必填事件名;其专用 Constructor 在真正调用时解析事件参数和子类字段。
|
||||
let factory = vm.allocate(ContextNativeFunction::new(
|
||||
"event",
|
||||
crate::event_dispatch::Constructor { signature: None },
|
||||
vec![],
|
||||
))?;
|
||||
roots.push(factory);
|
||||
// 与列表工厂保持相同函数类型体系;无 desktop 捕获的分配器不额外引入桌面强边。
|
||||
let function_type = object(Some(&vm.get_attr(ui, "Function")?))?;
|
||||
vm.attach_type(factory, function_type)?;
|
||||
TypeConstructor::NativeTyped(factory)
|
||||
@@ -176,6 +207,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
.map(Operation::SceneObject)
|
||||
.ok_or(invalid("unknown native type allocator"))?,
|
||||
};
|
||||
// 通用 NativeFactory 在调用时区分原生参数与源子类字段,并保留 requested Type。
|
||||
let factory = vm.allocate(ContextNativeFunction::new(
|
||||
constructor_name,
|
||||
NativeFactory {
|
||||
@@ -187,10 +219,12 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
vec![desktop],
|
||||
))?;
|
||||
roots.push(factory);
|
||||
// 这个函数属于构造器元数据,不直接放在 ui.name;但仍需标为 Function 实例。
|
||||
let function_type = object(Some(&vm.get_attr(ui, "Function")?))?;
|
||||
vm.attach_type(factory, function_type)?;
|
||||
TypeConstructor::NativeTyped(factory)
|
||||
}
|
||||
// 没有公开分配规则的类型仍可用于类型检查/协议;直接把它当构造器调用会由 VM 拒绝。
|
||||
None => TypeConstructor::Abstract,
|
||||
};
|
||||
// 记住实际工厂身份,类型构建完成后原地补签名,避免替换类型内指向的构造函数对象。
|
||||
@@ -198,6 +232,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
{
|
||||
factories.push((tag, factory));
|
||||
}
|
||||
// 生成新 Type 后同时公开到 ui,并加入待提交注册表,两个入口必须指向同一对象。
|
||||
let definition = vm.new_type(name, base, constructor)?;
|
||||
roots.push(definition);
|
||||
vm.set_attr(ui, name, Value::Object(definition))?;
|
||||
@@ -217,6 +252,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
("PaintCommand", vec![]),
|
||||
("CameraGesture", vec![]),
|
||||
] {
|
||||
// 表里的字段名转成 VM 可检查的字段声明;map 的每次调用只描述一个字段,不执行布局。
|
||||
let fields = fields
|
||||
.into_iter()
|
||||
.map(|name| endor_core::ClassField {
|
||||
@@ -225,11 +261,13 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
type_name: if matches!(name, "children" | "commands") {
|
||||
"Object"
|
||||
} else {
|
||||
// 余下字段都是测量/几何数值,让 VM 在实例化时先拒绝非数字实参。
|
||||
"Number"
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
.collect();
|
||||
// 这些协议值只携带源字段,使用普通 class 分配;不需要 Block 的原生 payload。
|
||||
let definition = vm.new_class(
|
||||
name,
|
||||
root_types.object,
|
||||
@@ -243,14 +281,17 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
// The desktop owns the registry through ordinary traced attributes. The
|
||||
// Rust session only borrows handles and adds no hidden lifetime roots.
|
||||
// 桌面通过普通属性拥有类型字典;后续 nominal 查询必须使用同一 desktop 中注册的精确类型身份。
|
||||
// 类型字典由 desktop 属性托管持有;后续桥接调用可从其查找 Block、Event 等精确类型。
|
||||
let types = vm.new_dict()?;
|
||||
roots.push(types);
|
||||
for (name, definition) in definitions {
|
||||
// 字符串键是宿主查询约定,值才是参与 is_instance/is_subtype 的真实名义身份。
|
||||
vm.dict_set(types, Value::Text(name.into()), Value::Object(definition))?;
|
||||
}
|
||||
vm.set_attr(desktop, "types", Value::Object(types))?;
|
||||
vm.attach_type(desktop, root_types.object)?;
|
||||
vm.attach_type(ui, root_types.object)?;
|
||||
// 安装后续编译函数的默认类型,也用于下方所有方法 wrapper 的 attach_type。
|
||||
let function_type = object(Some(&vm.get_attr(ui, "Function")?))?;
|
||||
// 让后续 VM 创建的函数使用宿主 Function 名义类型,并打开源类型检查,再安装完整本机签名。
|
||||
vm.set_function_type(function_type)?;
|
||||
@@ -273,6 +314,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
)?;
|
||||
}
|
||||
for (name, function) in factories {
|
||||
// 恢复第一阶段记录的分配器类别;Event/ObjectList 不走 NativeFactory,需要单独补外壳。
|
||||
let operation = match name {
|
||||
"text" => Some(Operation::Text),
|
||||
"rect" => Some(Operation::Rect),
|
||||
@@ -288,6 +330,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
"button" => Some(Operation::Button),
|
||||
"page" => Some(Operation::Page),
|
||||
"window-create" => Some(Operation::NewWindow),
|
||||
// None 在本轮只表示专用构造器,不表示前一阶段的 Abstract 类型。
|
||||
"event" | "object-list" => None,
|
||||
name => Some(
|
||||
crate::scene3d_model::Constructor::from_function(name)
|
||||
@@ -296,6 +339,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
),
|
||||
};
|
||||
if let Some(operation) = operation {
|
||||
// options 只描述原生构造选项,调用时由 NativeFactory 在切掉 Type 和子类字段后校验。
|
||||
let options = operation_signature(vm.object_store(), desktop, operation)?;
|
||||
// 外层 NativeTyped 工厂接收 Type 首参和任意候选尾参,允许子类字段;NativeFactory 用 options 再核对基构造参数。
|
||||
let envelope = CallSignature {
|
||||
@@ -305,6 +349,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
returns: options.returns.clone(),
|
||||
effect: options.effect,
|
||||
};
|
||||
// 初始强边是注册表;随后追加签名引用的类型,供 ContextNativeFunction::trace 使用。
|
||||
let mut captures = vec![desktop];
|
||||
// 参数和返回注解中的名义类型也是托管引用,必须纳入 captures,不能只把它们存进 Rust 签名结构。
|
||||
captures.extend(options.references());
|
||||
@@ -323,16 +368,20 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
)?;
|
||||
} else {
|
||||
// 特殊构造器保留宽变参外壳,再由对应 Constructor 按类型字段数拆实参,返回准确 Event/ObjectList 名义类型。
|
||||
// 为返回值选取专用名义类型,避免宽变参外壳同时放宽了返回值检查。
|
||||
let kind = if name == "event" {
|
||||
"Event"
|
||||
} else {
|
||||
"ObjectList"
|
||||
};
|
||||
// Event 的 Type 之后还有必填字符串事件名;ObjectList 只有 Type 为必填,其余由实现解释。
|
||||
let signature = CallSignature {
|
||||
arity: endor_core::CallArity::range(
|
||||
// 这里包含 Type 首参;源子类字段由构造器验证,所以最大个数保留开放。
|
||||
if name == "event" { 2 } else { 1 },
|
||||
usize::MAX,
|
||||
),
|
||||
// 固定前缀与 required 数量保持一致,额外实参由 variadic 和字段验证共同约束。
|
||||
parameters: if name == "event" {
|
||||
vec![AnnotationType::Type.into(), AnnotationType::String.into()]
|
||||
} else {
|
||||
@@ -344,6 +393,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
effect: CallEffect::ReadOnly,
|
||||
};
|
||||
if name == "event" {
|
||||
// 原地更新 Event 工厂,TypeConstructor 中已保存的对象引用继续有效。
|
||||
vm.replace_payload(
|
||||
function,
|
||||
ContextNativeFunction::new(
|
||||
@@ -355,6 +405,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
),
|
||||
)?;
|
||||
} else {
|
||||
// 这里只有 ObjectList;保留 desktop 捕获,以便专用分配器给结果附加列表类型。
|
||||
vm.replace_payload(
|
||||
function,
|
||||
ContextNativeFunction::new(
|
||||
@@ -369,6 +420,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 对这批公开入口补 Function 类型身份;不重新分配 callable,ui 属性仍指向原对象。
|
||||
for name in [
|
||||
"open",
|
||||
"text",
|
||||
@@ -390,6 +442,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
"implements",
|
||||
"supports",
|
||||
] {
|
||||
// 从 ui 取回前面已补完签名的函数;名称表与安装表不一致会在这里报错而非静默漏装。
|
||||
let function = object(Some(&vm.get_attr(ui, name)?))?;
|
||||
vm.attach_type(function, function_type)?;
|
||||
}
|
||||
@@ -437,7 +490,9 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
("Page", "back", Operation::Back),
|
||||
("Page", "forward", Operation::Forward),
|
||||
] {
|
||||
// expected 同时用于方法所属类型、receiver 校验和签名中的名义参数身份。
|
||||
let expected = object(Some(&vm.get_attr(ui, type_name)?))?;
|
||||
// VM 的方法分派负责绑定 receiver;TypeMethod 再验证它并转交 Function 执行业务操作。
|
||||
let method = vm.allocate(ContextNativeFunction::new(
|
||||
name,
|
||||
TypeMethod {
|
||||
@@ -451,20 +506,26 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
))?;
|
||||
roots.push(method);
|
||||
vm.attach_type(method, function_type)?;
|
||||
// 写到类型方法表而非每个实例的属性,实例与源子类可共享同一个原生实现。
|
||||
vm.define_method(expected, name, method)?;
|
||||
}
|
||||
// 类型与基础方法就绪后再装窗口、默认视觉、事件方法和能力契约,顺序满足各模块查类型/方法的前提。
|
||||
crate::window::install(vm, roots, desktop, tabs)?;
|
||||
// 默认 measure/arrange/paint 安装到 Block 体系;源代码之后可按协议覆盖它们。
|
||||
let block_type = object(Some(&vm.get_attr(ui, "Block")?))?;
|
||||
crate::native_visual::install(vm, roots, desktop, block_type, function_type)?;
|
||||
// 事件方法需要识别 Event 实例;订阅返回值则使用 Subscription 身份管理取消连接。
|
||||
let event_type = object(Some(&vm.get_attr(ui, "Event")?))?;
|
||||
// token_type 传给事件安装器,不能拿 Event 类型代替,否则取消令牌会违反返回契约。
|
||||
let token_type = object(Some(&vm.get_attr(ui, "Subscription")?))?;
|
||||
crate::event_dispatch::install(vm, roots, event_type, token_type, function_type)?;
|
||||
crate::protocols::install(vm, roots, desktop, ui)?;
|
||||
// 每个桥接域配套自己的任务 Scheduler,再接工具入口及托管起始页工厂,最后返回可使用的 UiBridge。
|
||||
// Scheduler 随 UiBridge 交还运行时;它还未开始执行用户任务,只完成 ui 异步 API 注册。
|
||||
let tasks = crate::tasks::install(vm, roots, ui, desktop)?;
|
||||
crate::bridge::graphics::install(vm, roots, ui, desktop)?;
|
||||
developer_pages::install(vm, ui, &session)?;
|
||||
// 将语言命名空间、展示会话与任务调度打包,运行时随后通过同一 bridge 推进刷新与回调。
|
||||
let bridge = UiBridge { ui, session, tasks };
|
||||
developer_pages::install_start_page(vm, &bridge)?;
|
||||
Ok(bridge)
|
||||
|
||||
@@ -1,46 +1,57 @@
|
||||
//! 把 Endor 方法调用分派到对应宿主操作。这里处理构造、树修改、绑定、状态与页面能力。
|
||||
//! 先检查参数和只读阶段限制,再修改托管状态;对象引用的生命周期仍交给 NativeContext 和 Aster 管理。
|
||||
//!
|
||||
//! Native Block operations executed on the owning VM worker.
|
||||
//! 调用来源:VM 直接执行 bootstrap 注册的 ui 自由函数,或由 TypeMethod/NativeFactory 剥离接收者/Type 后转交。
|
||||
//! Function.owner 保存绑定对象,requested_type 保存原生子类构造请求;本文件 args 只包含显式原生参数。
|
||||
//! 简单分支同步返回 NativeResult::Return;需要执行源协议/事件处理器的分支返回 Invoke,让 VM 接着推进调用栈。
|
||||
//! NativeContext 追踪本次分配和 upgrade 的临时根,返回/委托时提升输出引用,其余在域退出时释放。
|
||||
use super::*;
|
||||
|
||||
impl NativeCallable for Function {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
/// VM 在调用边界查询 bootstrap/bind 写入的签名;由外层已校验的临时转交 Function 可返回 None。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
self.signature.clone()
|
||||
}
|
||||
/// 接收已经剥离 receiver 的显式参数,数量沿用 Operation 的范围。
|
||||
/// VM 查询自由函数的实参数量,或供转交适配器使用;这里不再给 receiver/Type 首参加一。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(self.operation.arity())
|
||||
}
|
||||
|
||||
/// 统一校验数量与只读限制,再按 Operation 分派构造、树修改、页面、绑定、状态与能力调用,返回托管结果或 VM 续调用。
|
||||
/// 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
|
||||
@@ -51,6 +62,7 @@ impl NativeCallable for Function {
|
||||
return Ok(NativeResult::Return(Value::Nil));
|
||||
}
|
||||
if matches!(self.operation, Translate3D | Rotate3D) {
|
||||
// 三分量实参共用转换入口,末尾布尔值区分旋转/位移;对象缺少 owner 时直接报错。
|
||||
crate::scene3d_model::transform(
|
||||
cx,
|
||||
self.owner
|
||||
@@ -62,9 +74,12 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 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
|
||||
}
|
||||
@@ -73,24 +88,30 @@ impl NativeCallable for Function {
|
||||
.implements_trait(object(args.first())?, capability)?,
|
||||
),
|
||||
Supports => {
|
||||
// Validate the contract even for immediate scalar values.
|
||||
// 即使第一参数是标量也先验证第二参数真为 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)));
|
||||
@@ -98,48 +119,61 @@ impl NativeCallable for Function {
|
||||
// 显式失效属于模型变更,只读阶段拒绝;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
|
||||
@@ -154,6 +188,7 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 跨窗口 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(),
|
||||
@@ -162,25 +197,31 @@ impl NativeCallable for Function {
|
||||
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()
|
||||
@@ -195,6 +236,7 @@ impl NativeCallable for Function {
|
||||
return Ok(NativeResult::Return(Value::Nil));
|
||||
}
|
||||
if matches!(self.operation, Window) {
|
||||
// ui.window() 查询当前选择窗口的资源对象;和 Window() 分配新窗口的路径不同。
|
||||
let desktop = self
|
||||
.session
|
||||
.lock()
|
||||
@@ -205,15 +247,19 @@ impl NativeCallable for Function {
|
||||
));
|
||||
}
|
||||
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(
|
||||
@@ -227,15 +273,19 @@ impl NativeCallable for Function {
|
||||
// 诊断会读对象总数与宿主变化统计,禁止在只读渲染中观察并形成反馈依赖。
|
||||
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)? {
|
||||
@@ -245,8 +295,11 @@ impl NativeCallable for Function {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 统计对象总数在分配 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),
|
||||
@@ -273,8 +326,10 @@ impl NativeCallable for Function {
|
||||
session.dependency_invalidations as i64,
|
||||
),
|
||||
] {
|
||||
// 统一转成语言整数,字段名字是 ui.stats 的观察接口,不影响调度逻辑。
|
||||
cx.set_attr(stats, name, Value::Number(value.into()))?;
|
||||
}
|
||||
// 单独保留文本失效原因,数值计数不能解释究竟哪条源属性触发了重算。
|
||||
cx.set_attr(
|
||||
stats,
|
||||
"lastInvalidDependency",
|
||||
@@ -297,6 +352,7 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 状态是普通名义 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())?;
|
||||
@@ -305,10 +361,13 @@ impl NativeCallable for Function {
|
||||
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"));
|
||||
};
|
||||
@@ -319,22 +378,28 @@ impl NativeCallable for Function {
|
||||
.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(),
|
||||
@@ -355,6 +420,7 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 普通组件先组装 Data,再统一由 new_typed_block 安装 Cell、属性和名义类型;不直接写原生窗口。
|
||||
let constructor = match self.operation {
|
||||
// 滚动视口有限宽高和唯一内容 Block;滚动轴/偏移由属性和 Mount 后续处理。
|
||||
Scroll => Some(Data::new(
|
||||
Kind::Scroll {
|
||||
width: dimension(args.first())?,
|
||||
@@ -364,6 +430,7 @@ impl NativeCallable for Function {
|
||||
0.0,
|
||||
0.0,
|
||||
)),
|
||||
// 文本是叶节点,size 的 u64→u32 转换和下面统一 1..=8 检查分别限制表示与语义。
|
||||
Text => Some(Data::new(
|
||||
Kind::Text {
|
||||
value: text(args.first())?,
|
||||
@@ -380,6 +447,7 @@ impl NativeCallable for Function {
|
||||
0.0,
|
||||
0.0,
|
||||
)),
|
||||
// 宽高必填,圆角缺省零,颜色缺省浅蓝;每个尺寸都先经有限非负检查。
|
||||
Rect => Some(Data::new(
|
||||
Kind::Rect {
|
||||
width: dimension(args.first())?,
|
||||
@@ -395,18 +463,23 @@ impl NativeCallable for Function {
|
||||
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 分配前统一检查。
|
||||
@@ -417,7 +490,9 @@ impl NativeCallable for Function {
|
||||
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()
|
||||
@@ -427,6 +502,7 @@ impl NativeCallable for Function {
|
||||
0.0,
|
||||
)),
|
||||
CanvasViewport => {
|
||||
// 视口自身参加有限布局,第三实参保存 Scene2D 引用,不把场景孩子展开成行列内容。
|
||||
let mut data = Data::new(
|
||||
Kind::CanvasViewport {
|
||||
width: dimension(args.first())?,
|
||||
@@ -441,12 +517,15 @@ impl NativeCallable for Function {
|
||||
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,
|
||||
@@ -454,6 +533,7 @@ impl NativeCallable for Function {
|
||||
crate::scene3d_model::Constructor::Perspective,
|
||||
&[],
|
||||
)?;
|
||||
// 普通 children 先为空,世界场景独立存于 scene3d 字段;面板 UI 会在提取时派生。
|
||||
let mut data = Data::new(
|
||||
Kind::Viewport3D {
|
||||
width: dimension(args.first())?,
|
||||
@@ -465,6 +545,7 @@ impl NativeCallable for Function {
|
||||
);
|
||||
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(
|
||||
@@ -478,6 +559,7 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 按钮标签也是普通 TextBlock 孩子,布局和生命周期沿同一组成树,不是原生按钮硬编码文字。
|
||||
Button => {
|
||||
// 先创建真实文字孩子,让按钮尺寸和文本测量、字体缓存、子树生命周期保持一致。
|
||||
let label = new_block(
|
||||
cx,
|
||||
Data::new(
|
||||
@@ -494,6 +576,7 @@ impl NativeCallable for Function {
|
||||
)?;
|
||||
Some(Data::new(Kind::Button, vec![label], 10.0, 0.0))
|
||||
}
|
||||
// 非构造操作继续进入页面/实例方法分派;None 不是失败,也不创建占位对象。
|
||||
_ => None,
|
||||
};
|
||||
// 统一检查文本等级和 UTF-8 字节量,再按 requested_type 生成实际对象,保留子类工厂身份。
|
||||
@@ -501,6 +584,7 @@ impl NativeCallable for Function {
|
||||
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(
|
||||
@@ -513,7 +597,9 @@ impl NativeCallable for Function {
|
||||
if matches!(self.operation, OpenValue | Present)
|
||||
&& let Value::Object(value) = args[0]
|
||||
{
|
||||
// 对象值可能实现 Presentable;标量不可能走这一协议路径,稍后按普通文字转换处理。
|
||||
let (desktop, current) = {
|
||||
// master 用于共享能力类型,current 用于传给源 present 的窗口资源,二者不能混用。
|
||||
let state = self
|
||||
.session
|
||||
.lock()
|
||||
@@ -530,15 +616,20 @@ impl NativeCallable for Function {
|
||||
.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 {
|
||||
@@ -559,6 +650,7 @@ impl NativeCallable for Function {
|
||||
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![
|
||||
@@ -570,8 +662,11 @@ impl NativeCallable for Function {
|
||||
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 {
|
||||
@@ -582,10 +677,13 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 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),
|
||||
@@ -596,6 +694,7 @@ impl NativeCallable for Function {
|
||||
return Ok(NativeResult::Return(Value::Object(page)));
|
||||
}
|
||||
if matches!(self.operation, Present) {
|
||||
// 只做值到视觉的转换,不调用 show,不改变活动页或窗口归属。
|
||||
return Ok(NativeResult::Return(Value::Object(present_value(
|
||||
cx,
|
||||
&args[0],
|
||||
@@ -605,7 +704,9 @@ impl NativeCallable for Function {
|
||||
// 从这里起都是需要接收者的 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(
|
||||
@@ -622,17 +723,22 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 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;
|
||||
@@ -640,13 +746,17 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 接收者可能是共享转发别名,解析到最终 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;
|
||||
@@ -654,21 +764,27 @@ impl NativeCallable for Function {
|
||||
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() {
|
||||
@@ -676,11 +792,14 @@ impl NativeCallable for Function {
|
||||
}
|
||||
(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
|
||||
@@ -692,6 +811,7 @@ impl NativeCallable for Function {
|
||||
} else {
|
||||
// remove/replace/move 首参为稳定 slot 而非当前数组下标;只有 move 的第二参才是目标位置。
|
||||
let slot = integer(args.first())?;
|
||||
// 在当前顺序中查该稳定身份的位置,不能直接把 slot 转 usize 当数组下标。
|
||||
let index = data
|
||||
.children
|
||||
.iter()
|
||||
@@ -699,31 +819,39 @@ impl NativeCallable for Function {
|
||||
.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()),
|
||||
@@ -734,17 +862,21 @@ impl NativeCallable for Function {
|
||||
}
|
||||
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);
|
||||
@@ -760,12 +892,15 @@ impl NativeCallable for Function {
|
||||
}
|
||||
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;
|
||||
@@ -777,11 +912,14 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 分离先创建新 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)?
|
||||
};
|
||||
@@ -789,24 +927,29 @@ impl NativeCallable for Function {
|
||||
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))?),
|
||||
@@ -816,20 +959,23 @@ impl NativeCallable for Function {
|
||||
}
|
||||
// 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)? {
|
||||
// Closing the tab ends its presentation even if source
|
||||
// reopens it before the next layout can detach the Mounts.
|
||||
// 关闭即终止旧出现的任务归属,不等待下一次布局实际卸载才取消。
|
||||
// 弱 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()
|
||||
@@ -837,9 +983,11 @@ impl NativeCallable for Function {
|
||||
.page
|
||||
== Some(page)
|
||||
{
|
||||
// 取消 owner/key 调度资格,旧 I/O 即使稍后完成也不能再向此出现交付。
|
||||
crate::tasks::cancel_owner(cx, mount)?;
|
||||
}
|
||||
}
|
||||
// 撤注册后为原窗口选择替代页或安排清空,生命周期卸载仍交后续刷新处理。
|
||||
remove_page(
|
||||
cx,
|
||||
state
|
||||
@@ -852,16 +1000,19 @@ impl NativeCallable for Function {
|
||||
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 文本明确阶段,其他值拒绝而不做真值转换。
|
||||
@@ -875,23 +1026,28 @@ impl NativeCallable for Function {
|
||||
// 清掉已断开连接后再计算容量,保留仍有效订阅顺序;新 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)?,
|
||||
})?;
|
||||
@@ -903,6 +1059,7 @@ impl NativeCallable for Function {
|
||||
// 基类事件/挂载回调默认空实现,子类可覆盖;不在这条默认路径生成额外状态。
|
||||
OnEvent | OnMount | OnUnmount => return Ok(NativeResult::Return(Value::Nil)),
|
||||
Click | Dispatch => {
|
||||
// 事件/订阅类型来自当前展示域,传播上下文仍由明确 owner/可选 Mount 指定。
|
||||
let desktop = self
|
||||
.session
|
||||
.lock()
|
||||
@@ -910,17 +1067,22 @@ impl NativeCallable for Function {
|
||||
.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")),
|
||||
}
|
||||
// 非提前返回的结构编辑/订阅操作在末尾统一经过写屏障,触发拥有边更新和变更日志。
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
//! 把内置开发工具和起始页接入托管 Page。工厂在所属 VM 中编译和执行,而不是在窗口线程另建业务状态。
|
||||
//!
|
||||
//! Developer workspaces are managed Pages, using the same roots and placement API.
|
||||
//! bootstrap::install 调用 install 和 install_start_page;前者注册 ui.developerTools,后者注册 ui.newTab。
|
||||
//! GUI 发出的 developer-open:* 输入由 UiBridge::dispatch 转成一次 VM 调用,再进入 OpenTool::invoke。
|
||||
//! 这里管理 Page 的创建与归属;Console/Network 等工具的原生内容由 shell/developer_tools 按页面标记绘制。
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 工具打开能力,保存所属会话和 ui 对象;调用时校验工具名,再在该 VM 中展示页面。
|
||||
struct OpenTool {
|
||||
// 查找现有页面和安排 show 时使用同一展示域;不另建开发工具专属 VM。
|
||||
session: Shared,
|
||||
// 保存 ui.newTab 工厂入口;ContextNativeFunction 的 captures 同时声明这个托管强边。
|
||||
ui: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for OpenTool {
|
||||
/// 仅接受一个工具名称字符串,工具类型与白名单在 invoke 检查。
|
||||
/// VM 在进入 invoke 前检查一个显式参数;这里只有数量检查,字符串类型与白名单由 invoke 检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
/// 把白名单工具名转换成对应托管 Page;New tab 转交预编译工厂,其余管理页复用已有实例或创建新页。
|
||||
/// VM 执行 ui.developerTools 时调用:New tab 委托源工厂,其余工具优先激活已有页面,否则创建标记页。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
// 同时验证数量和字符串表示;不能把任意对象的可读表示当作工具名字接受。
|
||||
let [Value::Text(name)] = args else {
|
||||
// 错误发生在任何页面分配/登记之前,因此无须撤销一次半完成的工具打开。
|
||||
return Err(invalid("developerTools requires a tool name"));
|
||||
};
|
||||
// 工具名白名单包含四个管理页和特殊新标签动作,拒绝任意字符串进入工厂/工具标记路径。
|
||||
if !["Console", "Sources", "Network", "Settings", "New tab"].contains(&name.as_str()) {
|
||||
// 白名单错误交回 VM,不把名字继续写入 _tesseraTool 让 GUI 猜测工具类型。
|
||||
return Err(invalid("unknown developer tool"));
|
||||
}
|
||||
// 新标签交给预编译 ui.newTab 托管工厂,经 NativeResult::Invoke 回到同一 VM 执行。
|
||||
if name == "New tab" {
|
||||
// 只解析函数身份,Invoke 让 VM 自己推进源工厂,保留调用栈/预算与托管返回值语义。
|
||||
let factory = object(Some(&cx.store().get_attr(self.ui, "newTab")?))?;
|
||||
return Ok(NativeResult::Invoke {
|
||||
function: factory,
|
||||
@@ -39,16 +47,19 @@ impl NativeCallable for OpenTool {
|
||||
.nodes
|
||||
.clone();
|
||||
for weak in nodes {
|
||||
// NativeContext::upgrade 的临时引用由原生调用域管理;这里只保留实际选中的工具页作为返回值。
|
||||
if let Some(page) = cx.upgrade(weak)?
|
||||
&& cx.store().get_attr(page, "_tesseraTool").ok() == Some(Value::Text(name.clone()))
|
||||
{
|
||||
// 已有同类工具页则激活并聚焦其所属原生窗口,不每次打开都创建重复 Page。
|
||||
show(cx, page, &self.session)?;
|
||||
let window = {
|
||||
// show 可能保留旧归属;重新查所属窗口,Focus 应发给真实窗口而不是当前主窗口。
|
||||
let state = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| invalid("UI session poisoned"))?;
|
||||
// show 成功后仍无登记表示桥接关系异常,不能随意选择其他窗口聚焦。
|
||||
let id = page_window(cx.store(), &state, page)?
|
||||
.ok_or(invalid("tool is not open"))?;
|
||||
object(Some(
|
||||
@@ -56,6 +67,7 @@ impl NativeCallable for OpenTool {
|
||||
.get_attr(state.presentations[&id].desktop, "window")?,
|
||||
))?
|
||||
};
|
||||
// 只登记原生命令,GUI 在发布后再执行系统聚焦;VM worker 不直接操作 OS 窗口。
|
||||
crate::window::enqueue(cx, window, crate::window::Action::Focus)?;
|
||||
return Ok(NativeResult::Return(Value::Object(page)));
|
||||
}
|
||||
@@ -67,14 +79,16 @@ impl NativeCallable for OpenTool {
|
||||
&self.session,
|
||||
None,
|
||||
)?;
|
||||
// title 是用户可见标签,_tesseraTool 是宿主识别内容的私有标记,不能只设置其中一个。
|
||||
cx.set_attr(page, "title", Value::Text(name.clone()))?;
|
||||
cx.set_attr(page, "_tesseraTool", Value::Text(name.clone()))?;
|
||||
show(cx, page, &self.session)?;
|
||||
Ok(NativeResult::Return(Value::Object(page)))
|
||||
}
|
||||
}
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
/// bootstrap::install 在任务/类型能力就绪后调用,注册开发工具 callable;此时不打开任何页面。
|
||||
pub(super) fn install(vm: &mut Vm, ui: ObjectRef, session: &Shared) -> Result<()> {
|
||||
// payload 共享 Session,captures 显式持有 ui;仅存 Rust ObjectRef 不会形成 VM 可追踪强边。
|
||||
let function = vm.allocate(ContextNativeFunction::new(
|
||||
"developerTools",
|
||||
OpenTool {
|
||||
@@ -89,16 +103,17 @@ pub(super) fn install(vm: &mut Vm, ui: ObjectRef, session: &Shared) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile the bundled Endor page once per VM, in a private module namespace.
|
||||
/// Its functions/classes and callbacks remain ordinary managed VM objects.
|
||||
/// 编译并安装 VM 私有的起始页工厂,供新页面动作重复调用。
|
||||
/// bootstrap 在 UiBridge 组装完成后调用,编译内置起始页模块并把 newTab 工厂挂到 ui。
|
||||
/// 每个 VM 仅安装一次,后续新标签操作重复调用工厂;源类/闭包仍由正常 VM 对象系统管理。
|
||||
pub(super) fn install_start_page(vm: &mut Vm, bridge: &UiBridge) -> Result<()> {
|
||||
// 源码随 binary 嵌入,每个 VM 安装时解析/编译一次;函数工厂之后重复执行不再重复编译模块。
|
||||
let source = frontend::EndorReader::from_text(
|
||||
"tessera:start-page.endor",
|
||||
include_str!("start_page.endor"),
|
||||
);
|
||||
// 按完整 Module 解析,保证类型与工厂定义属于一次安装环境,而不是用户 REPL 的多个输入片段。
|
||||
let ast = frontend::parse_source_ast(&source, frontend::ParseMode::Module)?;
|
||||
// 这里只生成代码模板,后面的 load 才把模板变成当前 VM 的托管代码对象。
|
||||
let compiled = compiler::compile_ast(
|
||||
&ast,
|
||||
&source,
|
||||
@@ -111,9 +126,12 @@ pub(super) fn install_start_page(vm: &mut Vm, bridge: &UiBridge) -> Result<()> {
|
||||
},
|
||||
)
|
||||
.map_err(|error| source.annotate(error))?;
|
||||
// 私有全局字典防止内置页面辅助类污染用户命名;导出闭包可通过托管边继续持有其环境。
|
||||
let globals = vm.new_dict()?;
|
||||
// 暂存安装/执行错误,闭包返回之后仍统一释放 globals 的局部拥有根。
|
||||
let result = (|| -> Result<()> {
|
||||
bridge.install_globals(vm, globals)?;
|
||||
// 加载所得代码有独立根,execute 成功/失败后都需 vm.release(code)。
|
||||
let code = compiled.load(vm)?;
|
||||
// 执行模块定义类和工厂,使用单次预算;这里不主动调用 newTab,不在安装时打开页面。
|
||||
let value = vm.execute_with_limits(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! 把页面、2D 视口和 3D 视口的相机输入转换成托管对象更新。
|
||||
//! 输入携带具体出现的 Mount;嵌套世界 UI 先做逆投影,避免把屏幕位移直接当成场景坐标。
|
||||
//!
|
||||
//! Camera mutations run on the owning VM worker, just like other page input.
|
||||
//! UiBridge::dispatch 按输入 kind 分流到下面三个方法:整页、局部二维视口和局部三维场景分别处理。
|
||||
//! 它们只在所属 VM worker 上修改托管相机属性或调用 controller.update,不直接改 GUI 已发布帧。
|
||||
//! 姿态变化随后由正常依赖/快照刷新观察;任何新帧仍需走 layout/paint/commit 的完整发布路径。
|
||||
use super::*;
|
||||
|
||||
impl UiBridge {
|
||||
/// 解码指定 3D 视口的旋转/平移/缩放输入,再调用其托管相机控制器。
|
||||
/// dispatch 的 scene-pan/orbit/zoom 分支调用,把命中出现位置的输入包装成 CameraGesture 交给控制器。
|
||||
pub(super) fn dispatch_scene_view(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -13,6 +15,7 @@ impl UiBridge {
|
||||
) -> Result<()> {
|
||||
// 锁内只读取当前发布版本与弱 Mount 身份,离开锁后才进入 VM 操作,避免持 session 锁调用用户代码。
|
||||
let (weak, desktop) = {
|
||||
// 当前窗口已由外层选定;本锁内同时读版本和 Mount,避免把旧几何与新弱身份混用。
|
||||
let state = self
|
||||
.session
|
||||
.lock()
|
||||
@@ -30,9 +33,11 @@ impl UiBridge {
|
||||
state.desktop,
|
||||
)
|
||||
};
|
||||
// upgrade 返回临时拥有根,下方即使任一步失败也通过闭包外的 release 清理。
|
||||
let mount = vm
|
||||
.upgrade(weak)?
|
||||
.ok_or_else(|| anyhow::anyhow!("3D viewport Mount expired"))?;
|
||||
// 不能直接 ? 返回整个方法,否则错误时跳过 Mount 根释放;用闭包暂存处理结果。
|
||||
let result = (|| {
|
||||
// 除 session 发布版本外,再检查目标自身仍挂接、版本匹配且确为 3D 视口,拒绝旧出现操作。
|
||||
let occurrence = vm.payload::<crate::mount::Mount>(mount, "Mount")?.clone();
|
||||
@@ -42,8 +47,11 @@ impl UiBridge {
|
||||
&& occurrence.geometry.scene3d_viewport,
|
||||
"obsolete 3D viewport"
|
||||
);
|
||||
// resolve 跟随 Block 的共享替换关系,再读取实际视觉状态,不能只信任旧 payload。
|
||||
let (_, data) = block::resolve(vm.object_store(), occurrence.block)?;
|
||||
// visual_data 合并当前可见属性,后面的相机/控制器来自模型而非原生画面的旧快照。
|
||||
let data = block::visual_data(vm.object_store(), data)?;
|
||||
// 没有显式相机就无法解释手势,拒绝更新而不是猜一个默认相机改变场景语义。
|
||||
let camera = data
|
||||
.camera
|
||||
.ok_or_else(|| anyhow::anyhow!("missing 3D camera"))?;
|
||||
@@ -51,18 +59,25 @@ impl UiBridge {
|
||||
let controller = data
|
||||
.controller
|
||||
.ok_or_else(|| anyhow::anyhow!("3D controller disabled"))?;
|
||||
// 同一视觉对象的边框/内边距由快照解析,手势锚点与可用尺寸都应扣除这部分。
|
||||
let node = block::snapshot(vm.object_store(), occurrence.block)?;
|
||||
// inset 将外框换算成视口内容盒,避免控制器把边框尺寸算成场景宽高。
|
||||
let inset = crate::layout::insets(&node);
|
||||
// 几何来自具体 Mount,两个窗口中共享同一场景仍可能有不同视图投影。
|
||||
let geometry = &occurrence.geometry;
|
||||
// 外层二维视图逆变换用于锚点定位;没有视图变换时使用恒等变换。
|
||||
let outer = geometry.view.unwrap_or_default();
|
||||
// delta 的第一分量还会用作缩放倍率;缺失说明输入信封不满足相机事件要求。
|
||||
let delta = input
|
||||
.delta
|
||||
.ok_or_else(|| anyhow::anyhow!("3D camera delta required"))?;
|
||||
// 缩放使用倍率而非二维位移;旋转/平移通过 logical_delta 处理嵌套二维/面板投影。
|
||||
// 不可逆映射时忽略本次连续增量,不在无效坐标上继续更新相机。
|
||||
let local_delta = if input.kind == "scene-zoom" {
|
||||
// 缩放不产生平移 dx/dy,只把原第一分量写到 gesture.factor。
|
||||
[0., 0.]
|
||||
} else {
|
||||
// 平移/旋转先撤销外层投影;映射失败表示本帧增量无法可靠应用。
|
||||
let Some(delta) = geometry.logical_delta(input.point, delta) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -79,6 +94,7 @@ impl UiBridge {
|
||||
));
|
||||
// 将输入封装为名义 CameraGesture 临时对象;不把原生 Input 或窗口指针直接交给源控制器。
|
||||
let gesture = vm.new_object()?;
|
||||
// 临时手势也有独立拥有根;把附类型/写字段/控制器错误暂存,外层始终 release(gesture)。
|
||||
let result = (|| {
|
||||
vm.attach_type(
|
||||
gesture,
|
||||
@@ -95,10 +111,12 @@ impl UiBridge {
|
||||
("pointX", point.x - geometry.x - inset.left),
|
||||
("pointY", point.y - geometry.y - inset.top),
|
||||
] {
|
||||
// 全部数值通过统一语言数值编码写属性,控制器读取的是普通托管字段。
|
||||
vm.set_attr(gesture, name, crate::native_visual::value(value))?;
|
||||
}
|
||||
// 绑定 controller.update 保留接收者语义,再带 camera/gesture 两个显式参数进行有界调用。
|
||||
let function = vm.bind_attr(controller, "update")?;
|
||||
// callable 校验在 VM 执行边界继续完成,非对象方法值直接作为契约错误返回。
|
||||
let result = match function {
|
||||
Value::Object(function) => vm.call_with_limits(
|
||||
function,
|
||||
@@ -124,7 +142,7 @@ impl UiBridge {
|
||||
result
|
||||
}
|
||||
|
||||
/// 把具体 CanvasViewport 的位移或缩放锚点映射到其显式 Camera2D。
|
||||
/// dispatch 的 viewport-pan/zoom 分支调用,逆投影具体 CanvasViewport 输入并改写其 Camera2D 属性。
|
||||
pub(super) fn dispatch_viewport_view(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -132,6 +150,7 @@ impl UiBridge {
|
||||
) -> Result<()> {
|
||||
// 二维视口同样先校验发布版本,再从活动表捕获原 Mount,不能因目标消失自动换成另一个视口。
|
||||
let weak = {
|
||||
// 锁内只检查发布版本并复制弱身份;下面读取相机和写 VM 属性时不持有会话锁。
|
||||
let state = self
|
||||
.session
|
||||
.lock()
|
||||
@@ -146,10 +165,13 @@ impl UiBridge {
|
||||
.get(&input.mount)
|
||||
.ok_or_else(|| anyhow::anyhow!("viewport is no longer active"))?
|
||||
};
|
||||
// 临时根保证输入处理期间 Mount 仍可读取,方法尾部无论成功失败都释放。
|
||||
let mount = vm
|
||||
.upgrade(weak)?
|
||||
.ok_or_else(|| anyhow::anyhow!("viewport Mount expired"))?;
|
||||
// 闭包集中实际处理,使所有 ? 都先回到统一释放路径。
|
||||
let result = (|| {
|
||||
// 弱身份能升级不代表仍挂载/版本有效,先验证具体出现上下文。
|
||||
let occurrence = vm.payload::<crate::mount::Mount>(mount, "Mount")?;
|
||||
anyhow::ensure!(
|
||||
occurrence.mounted
|
||||
@@ -157,32 +179,44 @@ impl UiBridge {
|
||||
&& occurrence.geometry.canvas_viewport,
|
||||
"obsolete viewport occurrence"
|
||||
);
|
||||
// 复制几何和 Block 身份,结束 occurrence 对 VM 的借用后才能改写相机。
|
||||
let geometry = occurrence.geometry.clone();
|
||||
// Block 只用于找相机/边距,输入定位仍来自 Mount 的几何。
|
||||
let block = occurrence.block;
|
||||
// 共享替换后的实际视觉状态才包含当前相机引用。
|
||||
let (_, data) = block::resolve(vm.object_store(), block)?;
|
||||
// 托管属性覆盖 payload 缺省值,保持与布局/绘制同一视觉读取约定。
|
||||
let data = block::visual_data(vm.object_store(), data)?;
|
||||
// 局部视口必须有 Camera2D,不把整页 Surface 当作相机替代。
|
||||
let camera = data
|
||||
.camera
|
||||
.ok_or_else(|| anyhow::anyhow!("viewport has no camera"))?;
|
||||
// 读取已校验的相机快照作为本次基准;共享相机用户会通过普通属性变更观察到相同更新。
|
||||
let old = crate::viewport::camera(vm.object_store(), camera)?;
|
||||
// 该分量在 zoom 时表示倍率,在 pan 时表示外部逻辑位移,随后分支分别解释。
|
||||
let delta = input
|
||||
.delta
|
||||
.ok_or_else(|| anyhow::anyhow!("viewport input has no delta"))?;
|
||||
// 基于旧快照计算三个新值,完成验证后才依次回写托管属性。
|
||||
let (mut x, mut y, mut zoom) = (old.x, old.y, old.zoom);
|
||||
if input.kind == "viewport-zoom" {
|
||||
// 缩放必须有锚点,不能让缺失指针导致隐式绕视口原点缩放。
|
||||
let pointer = input
|
||||
.point
|
||||
.ok_or_else(|| anyhow::anyhow!("zoom requires anchor"))?;
|
||||
// 先用屏幕/视图坐标做外框检查,再逆投影到布局逻辑坐标。
|
||||
let pointer = crate::geometry::Point::new(pointer[0], pointer[1]);
|
||||
anyhow::ensure!(
|
||||
geometry.contains(pointer),
|
||||
"zoom anchor is outside viewport"
|
||||
);
|
||||
// 外框命中不保证投影可逆;None 作为错误返回,不能继续使用原屏幕数值。
|
||||
let pointer = geometry
|
||||
.logical_point(pointer)
|
||||
.ok_or_else(|| anyhow::anyhow!("zoom anchor cannot be projected"))?;
|
||||
// 当前 Block 快照给出布局边距,用它取得正确内容盒的缩放锚点。
|
||||
let node = block::snapshot(vm.object_store(), block)?;
|
||||
// inset 与布局模块共用同一规则,避免相机手势与绘制对内容原点理解不同。
|
||||
let inset = crate::layout::insets(&node);
|
||||
// 已验证指针落在出现外框还不够;扣除 inset 后必须落入实际内容盒,禁止用边框作为缩放锚点。
|
||||
let local = [
|
||||
@@ -201,6 +235,7 @@ impl UiBridge {
|
||||
x += local[0] / old.zoom - local[0] / zoom;
|
||||
y += local[1] / old.zoom - local[1] / zoom;
|
||||
} else {
|
||||
// 平移没有锚点保持公式,只需逆投影增量再除相机自身缩放。
|
||||
let Some(delta) = geometry.logical_delta(input.point, delta) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -216,11 +251,12 @@ impl UiBridge {
|
||||
vm.set_attr(camera, "zoom", crate::native_visual::value(zoom))?;
|
||||
Ok(())
|
||||
})();
|
||||
// release 发生在 result? 之前,保证投影/属性写入错误不会泄漏升级得到的 Mount。
|
||||
vm.release(mount)?;
|
||||
result
|
||||
}
|
||||
|
||||
/// 处理旧 Page 模式的兼容相机事件;独立视口使用各自的输入路径。
|
||||
/// dispatch 的 page-pan/zoom 分支调用,兼容旧 Page 表面相机;独立 CanvasViewport 不走此方法。
|
||||
pub(super) fn dispatch_page_view(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -228,6 +264,7 @@ impl UiBridge {
|
||||
) -> Result<()> {
|
||||
// 兼容 Page 相机不通过 Mount 定位,而是读取当前活动页面及其 Surface 副本;发布版本仍必须一致。
|
||||
let (page, mut surface, viewport) = {
|
||||
// 只复制当前已发布页的相机与真实视口,不持锁执行下面的坐标计算或 VM 属性写入。
|
||||
let state = self
|
||||
.session
|
||||
.lock()
|
||||
@@ -244,17 +281,21 @@ impl UiBridge {
|
||||
state.layout.viewport,
|
||||
)
|
||||
};
|
||||
// 外层信封校验之后仍要求本路径必需的增量字段,防止不完整调用静默改变相机。
|
||||
let delta = input
|
||||
.delta
|
||||
.ok_or_else(|| anyhow::anyhow!("page view requires delta"))?;
|
||||
if input.kind == "page-zoom" {
|
||||
// 旧 Page 模式只允许 Canvas 缩放,有限文档或 Window 页面保持各自尺寸语义。
|
||||
anyhow::ensure!(
|
||||
surface.mode == crate::page::Mode::Canvas,
|
||||
"zoom requires canvas mode"
|
||||
);
|
||||
// 以光标为缩放锚点,先要求它存在,再检查可用视口范围。
|
||||
let pointer = input
|
||||
.point
|
||||
.ok_or_else(|| anyhow::anyhow!("zoom requires anchor"))?;
|
||||
// 转为页面几何共用的 Point,便于调用 Surface::to_world 做缩放前逆变换。
|
||||
let pointer = crate::geometry::Point::new(pointer[0], pointer[1]);
|
||||
anyhow::ensure!(
|
||||
pointer.x >= 0.0
|
||||
@@ -270,6 +311,7 @@ impl UiBridge {
|
||||
world.y - pointer.y / surface.zoom,
|
||||
);
|
||||
} else {
|
||||
// 平移按当前 zoom 换成世界距离,不直接把屏幕像素累加进世界 pan。
|
||||
surface.pan.x += delta[0] / surface.zoom;
|
||||
surface.pan.y += delta[1] / surface.zoom;
|
||||
}
|
||||
@@ -277,6 +319,7 @@ impl UiBridge {
|
||||
if let Some(viewport) = viewport {
|
||||
surface.clamp(viewport);
|
||||
}
|
||||
// 内容模式 clamp 之外再加通用坐标预算;Canvas 可自由漫游,但也不能无限增长偏移。
|
||||
let limit = MAX_DIMENSION as f32;
|
||||
surface.pan.x = surface.pan.x.clamp(-limit, limit);
|
||||
surface.pan.y = surface.pan.y.clamp(-limit, limit);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
//! 声明宿主方法在 Endor 类型系统中的参数和返回契约。
|
||||
//! 签名用于构造/调用检查,应与 calls 和各 NativeCallable 实现保持一致;这里不执行实际业务操作。
|
||||
//!
|
||||
//! Canonical contracts for native Block methods and constructors.
|
||||
//! bootstrap 在类型安装的第二阶段调用这两个函数,把完整签名写入 TypeMethod、Function 和 NativeFactory。
|
||||
//! ui_bridge::bind 也会为状态对象的绑定函数调用 operation_signature。
|
||||
//! VM 随后在调用边界读取这些签名,验证数量、类型及调用效果;业务实现位于 calls/scene3d 等模块。
|
||||
//! 参数注解并不决定是否必填:Operation::arity 给出实参数量,注解表则检查实际出现的每一个参数。
|
||||
use super::*;
|
||||
|
||||
/// 根据宿主方法类别提供 Endor 方法的签名,供类型与调用检查使用。
|
||||
/// bootstrap 安装实例方法时调用,构造包含 receiver 的 VM 方法契约。
|
||||
/// receiver 是方法声明所在类型,不是某次调用的实例;实际接收者可以是该类型的合法子类。
|
||||
pub(super) fn method_signature(
|
||||
store: &endor_core::ObjectStore,
|
||||
desktop: ObjectRef,
|
||||
@@ -13,11 +17,14 @@ pub(super) fn method_signature(
|
||||
) -> Result<CallSignature, VmError> {
|
||||
use AnnotationType::{Any, Function, Nominal, Number, String as TextType, Unit};
|
||||
use Operation::*;
|
||||
// 闭包把宿主约定的名字解析成当前 desktop 的真实 Type;查找失败中止安装,不能降级为 Any。
|
||||
let nominal = |name| desktop_type(store, name, desktop).map(Nominal);
|
||||
// 先描述显式实参,不含隐式 receiver;后面统一在 parameters 首部补接收者。
|
||||
// ReadOnly/Mutating/External 是 VM 调用效果契约,不是线程调度策略。
|
||||
let (arguments, returns, effect) = match operation {
|
||||
// 三个数分别供位置/旋转操作使用;方法修改现有三维对象,没有额外返回值。
|
||||
Translate3D | Rotate3D => (vec![Number, Number, Number], Unit, CallEffect::Mutating),
|
||||
// 控制器处理被控对象和相机手势;手势是类型化协议值,不能接任意文本或标量。
|
||||
CameraControl => (
|
||||
vec![nominal("Object")?, nominal("CameraGesture")?],
|
||||
Unit,
|
||||
@@ -29,9 +36,13 @@ pub(super) fn method_signature(
|
||||
nominal("Subscription")?,
|
||||
CallEffect::Mutating,
|
||||
),
|
||||
// 查询已有出现位置,返回托管列表;不修改对象或展示关系。
|
||||
Mounts => (vec![], nominal("ObjectList")?, CallEffect::ReadOnly),
|
||||
// Children 的实际值仍由容器实现解释;这里的 Any 不改变只读效果约束。
|
||||
Children => (vec![], Any, CallEffect::ReadOnly),
|
||||
// 可选文本用于标记失效类别;是否可省略由 arity 决定,非此处的 vec 长度决定。
|
||||
Invalidate => (vec![TextType], Unit, CallEffect::Mutating),
|
||||
// 修改既有文本/颜色,内容合法性(如颜色格式)由调用实现进一步校验。
|
||||
SetText | SetColor => (vec![TextType], Unit, CallEffect::Mutating),
|
||||
// append/insert 返回稳定 slot 数值;remove/replace/move 返回 Unit,调用方不能把它们当返回孩子对象的集合方法。
|
||||
Append => (vec![nominal("Block")?], Number, CallEffect::Mutating),
|
||||
@@ -40,15 +51,19 @@ pub(super) fn method_signature(
|
||||
Number,
|
||||
CallEffect::Mutating,
|
||||
),
|
||||
// 数字注解只验证 Number;非负整数、稳定槽位是否存在等语义由 calls 检查。
|
||||
Remove => (vec![Number], Unit, CallEffect::Mutating),
|
||||
Replace => (vec![Number, nominal("Block")?], Unit, CallEffect::Mutating),
|
||||
Move => (vec![Number, Number], Unit, CallEffect::Mutating),
|
||||
// 页面内容替换/导航接受可视 Block,并改写待展示状态;此处不提前执行布局。
|
||||
Render | Navigate => (vec![nominal("Block")?], Unit, CallEffect::Mutating),
|
||||
// 链式 API 返回接收者名义类型,保留子类返回契约,而不是统一降为 Block。
|
||||
// 链式 API 返回声明 receiver 的名义类型;源子类实例也满足此基类契约,不承诺精确动态子类。
|
||||
OnClick => (vec![Function], Nominal(receiver), CallEffect::Mutating),
|
||||
// 事件和生命周期方法可能执行任意用户回调,使用 External,不能冒充只读视觉查询。
|
||||
OnEvent => (vec![nominal("Event")?], Unit, CallEffect::External),
|
||||
// Mount 标识一次具体出现位置;同一 Block 的多次展示不能共用一个隐式上下文。
|
||||
OnMount | OnUnmount => (vec![nominal("Mount")?], Unit, CallEffect::External),
|
||||
// click/dispatch 返回实际事件,便于调用者观察传播状态;Mount 可选性由 arity 给出。
|
||||
Click => (
|
||||
vec![nominal("Mount")?],
|
||||
nominal("Event")?,
|
||||
@@ -59,28 +74,38 @@ pub(super) fn method_signature(
|
||||
nominal("Event")?,
|
||||
CallEffect::External,
|
||||
),
|
||||
// 迁移要求真实 Window 实例并返回页面,窗口是否关闭、是否属于当前运行时由实现继续检查。
|
||||
MoveWindow => (
|
||||
vec![nominal("Window")?],
|
||||
Nominal(receiver),
|
||||
CallEffect::Mutating,
|
||||
),
|
||||
// 标签位置是数值,Activate 无显式实参;二者都改变展示选择并允许链式使用。
|
||||
MoveTab => (vec![Number], Nominal(receiver), CallEffect::Mutating),
|
||||
Activate => (vec![], Nominal(receiver), CallEffect::Mutating),
|
||||
// detach 新建目标窗口,返回 Window 而非当前页面;资源创建属于修改操作。
|
||||
Detach => (vec![], nominal("Window")?, CallEffect::Mutating),
|
||||
// 这些操作只修改生命周期或历史状态,均没有业务返回值。
|
||||
Unmount | Close | CloseWindow | Back | Forward => (vec![], Unit, CallEffect::Mutating),
|
||||
// 先以 Any 占位,后面用 nullable(Window) 收窄,允许省略/显式 nil 选择默认归属。
|
||||
Open => (vec![Any], Nominal(receiver), CallEffect::Mutating),
|
||||
// 页面尚未打开时无所属窗口,稍后把返回 Any 改为 Window | nil。
|
||||
PageWindow => (vec![], Any, CallEffect::ReadOnly),
|
||||
// 事件名、回调和可选捕获选项;返回 Subscription 令牌供取消,不直接返回 receiver。
|
||||
On => (
|
||||
vec![TextType, Function, Any],
|
||||
nominal("Subscription")?,
|
||||
CallEffect::Mutating,
|
||||
),
|
||||
// 自由工厂 Operation 不应误装成实例方法;缺少明确契约就拒绝安装,防止默认放宽检查。
|
||||
_ => return Err(invalid("unsupported native method signature")),
|
||||
};
|
||||
// Operation 维护显式参数的必需/最大个数;方法签名整体加一,将 receiver 纳入调用约定。
|
||||
let arity = operation.arity();
|
||||
// parameters[0] 固定为 receiver,因此后面细化显式参数时都必须把索引加一。
|
||||
let mut parameters = vec![Nominal(receiver)];
|
||||
parameters.extend(arguments);
|
||||
// 先把简单 AnnotationType 升格为 CallType;下面再改写少数联合/可空位置。
|
||||
let mut signature = CallSignature {
|
||||
arity: endor_core::CallArity::range(arity.required + 1, arity.maximum + 1),
|
||||
parameters: parameters.into_iter().map(CallType::from).collect(),
|
||||
@@ -91,15 +116,20 @@ pub(super) fn method_signature(
|
||||
// 少数参数/返回需要联合或可空类型,先用占位 Any 建基础签名,再按下标细化。
|
||||
// On 第三个显式选项位于 parameters[3],可接受 Bool 或字符串。
|
||||
match operation {
|
||||
// 参数允许 nil,但非空值必须是本 desktop 注册的 Window 实例。
|
||||
Open => signature.parameters[1] = CallType::nullable(nominal("Window")?),
|
||||
// 没有归属是正常查询结果,与错误窗口对象的类型失败不同。
|
||||
PageWindow => signature.returns = CallType::nullable(nominal("Window")?),
|
||||
// 兼容布尔捕获标记和命名阶段字符串;具体字符串语义在事件连接操作中检查。
|
||||
On => signature.parameters[3] = CallType::Union(vec![AnnotationType::Bool, TextType]),
|
||||
// 其余操作已经完整表达,不应额外增加可空或联合类型。
|
||||
_ => {}
|
||||
}
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// 将具体宿主 Operation 映射到参数/返回类型契约。
|
||||
/// bootstrap 和 ui_bridge::bind 调用,描述不含 receiver 的自由函数/状态绑定函数原生参数。
|
||||
/// NativeFactory 复用本结果作为 options 窄签名,再在外部包上 Type 首参和源子类字段。
|
||||
pub(super) fn operation_signature(
|
||||
store: &endor_core::ObjectStore,
|
||||
desktop: ObjectRef,
|
||||
@@ -107,27 +137,36 @@ pub(super) fn operation_signature(
|
||||
) -> Result<CallSignature, VmError> {
|
||||
use AnnotationType::{Any, Bool, Number, String as TextType, Trait, Type, Unit};
|
||||
use Operation::*;
|
||||
// 注册表来自所属 VM 的 desktop;同名但不同身份的外部 Type 不能满足当前名义契约。
|
||||
let nominal = |name| desktop_type(store, name, desktop).map(AnnotationType::Nominal);
|
||||
// 自由 UI 操作没有隐式 receiver;同时声明固定参数、可变尾参、返回名义类型和调用效果。
|
||||
let (parameters, variadic, returns, effect) = match operation {
|
||||
// 类型关系查询不改变类型表;IsInstance 签名首参为 Any,但 calls 当前仍要求实际值为对象。
|
||||
IsInstance => (vec![Any, Type], None, Bool, CallEffect::ReadOnly),
|
||||
IsSubtype => (vec![Type, Type], None, Bool, CallEffect::ReadOnly),
|
||||
// 注册能力实现会修改类型关系;implements/supports 只查询已有关系。
|
||||
Implement => (vec![Type, Trait], None, Unit, CallEffect::Mutating),
|
||||
Implements => (vec![Type, Trait], None, Bool, CallEffect::ReadOnly),
|
||||
Supports => (vec![Any, Trait], None, Bool, CallEffect::ReadOnly),
|
||||
// open 可把普通值包装成视觉/Page 并安排展示,返回供继续操作的 Page。
|
||||
OpenValue => (vec![Any], None, nominal("Page")?, CallEffect::Mutating),
|
||||
// present 可能转交用户展示协议,效果交由被委托调用确定,不能声明成固定 ReadOnly。
|
||||
Present => (vec![Any], None, nominal("Block")?, CallEffect::Delegated),
|
||||
// state 可分配新的状态对象;ReadOnly 允许新对象初始化,不授权修改原有对象。
|
||||
State => (vec![Any], None, Any, CallEffect::ReadOnly),
|
||||
// set/increment 的 receiver 已存在 Function.owner 中,不计入本处参数表。
|
||||
StateSet => (vec![Any], None, Unit, CallEffect::Mutating),
|
||||
Increment => (vec![], None, Unit, CallEffect::Mutating),
|
||||
// stats 读取宿主计数而非纯托管属性依赖,按 External 描述,不作为只读布局计算接口。
|
||||
Stats => (vec![], None, Any, CallEffect::External),
|
||||
// viewport 返回尺寸协议值;窗口尺寸尚缺失等情形仍由实际实现处理。
|
||||
Viewport => (
|
||||
vec![],
|
||||
None,
|
||||
nominal("MeasureResult")?,
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
// ui.window 查询现有窗口,Window 类型构造则创建新资源,二者不能共享调用效果。
|
||||
Window => (vec![], None, nominal("Window")?, CallEffect::ReadOnly),
|
||||
NewWindow => (vec![], None, nominal("Window")?, CallEffect::Mutating),
|
||||
// 这些构造操作返回新对象,ReadOnly 允许分配并初始化新值,但不允许因此修改已有业务对象。
|
||||
@@ -137,18 +176,21 @@ pub(super) fn operation_signature(
|
||||
nominal("TextBlock")?,
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
// TextOf 前两个参数表示对象/属性名;Text 直接接收字符串,其后字号和颜色可选。
|
||||
Text => (
|
||||
vec![TextType, Number, TextType],
|
||||
None,
|
||||
nominal("TextBlock")?,
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
// 矩形宽高为必填,后续数值/颜色的缺省规则由 Operation::arity 与构造实现共同约束。
|
||||
Rect => (
|
||||
vec![Number, Number, Number, TextType],
|
||||
None,
|
||||
nominal("RectBlock")?,
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
// 空白块只需要长度;滚动块额外要求宽高和一个 Block 内容。
|
||||
Spacer => (vec![Number], None, nominal("Spacer")?, CallEffect::ReadOnly),
|
||||
Scroll => (
|
||||
vec![Number, Number, nominal("Block")?],
|
||||
@@ -160,15 +202,20 @@ pub(super) fn operation_signature(
|
||||
Row | Column | Overlay => (
|
||||
vec![Number, Number],
|
||||
Some(nominal("Block")?),
|
||||
// 共享参数规则,但返回值必须保留具体容器名义类型;matches 只选择类型名,不执行排版。
|
||||
nominal(if matches!(operation, Row) {
|
||||
// 主轴水平排列的容器。
|
||||
"Row"
|
||||
} else if matches!(operation, Overlay) {
|
||||
// 多个孩子叠放在同一内容区域的容器。
|
||||
"Overlay"
|
||||
} else {
|
||||
// 此分支入口只有 Row/Column/Overlay,前两者排除后剩 Column。
|
||||
"Column"
|
||||
})?,
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
// 按钮接收初始文本,Page 无显式参数;两者都只创建值,不自动打开窗口。
|
||||
Button => (
|
||||
vec![TextType],
|
||||
None,
|
||||
@@ -176,6 +223,7 @@ pub(super) fn operation_signature(
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
Page => (vec![], None, nominal("Page")?, CallEffect::ReadOnly),
|
||||
// 三维视口的宽高和 Scene3D 在调用边界检查;摄像机、场景元素等由场景模块继续验证。
|
||||
Viewport3D => (
|
||||
vec![Number, Number, nominal("Scene3D")?],
|
||||
None,
|
||||
@@ -184,12 +232,15 @@ pub(super) fn operation_signature(
|
||||
),
|
||||
// 3D 构造器参数来自 Kind 元数据:Number/String 映成基础注解,其余名称从本 desktop 注册表查名义类型。
|
||||
SceneObject(kind) => {
|
||||
// 元数据与真实构造器共用一份参数定义,避免桥接手工重复参数表后与 3D 实现漂移。
|
||||
let parameters = kind
|
||||
.parameters()
|
||||
.iter()
|
||||
.map(|name| match *name {
|
||||
// 基础数值/字符串是语言原生注解,不需要到宿主类型字典查询。
|
||||
"Number" => Ok(Number),
|
||||
"String" => Ok(TextType),
|
||||
// Vec3/Geometry3D 等名称必须解析为本会话真实 Type;未知名让整个签名安装失败。
|
||||
name => nominal(name),
|
||||
})
|
||||
.collect::<Result<Vec<_>, VmError>>()?;
|
||||
@@ -197,14 +248,17 @@ pub(super) fn operation_signature(
|
||||
parameters,
|
||||
// 3D 集合尾参使用 Object 名义约束;各具体构造操作仍需在实现中检查允许的场景对象类别。
|
||||
if kind.variadic() {
|
||||
// 允许额外对象参数的场景组等构造器用 Object 作尾参上界。
|
||||
Some(nominal("Object")?)
|
||||
} else {
|
||||
// 固定参数构造器禁止多余实参,不能统一使用 Any 尾参放开上界。
|
||||
None
|
||||
},
|
||||
nominal(kind.name())?,
|
||||
CallEffect::ReadOnly,
|
||||
)
|
||||
}
|
||||
// 二维场景可接受零到多个 Block;场景视口固定接宽、高、Scene2D,不把孩子直接当场景。
|
||||
Scene2D => (
|
||||
vec![],
|
||||
Some(nominal("Block")?),
|
||||
@@ -217,12 +271,14 @@ pub(super) fn operation_signature(
|
||||
nominal("CanvasViewport")?,
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
// x/y/zoom 都声明为 Number,但可缺省;缩放范围等数值约束由 new_camera2d 校验。
|
||||
Camera2D => (
|
||||
vec![Number, Number, Number],
|
||||
None,
|
||||
nominal("Camera2D")?,
|
||||
CallEffect::ReadOnly,
|
||||
),
|
||||
// 实例方法专用 Operation 不在自由函数表内,误用在这里应立即暴露安装错误。
|
||||
_ => return Err(invalid("unsupported UI function signature")),
|
||||
};
|
||||
// 最终 arity 直接沿用 Operation,参数注解与可变尾参分开保存;不在此处执行操作或分配业务对象。
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
// The page, cards and actions belong to Endor. The host only owns its sidebar.
|
||||
// 默认页由真实 Endor 对象组成:宿主负责窗口和侧栏,卡片布局、绘制和按钮回调在源代码中定义。
|
||||
// developer_pages::install_start_page 在每个 VM 安装此模块;OpenTool 的 New tab 动作调用下面的 newTab。
|
||||
// 渲染器通过视觉协议调用 measure/paint,按钮事件通过 VM 分派到 StartActions 的绑定方法。
|
||||
// Note 复用 Column 的内容测量,但固定卡片宽度;paint 只提供背景,孩子仍沿正常树绘制。
|
||||
class Note: Column {
|
||||
// 卡片构造的最后一个子类字段保存背景色;前面的变参仍按 Column 的孩子参数处理。
|
||||
tint: String
|
||||
// source_layout 调用这个覆盖方法;显式借用 Column 的测量结果,只替换卡片宽度,保留内容自然高度。
|
||||
function measure(self: Note, mount: Mount, constraints: Constraints): MeasureResult {
|
||||
// 沿用 Column 内容测量得到高度,但卡片宽固定为 224;不把背景圆角绘制误当布局尺寸。
|
||||
measured = Column.measure(self, mount, constraints)
|
||||
return MeasureResult(224, measured.height)
|
||||
}
|
||||
// source_paint 在已完成几何上调用;这里返回绘制命令,未直接向 OS 或像素缓冲写入。
|
||||
function paint(self: Note, mount: Mount, geometry: Bounds): DisplayList {
|
||||
// 只生成当前卡片背景,命令使用宿主给定 Bounds 的绝对布局位置,孩子仍由各自 paint 绘制。
|
||||
fill = PaintCommand()
|
||||
@@ -36,25 +40,35 @@ class StartActions {
|
||||
home: Page
|
||||
// 窗口页面是普通有限 Column;可用区域来自宿主发布的逻辑视口。
|
||||
function blank(self: StartActions, event: Event): Unit {
|
||||
// “窗口页面”按钮的 click 回调创建独立 Page,标题和内容属于新页面,home 仍指向原画布。
|
||||
page = Page()
|
||||
// 标题供 project_tabs 投影到原生侧栏,不影响页面身份。
|
||||
page.title = "窗口页面"
|
||||
// 返回按钮保留原 home 的绑定方法,点击时激活已有页而非新建一个画布。
|
||||
back = Button("返回画布")
|
||||
// 绑定的是原 home 的 activate 方法;此回调恢复已有页身份,不重新构造主画布。
|
||||
back.onClick(self.home.activate)
|
||||
// render 设置内容并安排展示,Column 的 padding/gap 只影响这张页的有限布局。
|
||||
page.render(Column(56, 20, TextBlock("恰好一个窗口", 3), TextBlock("页面跟随可用区域调整。内容和工具栏由 Endor 自己组织。"), back))
|
||||
}
|
||||
// 延伸方向属于容器与滚动组合:Column 安排内容,有限 ScrollBlock 控制可见范围。
|
||||
function vertical(self: StartActions, event: Event): Unit {
|
||||
// “纵向延伸”按钮调用;保持 Page 默认有限布局,滚动行为由后面的 ScrollBlock 承担。
|
||||
page = Page()
|
||||
// 标签标题和其他示例区分,Page 本身不切换到旧式无限页面模式。
|
||||
page.title = "纵向页面"
|
||||
// 外 Column 累加卡片高度,内 Column 组织说明文字;自然内容高度可超过可见视口。
|
||||
content = Column(48, 28, Column(0, 12, TextBlock("向下,继续生长", 3), TextBlock("纵向滚动和 Column 排版可以独立组合。")))
|
||||
// 每张 Note 的最后一参对应 tint 子类字段,前面的视觉值仍是 Column 的子项。
|
||||
content.append(Note(24, 16, TextBlock("01 / 收集想法", 2), RectBlock(176, 130, 10, "#E2EADB"), TextBlock("从一个小小的念头开始。"), "#F0F3E9"))
|
||||
content.append(Note(24, 16, TextBlock("02 / 连接片段", 2), RectBlock(176, 130, 10, "#E6DBEF"), TextBlock("内容保留在同一条连续的页面里。"), "#F4EEF7"))
|
||||
content.append(Note(24, 16, TextBlock("03 / 慢慢展开", 2), RectBlock(176, 130, 10, "#EDDCC5"), TextBlock("追加一个子组件,内容自然延伸。"), "#FAF2E7"))
|
||||
content.append(Note(24, 16, TextBlock("04 / 留一点空白", 2), TextBlock("不必把所有想法都放在第一屏。"), "#EDF1F6"))
|
||||
// 固定初始视口内装完整内容;下一行限制滚动轴,flex 和页面 stretch 再让视口服从窗口分配。
|
||||
view = ScrollBlock(1024, 800, content)
|
||||
// 只消费纵向滚轮,横向内容不会因普通滚动意外平移。
|
||||
view.axis = "vertical"
|
||||
// flex 请求父布局分配剩余主轴空间,crossAlign 让有限视口横向填满页面。
|
||||
view.flex = 1
|
||||
page.crossAlign = "stretch"
|
||||
// 最后提交有限视口作为 Page 内容,触发展示请求;真正的测量、绘制与原生发布由宿主刷新流程完成。
|
||||
@@ -62,18 +76,25 @@ class StartActions {
|
||||
}
|
||||
// Row 内可继续放 Column 卡片;横向滚动不要求改变 Page 的身份或生命周期。
|
||||
function horizontal(self: StartActions, event: Event): Unit {
|
||||
// “横向延伸”按钮调用;横向排列和可见区域分别交给 Row 与 ScrollBlock。
|
||||
page = Page()
|
||||
// 仅设置标签内容,实际页面资源仍在当前 VM 的 Window 注册表中登记。
|
||||
page.title = "横向页面"
|
||||
// Row 沿横轴连接 Note;各 Note 保留自己的纵向 Column 测量/背景绘制覆盖。
|
||||
content = Row(48, 32, Note(24, 16, TextBlock("从左,走向右", 2), RectBlock(176, 180, 10, "#DDE8F1"), TextBlock("Row 内仍然可以包含 Column。"), "#EDF3F8"))
|
||||
// append 按顺序创建稳定子 slot,横向坐标由布局累加,不需要手工填写 x/y。
|
||||
content.append(Note(24, 16, TextBlock("收集", 2), RectBlock(176, 210, 10, "#D7E5DA"), TextBlock("一张卡片,一个想法。"), "#EFF4EC"))
|
||||
content.append(Note(24, 16, TextBlock("整理", 2), RectBlock(176, 170, 10, "#E5D9EB"), TextBlock("横向滚动,保留思考的顺序。"), "#F4EFF7"))
|
||||
content.append(Note(24, 16, TextBlock("继续", 2), RectBlock(176, 230, 10, "#EFDFC7"), TextBlock("Shift + 滚轮浏览更多内容。"), "#FAF3E8"))
|
||||
content.append(Note(24, 16, TextBlock("不止一屏", 2), TextBlock("右侧仍有空间。"), "#EEF1F5"))
|
||||
// 这些尺寸是构造缺省值,flex/stretch 会让视口接受当前窗口的实际可用空间。
|
||||
view = ScrollBlock(1024, 800, content)
|
||||
// Row 自然宽度可超出视口,只有横向滚动;卡片内部仍是 Column 的纵向排版。
|
||||
view.axis = "horizontal"
|
||||
// 保留横向滚动内容的自然宽度,视口本身仍参加有限页面的弹性布局。
|
||||
view.flex = 1
|
||||
page.crossAlign = "stretch"
|
||||
// 只安排展示,不在 click 回调里递归运行 measure/paint;worker 在回调结束后刷新。
|
||||
page.render(view)
|
||||
}
|
||||
// 重新调用工厂得到独立页面与相机,已有画布继续保留自己的视角。
|
||||
@@ -85,8 +106,10 @@ class StartActions {
|
||||
function counter(self: StartActions, event: Event): Unit {
|
||||
// 共享的是同一个托管状态对象;textOf 两次读取该对象 value,increment 更新后两个展示一起刷新。
|
||||
count = ui.state(0)
|
||||
// 按钮保留 increment 的绑定 owner,同一状态可同时驱动两个 textOf 派生视觉对象。
|
||||
add = Button("增加计数")
|
||||
add.onClick(count.increment)
|
||||
// 创建承载两个观察视图的新页面,状态对象不会因函数返回立即消失:绑定/回调仍持有它。
|
||||
page = Page()
|
||||
page.title = "共享对象"
|
||||
page.render(Column(48, 24, TextBlock("一个对象,两个视图", 2), Row(0, 32, ui.textOf(count, "value", 3), ui.textOf(count, "value", 3)), add))
|
||||
@@ -94,14 +117,21 @@ class StartActions {
|
||||
}
|
||||
// 页面工厂:先建立业务对象/动作,再建立世界组成,最后用一个有限视口显示它。
|
||||
function newTab(): Page {
|
||||
// OpenTool 的 New tab 与 StartActions.canvas 都调用这个工厂;每次都创建不同页面身份。
|
||||
page = Page()
|
||||
// 后续标签投影读此字段,不使用标题作为查找或去重依据。
|
||||
page.title = "自由画布"
|
||||
// 先建立引用当前 Page 的动作对象,再绑定按钮;回调中的 home 指向这一轮工厂创建的页面。
|
||||
actions = StartActions(page)
|
||||
// 每个按钮只保存动作入口,点击前不提前创建示例 Page。
|
||||
fixed = Button("窗口页面")
|
||||
// 绑定 vertical 回调后才会创建纵向滚动内容。
|
||||
vertical = Button("纵向延伸")
|
||||
// 横向示例和纵向示例各有独立 Page/ScrollBlock,互不共享滚动偏移。
|
||||
horizontal = Button("横向延伸")
|
||||
// 新画布动作再调用工厂,使相机导航状态与此画布隔离。
|
||||
canvas = Button("新的画布")
|
||||
// QuietButton 只隐藏原生按钮背景,其 click 事件和孩子文字仍沿普通 Button 协议工作。
|
||||
shared = QuietButton("试试:一个对象,两个视图 ↗")
|
||||
// 订阅绑定方法而非提前调用;事件触发后在原 VM 中创建/展示相应示例页。
|
||||
fixed.on("click", actions.blank)
|
||||
@@ -109,37 +139,45 @@ function newTab(): Page {
|
||||
horizontal.on("click", actions.horizontal)
|
||||
canvas.on("click", actions.canvas)
|
||||
shared.on("click", actions.counter)
|
||||
// hero 内部用有限行列排版,只有整个 hero 在世界场景中的摆放采用显式 x/y。
|
||||
hero = Column(0, 16, TextBlock("给想法,一点空间。", 3, "#303941"), TextBlock("页面可以是一扇窗口,也可以一直延伸。", 1, "#7D858C"), Spacer(8), Row(0, 10, fixed, vertical), Row(0, 10, horizontal, canvas), shared)
|
||||
// x/y 属于 Scene2D 子组件的世界位置;卡片内部依旧由普通 Row/Column 排版。
|
||||
hero.x = 316
|
||||
hero.y = 254
|
||||
// Scene2D 只规定孩子的世界位置;加入后仍可追加普通卡片,位置不受窗口宽高限制。
|
||||
world = Scene2D(hero)
|
||||
// 左侧卡片由 Note 测高并绘制 tint 背景,文字和矩形作为正常孩子分别绘制。
|
||||
left = Note(24, 14, TextBlock("随手记下", 2), RectBlock(176, 100, 10, "#D5E4DA"), TextBlock("想法不必排成一条直线。"), "#EDF3E9")
|
||||
left.x = 36
|
||||
left.y = 92
|
||||
// 将卡片以稳定 slot 加入场景;其 x/y 在前面单独设置,不依赖追加次序推算位置。
|
||||
world.append(left)
|
||||
// 顶部卡片只有文本,Note 仍复用内容测量,不需要为没有矩形的情况增加特殊宿主代码。
|
||||
top = Note(24, 14, TextBlock("四种页面,同一个世界"), TextBlock("窗口 · 纵向 · 横向 · 自由画布", 1, "#7D858C"), "#EFF1F7")
|
||||
top.x = 466
|
||||
top.y = 58
|
||||
world.append(top)
|
||||
// 右侧卡片展示“组件布局”和“相机视角”分开:移动相机不改写这个对象的世界坐标。
|
||||
right = Note(24, 14, RectBlock(176, 138, 12, "#DDDAEA"), TextBlock("把片段放在一起", 2), TextBlock("布局属于组件,视角属于摄像机。"), "#F1EDF5")
|
||||
right.x = 826
|
||||
right.y = 190
|
||||
world.append(right)
|
||||
// 底部说明中的换行属于源文本内容,不能把它误当实际控制输入或页面模式配置。
|
||||
bottom = Note(24, 14, TextBlock("不止眼前这一屏", 2), TextBlock("触控板或滚轮平移\n空格拖动 / 鼠标中键拖动\n双指捏合 / ⌘ Ctrl + 滚轮缩放"), "#F7F0E5")
|
||||
bottom.x = 104
|
||||
bottom.y = 524
|
||||
world.append(bottom)
|
||||
// 场景边缘再放一张普通 Note,展示超出初始屏幕范围的内容仍可通过相机漫游到达。
|
||||
end = Note(24, 14, TextBlock("继续向外探索", 2), RectBlock(176, 100, 10, "#CEDFE7"), TextBlock("画布上的对象仍是普通 Endor 组件。"), "#EAF1F5")
|
||||
end.x = 716
|
||||
end.y = 614
|
||||
world.append(end)
|
||||
// 视口参加有限布局并占用剩余空间;移动它的 Camera2D 不改写卡片世界位置。
|
||||
view = CanvasViewport(1024, 800, world)
|
||||
// 有限视口申请剩余空间,页面交叉轴拉伸,不把 Scene2D 改造成窗口尺寸的有限容器。
|
||||
view.flex = 1
|
||||
page.crossAlign = "stretch"
|
||||
page.render(view)
|
||||
// render 已登记/安排展示,返回 Page 给 OpenTool/调用者;返回值不是 GUI 线程的原生窗口对象。
|
||||
return page
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! 定义从托管对象提取的不可变视觉树。共享业务对象可以有多次展示,树节点与 Mount 出现的身份不是同一概念。
|
||||
//! 节点保存排版、绘制和协议投影信息;浅比较用于决定是否复用已有快照。
|
||||
//!
|
||||
//! Owned immutable visual derivation; no managed VM handles.
|
||||
//! model::snapshot/Block 快照提取构造 UiNode;render::layout/source_layout、绘制和命中流程读取它。
|
||||
//! 这里不持有 ObjectRef,因此 Arc 树可作为纯数据派生结果跨线程传递,不授权另一线程访问 VM。
|
||||
//! shallow_eq 由快照缓存判断能否复用旧 Arc;children/has_flex/allocates_children 供结构遍历和布局选择使用。
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -9,33 +11,46 @@ use std::sync::Arc;
|
||||
pub struct UiNode {
|
||||
// 只有页面根带兼容 Page 相机;其余字段是普通有限盒子的布局策略。
|
||||
pub surface: Option<crate::page::Surface>,
|
||||
// 兼容 flex 权重,父布局据此分配剩余主轴空间;不表示节点自身已经测量出的尺寸。
|
||||
pub flex: f32,
|
||||
// 显式 grow/shrink 等分配策略;与 flex 数值一起参与缓存相等性和父级分配判断。
|
||||
pub flex_policy: crate::layout::FlexPolicy,
|
||||
// 父容器主轴空白的对齐策略,与孩子排列次序和具体尺寸计算分开。
|
||||
pub main_align: crate::layout::MainAlign,
|
||||
// 容器为未指定 align_self 的孩子提供的交叉轴缺省策略。
|
||||
pub cross_align: crate::layout::CrossAlign,
|
||||
// 孩子对父 cross_align 的单独覆盖;None 表示继承,不能解释为禁止 stretch。
|
||||
pub align_self: Option<crate::layout::CrossAlign>,
|
||||
// 父布局分配时需要预留的外边距,不作为孩子内容绘制的本地原点偏移。
|
||||
pub margin: crate::layout::Insets,
|
||||
// 两轴的自然/固定/填充尺寸规则,由盒模型参与测量和最终安排。
|
||||
pub sizing: crate::box_model::Sizing,
|
||||
// 单边可选覆盖值;None 表示沿用 kind 的统一 padding,不代表边距为零。
|
||||
pub padding_edges: [Option<f32>; 4],
|
||||
pub border: crate::layout::Insets,
|
||||
// 留原始颜色字符串供显示列表解码;颜色变更也必须使 shallow_eq 为 false。
|
||||
pub border_color: String,
|
||||
// 是否把后代限制在内容可视区域内,绘制/命中需要采用同一剪裁关系。
|
||||
pub clip_children: bool,
|
||||
// 这些字符串只是派生身份/协议版本键,不是可以操作托管堆的 ObjectRef。
|
||||
pub block_id: String,
|
||||
// 源视觉协议相关的派生版本键;即使盒模型未变,协议变化仍可能要求重新测量或绘制。
|
||||
pub protocol_key: String,
|
||||
// slot 保持组成身份;数组次序确定当前绘制/布局顺序,二者不是同一概念。
|
||||
pub slots: Vec<u64>,
|
||||
// 从按钮、点击连接或自定义事件等能力推导的交互资格,供命中宽阶段使用,不代表最终 hitTest 必然成功。
|
||||
pub interactive: bool,
|
||||
// 标识原始 pointer 事件能力,与高层 click 的可交互标记分别参与输入分派。
|
||||
pub pointer_events: bool,
|
||||
// 具体叶/容器/场景内容,原生测量与软件绘制按此枚举选择规则。
|
||||
pub kind: UiKind,
|
||||
}
|
||||
|
||||
impl UiNode {
|
||||
/// 按节点种类返回其视觉子项;叶节点使用空切片,避免为遍历另建组成。
|
||||
/// 布局、快照和命中结构遍历调用,返回已有孩子切片,不分配新列表或递归复制子树。
|
||||
pub fn children(&self) -> &[Arc<UiNode>] {
|
||||
match &self.kind {
|
||||
// 这些种类都显式包含 children;三维面板 UI 也复用普通 UiNode 子树遍历。
|
||||
UiKind::Column { children, .. }
|
||||
| UiKind::Overlay { children, .. }
|
||||
| UiKind::Canvas { children, .. }
|
||||
@@ -45,11 +60,12 @@ impl UiNode {
|
||||
| UiKind::Row { children, .. }
|
||||
| UiKind::Button { children, .. }
|
||||
| UiKind::Scroll { children, .. } => children,
|
||||
// Text/Rect/Spacer 没有视觉子树;返回静态空切片,调用方无需按种类再分支。
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断是否需要对子项执行弹性主轴分配。
|
||||
/// 父布局/allocates_children 查询此孩子是否需要分配空间;判断的是节点需求,不会在此实际改尺寸。
|
||||
pub fn has_flex(&self) -> bool {
|
||||
// 不仅旧 flex 数值,非默认 FlexPolicy 或任一轴 Fill 也要求父节点执行分配,不能仅按自然尺寸排布。
|
||||
self.flex > 0.0
|
||||
@@ -58,17 +74,18 @@ impl UiNode {
|
||||
|| self.sizing.height == crate::box_model::Dimension::Fill
|
||||
}
|
||||
|
||||
/// 判断当前节点是否会为子项指定实际分配范围,供源布局选择安排路径。
|
||||
/// source_layout 选择安排路径时调用,检查任一孩子的弹性需求或交叉轴拉伸是否要求明确分配。
|
||||
pub fn allocates_children(&self) -> bool {
|
||||
// 任何孩子需要弹性或拉伸即进入分配路径;align_self 优先于父 cross_align。
|
||||
self.children().iter().any(|child| {
|
||||
// any 短路即可确定是否需要分配;align_self 优先级高于容器 cross_align。
|
||||
child.has_flex()
|
||||
|| child.align_self.unwrap_or(self.cross_align)
|
||||
== crate::layout::CrossAlign::Stretch
|
||||
})
|
||||
}
|
||||
|
||||
/// 比较节点自身配置和直接子项身份,决定能否复用旧 Arc;不是深拷贝业务图。
|
||||
/// model::snapshot 缓存重建节点后调用,判断可否保留旧 Arc;返回 true 不会修改任何节点或 VM 对象。
|
||||
pub fn shallow_eq(&self, other: &Self) -> bool {
|
||||
// 先比较所有公共视觉/协议配置,任一变化都不能复用旧节点 Arc,即使子内容完全相同。
|
||||
if self.surface != other.surface
|
||||
@@ -89,6 +106,7 @@ impl UiNode {
|
||||
|| self.interactive != other.interactive
|
||||
|| self.pointer_events != other.pointer_events
|
||||
{
|
||||
// 共有字段任一不同都足以否决复用,不再比较 kind 或递归访问后代。
|
||||
return false;
|
||||
}
|
||||
// 直接孩子按 Arc 指针比较而不是递归深比较;依赖提取保证未变子树复用 Arc,父级只需验证其身份。
|
||||
@@ -102,6 +120,7 @@ impl UiNode {
|
||||
};
|
||||
// 同类变体再比较其专有数据和直接子项;变体不同必为 false,避免布局种类切换后误复用缓存。
|
||||
match (&self.kind, &other.kind) {
|
||||
// a/b 与 d/e 是尺寸,c/f 是场景视图,g/h 是控制开关,i/j 是面板尺寸;孩子还需同身份。
|
||||
(
|
||||
UiKind::Viewport3D {
|
||||
width: a,
|
||||
@@ -120,6 +139,7 @@ impl UiNode {
|
||||
..
|
||||
},
|
||||
) => a == d && b == e && c == f && g == h && i == j && same_children(),
|
||||
// 二维视口除了宽高还比较相机快照;相机变了即使场景孩子没变也不能复用旧节点。
|
||||
(
|
||||
UiKind::CanvasViewport {
|
||||
width: a,
|
||||
@@ -135,14 +155,18 @@ impl UiNode {
|
||||
},
|
||||
) => a == d && b == e && c == f && same_children(),
|
||||
(UiKind::Scene2D { positions: a, .. }, UiKind::Scene2D { positions: b, .. }) => {
|
||||
// 显式世界位置和孩子身份一起决定组成,不能因同一批对象只移动位置而漏掉刷新。
|
||||
a == b && same_children()
|
||||
}
|
||||
(UiKind::Overlay { padding: a, .. }, UiKind::Overlay { padding: b, .. }) => {
|
||||
// Overlay 没有 gap/累计主轴位置,专有属性只比 padding,再检查孩子序列。
|
||||
a == b && same_children()
|
||||
}
|
||||
(UiKind::Canvas { positions: a, .. }, UiKind::Canvas { positions: b, .. }) => {
|
||||
// 兼容 Page Canvas 与 Scene2D 相同地保留显式位置,但仍要求两边变体相同。
|
||||
a == b && same_children()
|
||||
}
|
||||
// 文本叶没有孩子,内容、字号和颜色相等即可;共有字段已在上面验证。
|
||||
(
|
||||
UiKind::Text {
|
||||
value: a,
|
||||
@@ -155,6 +179,7 @@ impl UiNode {
|
||||
color: cb,
|
||||
},
|
||||
) => a == b && sa == sb && ca == cb,
|
||||
// 矩形依次比较宽、高、圆角和颜色,不以外框尺寸相同掩盖绘制变化。
|
||||
(
|
||||
UiKind::Rect {
|
||||
width: a,
|
||||
@@ -169,7 +194,9 @@ impl UiNode {
|
||||
color: h,
|
||||
},
|
||||
) => (a, b, c, d) == (e, f, g, h),
|
||||
// Spacer 唯一专有属性是占位长度。
|
||||
(UiKind::Spacer(a), UiKind::Spacer(b)) => a == b,
|
||||
// 滚动偏移属于 Mount,此处只比轴/有限尺寸/内容规则及孩子,避免共享节点携带单次出现偏移。
|
||||
(
|
||||
UiKind::Scroll {
|
||||
axis: aa,
|
||||
@@ -188,6 +215,7 @@ impl UiNode {
|
||||
children: _,
|
||||
},
|
||||
) => aa == ab && a == c && b == d && pa == pb && ga == gb && same_children(),
|
||||
// 这三种容器共用 padding/gap 比较,但模式配对仍只允许 Column 对 Column 等同种比较。
|
||||
(
|
||||
UiKind::Column {
|
||||
padding: a, gap: b, ..
|
||||
@@ -212,6 +240,7 @@ impl UiNode {
|
||||
padding: c, gap: d, ..
|
||||
},
|
||||
) => a == c && b == d && same_children(),
|
||||
// 任何不同变体都不能复用,哪怕专有数据恰巧具有相同数值。
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -222,27 +251,36 @@ impl UiNode {
|
||||
pub enum UiKind {
|
||||
// 有限 3D 视口包含不可变场景视图和面板 UI 孩子;panels 与 children/slots 按顺序对应。
|
||||
Viewport3D {
|
||||
// 有限布局盒子尺寸,不是三维世界的几何范围。
|
||||
width: f32,
|
||||
height: f32,
|
||||
// 已提取的场景、相机和投影数据,render 不需要为每个像素回读 VM。
|
||||
view: Arc<crate::scene3d::View>,
|
||||
// 是否存在可用控制器,输入宽阶段据此判断相机手势能力。
|
||||
controls: bool,
|
||||
// 各世界 UI 面板的逻辑尺寸,与 children 次序对应,供投影与布局共用。
|
||||
panels: Vec<crate::layout::Size>,
|
||||
// 面板 UI 的普通二维树;业务对象身份仍由 UiNode.block_id/slots 描述。
|
||||
children: Vec<Arc<UiNode>>,
|
||||
},
|
||||
// 二维世界组成保留孩子的显式位置,不使用行列的累计偏移定位。
|
||||
Scene2D {
|
||||
// 每个孩子的世界 x/y,数组下标与 children 对齐,不按叠放顺序推算位置。
|
||||
positions: Vec<[f32; 2]>,
|
||||
children: Vec<Arc<UiNode>>,
|
||||
},
|
||||
// 有限布局盒子观察独立 Scene2D,相机仅影响投影,不改变场景节点原始逻辑位置。
|
||||
CanvasViewport {
|
||||
// 参加父布局的有限盒子尺寸;不是 Scene2D 孩子的总包围盒。
|
||||
width: f32,
|
||||
height: f32,
|
||||
// 纯数据相机快照,只影响本视口的世界到屏幕映射,不改写原世界树。
|
||||
camera: crate::viewport::CameraView,
|
||||
children: Vec<Arc<UiNode>>,
|
||||
},
|
||||
// 孩子重叠布置,顺序同时决定叠放次序;普通有限布局与世界 Canvas 不同。
|
||||
Overlay {
|
||||
// 统一内边距,单边覆盖仍由 UiNode.padding_edges 提供。
|
||||
padding: f32,
|
||||
children: Vec<Arc<UiNode>>,
|
||||
},
|
||||
@@ -259,16 +297,21 @@ pub enum UiKind {
|
||||
},
|
||||
// 文字叶节点保留源字号等级与颜色文本;排版及最终设备比例由后续布局/显示列表转换处理。
|
||||
Text {
|
||||
// 已读取的文本内容,字体测量与软件绘制使用同一字符串。
|
||||
value: String,
|
||||
// 宿主字号等级,实际字体像素大小由 text/布局映射规则确定。
|
||||
size: u32,
|
||||
// 源颜色格式已在提取路径验证,绘制阶段再转成原生颜色值。
|
||||
color: String,
|
||||
},
|
||||
// 纯绘制叶节点;几何尺寸与圆角供布局/显示列表共用,不含托管对象句柄。
|
||||
Rect {
|
||||
width: f32,
|
||||
height: f32,
|
||||
radius: f32,
|
||||
color: String,
|
||||
},
|
||||
// Column 沿纵轴累加孩子位置,Row 沿横轴;二者都保存统一内边距和孩子间距。
|
||||
Column {
|
||||
padding: f32,
|
||||
gap: f32,
|
||||
@@ -283,6 +326,7 @@ pub enum UiKind {
|
||||
Spacer(f32),
|
||||
// 滚动盒保存有限分配尺寸、允许轴和普通内容;实际偏移按 Mount 出现分别维护,不放到共享节点里。
|
||||
Scroll {
|
||||
// axis 指定允许消费的滚动方向;实际可消费距离还取决于具体 Mount 的内容/视口范围。
|
||||
axis: crate::scroll::Axis,
|
||||
width: f32,
|
||||
height: f32,
|
||||
|
||||
Reference in New Issue
Block a user