158 lines
8.6 KiB
Rust
158 lines
8.6 KiB
Rust
//! 把内置开发工具和起始页接入托管 Page。工厂在所属 VM 中编译和执行,而不是在窗口线程另建业务状态。
|
||
//!
|
||
//! 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 {
|
||
/// VM 在进入 invoke 前检查一个显式参数;这里只有数量检查,字符串类型与白名单由 invoke 检查。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(1))
|
||
}
|
||
/// 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,
|
||
arguments: vec![],
|
||
});
|
||
}
|
||
// 锁内只复制弱节点索引,之后逐个升级并查工具标记,不持 session 锁运行 show 的流程。
|
||
let nodes = self
|
||
.session
|
||
.lock()
|
||
.map_err(|_| invalid("UI session poisoned"))?
|
||
.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(
|
||
&cx.store()
|
||
.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)));
|
||
}
|
||
}
|
||
// 没有现存工具页才创建空托管 Page,_tesseraTool 标记让宿主在该页面位置绘制对应管理工具。
|
||
let page = new_typed_block(
|
||
cx,
|
||
Data::new(Kind::Page, vec![], 0.0, 0.0),
|
||
&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 {
|
||
session: session.clone(),
|
||
ui,
|
||
},
|
||
vec![ui],
|
||
))?;
|
||
// 函数 captures 保活 ui;安装属性建立拥有边后释放局部分配根,避免额外常驻根。
|
||
vm.set_attr(ui, "developerTools", Value::Object(function))?;
|
||
vm.release(function)?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 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,
|
||
&compiler::CompileOptions {
|
||
// 显式声明模块可见的 ui 与宿主类型名;私有 globals 不污染用户主会话的命名空间。
|
||
globals: std::iter::once("ui")
|
||
.chain(TYPE_NAMES.iter().copied())
|
||
.map(str::to_owned)
|
||
.collect(),
|
||
},
|
||
)
|
||
.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(
|
||
code,
|
||
globals,
|
||
endor_core::ExecutionLimits {
|
||
max_frames: 128,
|
||
max_instructions: 100_000,
|
||
},
|
||
);
|
||
vm.release(code)?;
|
||
vm.release_value(value?)?;
|
||
// 要求模块导出 newTab,将函数保存到 ui 上;该函数通过托管引用维持自己的私有全局环境。
|
||
let factory = vm
|
||
.dict_get(globals, &Value::Text("newTab".into()))?
|
||
.ok_or_else(|| anyhow::anyhow!("start page module requires newTab"))?;
|
||
vm.set_attr(bridge.ui, "newTab", factory)?;
|
||
Ok(())
|
||
})();
|
||
// 无论编译代码执行或导出查找成功与否,释放私有字典的局部根,再返回保存的处理结果。
|
||
vm.release(globals)?;
|
||
result
|
||
}
|