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

161 lines
9.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 把内置开发工具和起始页接入托管 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 的流程。
// 搜索范围是会话全部节点而非 current 的标签;已关闭但仍被源引用的工具页也可复用,死亡弱项则跳过。
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 标记让宿主在该页面位置绘制对应管理工具。
// 工具原生内容不需要伪造 Block 子树;空 Page 仍沿正常窗口注册、标签投影及生命周期刷新路径展示。
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 安装时解析/编译一次;函数工厂之后重复执行不再重复编译模块。
// 模板之后必须 load 到传入的所属 VM;不同会话的类、闭包和私有 globals 不能复用另一 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
}