Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fedd21d3c | |||
| 4c3fc8b5dd |
@@ -0,0 +1,116 @@
|
||||
# Tessera 中文源码注释与阅读索引
|
||||
|
||||
本轮只为 Tessera 主项目补充注释,Aster 子模块固定版本与内容均沿用主线。注释按模块职责、类型/函数入口、关键字段、执行阶段和失败/取消路径组织;没有改变接口、算法、常量或资源预算。
|
||||
|
||||
## 建议阅读顺序
|
||||
|
||||
1. [main](../src/main.rs):创建原生循环、共享文档与执行 worker。
|
||||
2. [原生应用](../src/shell/app.rs):理解窗口生命周期、可用视口、输入边界和最终 present。
|
||||
3. [API worker](../src/runtime/api/worker.rs) 与 [运行时](../src/runtime/endor_host.rs):理解候选/活跃 VM、输入串行化、分片刷新和发布。
|
||||
4. [桥接层](../src/bridge/ui_bridge.rs)、[Block](../src/model/block.rs) 与 [Mount](../src/model/mount.rs):区分共享业务对象、每次出现和展示事务。
|
||||
5. [快照](../src/model/snapshot.rs)、[源布局](../src/render/source_layout.rs) 和 [源绘制](../src/render/source_paint.rs):理解依赖缓存、只读执行及挂起工作。
|
||||
6. [二维视口](../src/render/viewport.rs)、[3D](../src/render/scene3d.rs) 和 [面板映射](../src/render/scene3d/panel.rs):理解布局/世界/视图的边界。
|
||||
7. [任务](../src/runtime/tasks.rs)、[provider](../src/runtime/io_tasks.rs) 和 [事件传播](../src/runtime/event_dispatch.rs):理解异步交付资格与生命周期。
|
||||
|
||||
## 阅读时需要保留的边界
|
||||
|
||||
- Block/Data 属于共享业务组成;Mount 属于一个展示位置,不能把一次出现的尺寸写回共享对象来代替排版。
|
||||
- ObjectRef/WeakObjectRef 的含义由真实托管拥有关系决定;派生缓存中的身份索引不能成为隐藏根。
|
||||
- 窗口线程消费完整发布值;VM、源回调和托管堆操作只在执行 worker 内推进。
|
||||
- submission 防止旧请求覆盖新输入;session 防止跨 VM 资源误用;layout_version 防止旧几何上的离散操作误触。
|
||||
- Pending Work 同时拥有续体与上下文;取消必须关闭原 VM 的只读执行并恢复未提交 Mount,不能只丢弃 Rust 值。
|
||||
- 场景世界不是无限尺寸的排版盒子;有限视口参加布局,摄像机负责投影,命中沿相同链逆变换。
|
||||
- 现有 CPU 绘制、缓存范围与输入队列限制仍按当前代码执行;说明性注释不代表性能优化已实现。
|
||||
|
||||
## Rust 逐文件说明
|
||||
|
||||
每行说明该文件本次新增注释解释的职责和目的。生产函数/方法均增加中文入口说明;复杂状态机另外补充阶段和不变量说明。集成测试文件增加整体阅读与检查点说明,测试程序文本和源码范围断言保持原样。
|
||||
|
||||
| 文件 | 注释解释的职责与目的 |
|
||||
| --- | --- |
|
||||
| [src/bridge/mod.rs](../src/bridge/mod.rs) | 语言桥接模块入口。集中导出 Endor 类型和宿主实现之间的适配层。 |
|
||||
| [src/bridge/ui_bridge.rs](../src/bridge/ui_bridge.rs) | Endor 与 Tessera 的展示事务协调层:维护每个窗口的 Page、Mount、布局缓存和发布版本。 |
|
||||
| [src/bridge/ui_bridge/bootstrap.rs](../src/bridge/ui_bridge/bootstrap.rs) | 安装一次 VM 域需要的宿主类型、对象和方法。类型注册表是源代码调用原生能力的入口。 |
|
||||
| [src/bridge/ui_bridge/calls.rs](../src/bridge/ui_bridge/calls.rs) | 把 Endor 方法调用分派到对应宿主操作。这里处理构造、树修改、绑定、状态与页面能力。 |
|
||||
| [src/bridge/ui_bridge/developer_pages.rs](../src/bridge/ui_bridge/developer_pages.rs) | 把内置开发工具和起始页接入托管 Page。工厂在所属 VM 中编译和执行,而不是在窗口线程另建业务状态。 |
|
||||
| [src/bridge/ui_bridge/page_input.rs](../src/bridge/ui_bridge/page_input.rs) | 把页面、2D 视口和 3D 视口的相机输入转换成托管对象更新。 |
|
||||
| [src/bridge/ui_bridge/signatures.rs](../src/bridge/ui_bridge/signatures.rs) | 声明宿主方法在 Endor 类型系统中的参数和返回契约。 |
|
||||
| [src/bridge/ui_bridge/tree.rs](../src/bridge/ui_bridge/tree.rs) | 定义从托管对象提取的不可变视觉树。共享业务对象可以有多次展示,树节点与 Mount 出现的身份不是同一概念。 |
|
||||
| [src/main.rs](../src/main.rs) | 应用组合入口。先创建原生事件循环和共享发布状态,再启动 API/VM worker,最后把窗口事件交给 TesseraApp。 |
|
||||
| [src/model/binding.rs](../src/model/binding.rs) | 属性绑定:在只读计算中收集依赖,将结果写回绑定目标。 |
|
||||
| [src/model/block.rs](../src/model/block.rs) | Block 的共享赋值、底层 Data 与视觉属性实现。Block 通过 Cell 间接指向 Data,使别名能观察到同一次替换。 |
|
||||
| [src/model/document.rs](../src/model/document.rs) | 运行时与窗口线程之间的发布数据契约。NamespaceState 管理提交序号、版本和当前完整文档。 |
|
||||
| [src/model/mod.rs](../src/model/mod.rs) | 模型层模块入口。包含托管业务对象、每次展示上下文、依赖缓存,以及交给窗口线程的发布值。 |
|
||||
| [src/model/mount.rs](../src/model/mount.rs) | 每次展示的托管上下文:同一个 Block 出现在两个位置时,有两个独立 Mount。 |
|
||||
| [src/model/object_list.rs](../src/model/object_list.rs) | 给源协议返回的对象列表提供受控封装。列表内容通过托管引用被追踪,读方法只暴露检查后的条目。 |
|
||||
| [src/model/presentation.rs](../src/model/presentation.rs) | 一个原生窗口对应一份 Presentation:独立视口、Mount、测量缓存、焦点与依赖记录。 |
|
||||
| [src/model/protocols.rs](../src/model/protocols.rs) | 安装 Visual、EventTarget、Children 和 Presentable 等能力契约。 |
|
||||
| [src/model/scene3d.rs](../src/model/scene3d.rs) | 将 Endor 托管场景对象转换成可供渲染线程读取的 3D 数据。 |
|
||||
| [src/model/snapshot.rs](../src/model/snapshot.rs) | 从可变托管对象图提取不可变 UiNode,并维护反向依赖与父子失效传播。 |
|
||||
| [src/model/window.rs](../src/model/window.rs) | 托管 Window 的页面所有权、原生命令与关闭生命周期。 |
|
||||
| [src/render/box_model.rs](../src/render/box_model.rs) | 有限布局盒子的尺寸策略。区分 auto、固定值和 fill,并处理 min/max 对约束与最终尺寸的限制。 |
|
||||
| [src/render/display_list.rs](../src/render/display_list.rs) | 面向设备像素的绘制指令集合。它承载矩形、文字、裁剪与 3D 表面等原语,不直接操作 VM。 |
|
||||
| [src/render/geometry.rs](../src/render/geometry.rs) | 共享的二维点和矩形值。命中判断在调用方完成坐标系转换后使用,矩形本身不隐式处理相机。 |
|
||||
| [src/render/layout.rs](../src/render/layout.rs) | 原生布局求解器:自底向上测量期望尺寸,再自顶向下分配位置和实际尺寸。 |
|
||||
| [src/render/mod.rs](../src/render/mod.rs) | 渲染模块入口。排版、源视觉协议、相机投影和最终光栅化各自独立,通过不可变帧衔接。 |
|
||||
| [src/render/native_visual.rs](../src/render/native_visual.rs) | 让原生组件也通过 Endor 的 measure/arrange/paint 协议被调用。 |
|
||||
| [src/render/page.rs](../src/render/page.rs) | 旧 Page.mode 的兼容空间策略,以及页面级的正逆相机变换。 |
|
||||
| [src/render/scene.rs](../src/render/scene.rs) | 合成整个原生窗口的显示列表:页面画面、宿主侧栏和开发工具绘制。 |
|
||||
| [src/render/scene3d.rs](../src/render/scene3d.rs) | 有界 CPU 3D 渲染器。使用相同摄像机与世界变换进行视锥裁剪、深度测试、纹理采样和射线拾取。 |
|
||||
| [src/render/scene3d/math.rs](../src/render/scene3d/math.rs) | 3D 数学基础:向量、仿射矩阵、摄像机基向量、投影射线和三角形相交。 |
|
||||
| [src/render/scene3d/panel.rs](../src/render/scene3d/panel.rs) | 连接 3D 平面与普通 UI 的坐标和绘制桥梁。 |
|
||||
| [src/render/scene3d/spatial.rs](../src/render/scene3d/spatial.rs) | 不可变世界空间 BVH。用网格变换后的包围盒快速筛选视锥与射线候选。 |
|
||||
| [src/render/scroll.rs](../src/render/scroll.rs) | 逐 Mount 的滚动状态、裁剪和输入目标选择。 |
|
||||
| [src/render/software_renderer.rs](../src/render/software_renderer.rs) | 把 DisplayList 写入 CPU 像素缓冲。裁剪栈限定矩形、文字和 3D 表面的实际写入区域。 |
|
||||
| [src/render/source_hit.rs](../src/render/source_hit.rs) | 通过真正的源/原生 hitTest 协议确定指针目标。 |
|
||||
| [src/render/source_layout.rs](../src/render/source_layout.rs) | 源 measure/arrange 协议的分片布局驱动器。自底向上的测量与自顶向下的安排共享一个只读 VM 调用域。 |
|
||||
| [src/render/source_paint.rs](../src/render/source_paint.rs) | 执行源 paint 覆盖并解码受检查的 PaintCommand。 |
|
||||
| [src/render/text.rs](../src/render/text.rs) | 统一字体发现、文字整形、测量与抗锯齿字形绘制。 |
|
||||
| [src/render/viewport.rs](../src/render/viewport.rs) | 独立 2D 相机与可逆投影。视口尺寸属于布局;平移、缩放属于视图,不改变世界对象的逻辑位置。 |
|
||||
| [src/render/viewport/spatial.rs](../src/render/viewport/spatial.rs) | 有界屏幕网格,用来减少普通二维命中和相机目标查询的候选。 |
|
||||
| [src/runtime/api.rs](../src/runtime/api.rs) | 本机 HTTP API 入口:路由请求、限制输入大小、观察活动,并将执行工作送入串行 worker。 |
|
||||
| [src/runtime/api/guide.rs](../src/runtime/api/guide.rs) | 从当前命名空间生成 /agent 使用指南。返回的示例、地址和版本描述来自运行时上下文;这里生成文档,不执行用户源码。 |
|
||||
| [src/runtime/api/tests.rs](../src/runtime/api/tests.rs) | API 集成回归。通过实际本地 HTTP 请求检查队列、候选替换、状态码、版本和完整文档发布。 |
|
||||
| [src/runtime/api/worker.rs](../src/runtime/api/worker.rs) | 唯一串行执行 VM 的 API worker。保留活跃运行时,另建 PUT 候选,并在候选完整成功后切换发布。 |
|
||||
| [src/runtime/endor_host.rs](../src/runtime/endor_host.rs) | 把 Aster 编译/执行与 Tessera 展示生命周期连接起来。Candidate 拥有 VM 与宿主根,ReplSession 保留实际运行中的全局环境。 |
|
||||
| [src/runtime/endor_host/tests.rs](../src/runtime/endor_host/tests.rs) | 运行时集成回归与示例验收。测试经真实 Aster 前端、编译器、VM 和展示协议执行,不是手工伪造布局结果。 |
|
||||
| [src/runtime/endor_script.rs](../src/runtime/endor_script.rs) | 区分完整 Module 与顶层 Script,并保留输入源码位置。 |
|
||||
| [src/runtime/event_dispatch.rs](../src/runtime/event_dispatch.rs) | 托管事件与订阅的传播实现。Connection 持有回调引用,Event 保存目标、阶段与停止传播状态。 |
|
||||
| [src/runtime/events.rs](../src/runtime/events.rs) | 原生窗口输入到执行 worker 的纯数据传输。Input 只含窗口/会话/版本/Mount 身份及有限坐标。 |
|
||||
| [src/runtime/io_tasks.rs](../src/runtime/io_tasks.rs) | 后台文件/网络 provider。线程池只携带纯数据、取消控制与弱 Task 身份,不持有 VM。 |
|
||||
| [src/runtime/mod.rs](../src/runtime/mod.rs) | 运行时模块入口。VM 生命周期、API、源程序组织、事件与异步任务在这里组合。 |
|
||||
| [src/runtime/tasks.rs](../src/runtime/tasks.rs) | owner/key 管理的异步任务与协作式源函数调度。弱索引用于查找,回调、等待对象和协程帧是托管图中的真实边。 |
|
||||
| [src/runtime/vm_sequence.rs](../src/runtime/vm_sequence.rs) | 构造共享 VM 调用序列与同帧循环驱动的字节码。 |
|
||||
| [src/shell/activity.rs](../src/shell/activity.rs) | 有界 HTTP 活动记录。保存请求/响应的可显示预览供 Network 工具读取,不负责执行请求。 |
|
||||
| [src/shell/app.rs](../src/shell/app.rs) | winit 原生应用生命周期和窗口事件入口。这里创建窗口、发布逻辑视口、转发输入,并把不可变文档画到原生表面。 |
|
||||
| [src/shell/clipboard.rs](../src/shell/clipboard.rs) | 把 UTF-8 文本读写适配到当前平台的系统剪贴板命令。 |
|
||||
| [src/shell/developer_tools.rs](../src/shell/developer_tools.rs) | Console/Sources/Network/Settings 的宿主交互协调与绘制状态。 |
|
||||
| [src/shell/developer_tools/console.rs](../src/shell/developer_tools/console.rs) | 绘制 Console 输入、结果历史和对应命中区域。 |
|
||||
| [src/shell/developer_tools/network.rs](../src/shell/developer_tools/network.rs) | 绘制已观察到的 HTTP 活动、请求列表与详情。 |
|
||||
| [src/shell/developer_tools/painter.rs](../src/shell/developer_tools/painter.rs) | 开发工具画面的绘制辅助:文字行、按钮、分隔线和滚动条。 |
|
||||
| [src/shell/developer_tools/selection.rs](../src/shell/developer_tools/selection.rs) | 多行文本选择和光标导航。选择端点映射到合法字符边界,避免把 UTF-8 字节中间当成一个字符。 |
|
||||
| [src/shell/developer_tools/settings.rs](../src/shell/developer_tools/settings.rs) | 绘制宿主设置页面和主题选择区域。 |
|
||||
| [src/shell/developer_tools/sources.rs](../src/shell/developer_tools/sources.rs) | 绘制 AST、当前源码快照和云文件视图。 |
|
||||
| [src/shell/mod.rs](../src/shell/mod.rs) | 原生宿主模块入口。窗口、管理控件、系统适配和开发工具状态与用户业务界面分开维护。 |
|
||||
| [src/shell/native_chrome.rs](../src/shell/native_chrome.rs) | macOS 原生标题栏适配。定位系统窗口按钮并沿用用户的标题栏双击设置。 |
|
||||
| [src/shell/sidebar.rs](../src/shell/sidebar.rs) | 宿主管理工具和页面列表的唯一侧栏。展开时占据页面左侧空间,折叠后只保留展开按钮区域。 |
|
||||
| [src/shell/sources.rs](../src/shell/sources.rs) | 云文件浏览的 provider 抽象与工作区状态。默认 provider 未连接,不假装已有外部文件。 |
|
||||
| [src/shell/system_ui/interaction.rs](../src/shell/system_ui/interaction.rs) | 即时绘制控件的跨帧交互状态:稳定 WidgetId、悬停、按下捕获、键盘焦点和释放激活。 |
|
||||
| [src/shell/system_ui/mod.rs](../src/shell/system_ui/mod.rs) | 宿主管理控件的共享组件入口。主题、绘制原语和跨帧交互状态分离,用户 Endor 页面不依赖这里的控件树。 |
|
||||
| [src/shell/system_ui/theme.rs](../src/shell/system_ui/theme.rs) | 宿主控件的颜色与状态调色。根据背景、强调色和交互状态生成可读颜色,不改变业务状态。 |
|
||||
| [src/shell/system_ui/widgets.rs](../src/shell/system_ui/widgets.rs) | 用统一主题绘制宿主管理控件,并同步注册交互目标。 |
|
||||
| [src/shell/tabs.rs](../src/shell/tabs.rs) | 发布页面标签的数据结构。保留稳定页面身份、标题、选中状态和开发工具类别;页面的托管所有权由模型层管理。 |
|
||||
|
||||
## 其他文件
|
||||
|
||||
| 文件 | 本次改动及目的 |
|
||||
| --- | --- |
|
||||
| [src/bridge/ui_bridge/start_page.endor](../src/bridge/ui_bridge/start_page.endor) | 解释卡片源测量/背景、页面工厂、纵横布局与滚动组合、共享计数和场景位置,便于从真实 Endor 页面理解宿主边界。 |
|
||||
| [scripts/build-macos-app.sh](../scripts/build-macos-app.sh) | 说明本地构建前提、锁定依赖、临时文件替换和 bundle 元数据,帮助理解每个打包步骤。 |
|
||||
| [scripts/render-uml.mjs](../scripts/render-uml.mjs) | 说明独立文档依赖、DOM/Canvas 适配、错误图检查与 SVG/PNG 产物生成,避免混淆运行依赖与文档工具。 |
|
||||
| [src/README.md](../src/README.md) | 增加本注释索引入口,便于从源码目录开始阅读。 |
|
||||
| [docs/code-comments.md](code-comments.md) | 本逐文件说明与阅读顺序,覆盖本轮提交的文件及共同边界。 |
|
||||
|
||||
## 核对方式
|
||||
|
||||
检查差异仅新增独立注释行和注释相关空白,并与主线去除注释后的文本对比;同时运行格式、完整 GUI 回归、Clippy 和包含私有项的 rustdoc。脚本使用语法检查,不执行文档渲染或应用打包。具体检查结果在 PR 中记录。
|
||||
@@ -1,18 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Native macOS build and app bundle. No Docker, mirror configuration, or tests.
|
||||
set -eu
|
||||
# 此入口只能在 macOS 使用;构建授权由调用方确认,脚本不自动执行测试或部署。
|
||||
if [ "$(uname -s)" != Darwin ]; then
|
||||
echo "Build this app bundle on macOS." >&2
|
||||
exit 1
|
||||
fi
|
||||
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
# 锁定依赖并生成 release 可执行文件;附加参数原样交给 Cargo。
|
||||
cargo build --release --locked "$@"
|
||||
version=$(awk -F '"' '$1 ~ /^version[[:space:]]*=/ { print $2; exit }' Cargo.toml)
|
||||
test -n "$version"
|
||||
bundle="$(pwd)/target/release/Tessera.app"
|
||||
mkdir -p "$bundle/Contents/MacOS"
|
||||
# 先写临时文件再重命名,避免复制途中将半个二进制留作 bundle 的主程序。
|
||||
cp target/release/tessera "$bundle/Contents/MacOS/tessera.new"
|
||||
mv -f "$bundle/Contents/MacOS/tessera.new" "$bundle/Contents/MacOS/tessera"
|
||||
# 生成最小 macOS bundle 元数据;应用名、入口与高分辨率标志供系统启动/显示使用。
|
||||
cat > "$bundle/Contents/Info.plist.new" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
|
||||
@@ -7,6 +7,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const tools = process.env.UML_RENDER_TOOLS;
|
||||
// 工具依赖放在独立目录,避免给应用运行依赖增加文档渲染包。
|
||||
if (!tools) throw new Error('Set UML_RENDER_TOOLS to the documentation tool directory; see docs/architecture/uml/README.md');
|
||||
const require = createRequire(path.join(path.resolve(tools), 'package.json'));
|
||||
const { createCanvas } = require('@napi-rs/canvas');
|
||||
@@ -23,6 +24,7 @@ const document = new DOMImplementation().createDocument(null, 'root', null);
|
||||
const element = document.createElement.bind(document);
|
||||
const elementNS = document.createElementNS.bind(document);
|
||||
function measurementCanvas() {
|
||||
// PlantUML 的文本布局需要真实字体宽高,统一用本机 Canvas 测量而非固定字符宽度。
|
||||
const canvas = createCanvas(1, 1);
|
||||
const context = canvas.getContext('2d');
|
||||
const measure = context.measureText.bind(context);
|
||||
@@ -41,6 +43,7 @@ function measurementCanvas() {
|
||||
}
|
||||
document.createElement = tag => tag === 'canvas' ? measurementCanvas() : element(tag);
|
||||
document.createElementNS = (namespace, tag) => {
|
||||
// 为无浏览器 DOM 补齐样式与 getBBox,满足官方渲染引擎的 SVG 文本布局需求。
|
||||
const node = elementNS(namespace, tag);
|
||||
node.style = {};
|
||||
node.getBBox = () => {
|
||||
@@ -65,6 +68,7 @@ const source = path.join(root, 'docs/architecture/uml');
|
||||
const output = path.join(source, 'rendered');
|
||||
await mkdir(output, { recursive: true });
|
||||
for (const filename of (await readdir(source)).filter(name => name.endsWith('.puml')).sort()) {
|
||||
// 每张图独立渲染并检查错误文本;错误图不能被当作正常产物覆盖已有预览。
|
||||
const text = await readFile(path.join(source, filename), 'utf8');
|
||||
const rendered = await new Promise((resolve, reject) => renderToString(text.split('\n'), resolve, reject));
|
||||
const xml = new DOMParser().parseFromString(rendered, 'image/svg+xml');
|
||||
@@ -77,6 +81,7 @@ for (const filename of (await readdir(source)).filter(name => name.endsWith('.pu
|
||||
for (const [key, value] of Object.entries({ x: '0', y: '0', width: '100%', height: '100%', fill: '#FFFFFF' })) paper.setAttribute(key, value);
|
||||
xml.documentElement.insertBefore(paper, xml.documentElement.firstChild);
|
||||
const svg = new XMLSerializer().serializeToString(xml);
|
||||
// 同时保存可缩放 SVG 和通用 PNG;写完读取 PNG 元数据确认生成尺寸。
|
||||
const stem = filename.slice(0, -5);
|
||||
await writeFile(path.join(output, `${stem}.svg`), svg);
|
||||
// High-resolution PNG is the universally visible Markdown preview; SVG keeps zoomable text.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Tessera 源码导航
|
||||
|
||||
本体代码已补充中文模块、类型、函数与关键流程注释;建议按[注释阅读索引和逐文件说明](../docs/code-comments.md)从启动、运行时发布和视觉协议依次阅读。
|
||||
|
||||
整体设计和当前进展从 [架构导览](../docs/architecture/README.md) 开始;本轮每个文件的迁移对应见 [逐文件清单](../docs/architecture/file-map.md)。
|
||||
|
||||
`main.rs` 只负责组合启动,并保留 crate 私有模块路径别名。五组目录仍在同一个 binary crate 中,不是严格单向分层。
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
//! 语言桥接模块入口。集中导出 Endor 类型和宿主实现之间的适配层。
|
||||
//!
|
||||
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
|
||||
pub(crate) mod ui_bridge;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! Endor 与 Tessera 的展示事务协调层:维护每个窗口的 Page、Mount、布局缓存和发布版本。
|
||||
//! Session 保存托管对象的展示上下文;跨线程发布的是不可变 UiNode/Frame,而不是可以在窗口线程执行的 VM。
|
||||
//! 刷新按 prepare → layout → paint → commit 推进。挂起工作必须恢复或显式取消;失败不能把未完成的几何和生命周期状态留作已发布结果。
|
||||
//!
|
||||
//! Tessera's native visual protocols over Aster's managed object graph.
|
||||
mod bootstrap;
|
||||
mod calls;
|
||||
@@ -90,6 +94,7 @@ pub const QUERY_NAMES: &[&str] = &[
|
||||
pub use tree::{UiKind, UiNode};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 整个 VM 的桥接会话。节点索引跨窗口共享,presentations 按窗口隔离派生状态;current 决定方法当前访问哪份展示上下文。
|
||||
struct Session {
|
||||
// Weak visual-object registry belongs to the VM, not a particular window.
|
||||
nodes: Vec<WeakObjectRef>,
|
||||
@@ -109,6 +114,7 @@ struct Session {
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// 收集所有窗口的私有 desktop 身份,用于排除宿主记账读取造成的无关布局失效。
|
||||
fn host_desktops(&self) -> std::collections::HashSet<ObjectRef> {
|
||||
self.presentations
|
||||
.values()
|
||||
@@ -119,6 +125,7 @@ impl Session {
|
||||
|
||||
impl std::ops::Deref for Session {
|
||||
type Target = crate::presentation::Presentation;
|
||||
/// 将包装类型的只读访问转交给内部当前上下文。
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.presentations
|
||||
.get(&self.current)
|
||||
@@ -126,6 +133,7 @@ impl std::ops::Deref for Session {
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for Session {
|
||||
/// 将包装类型的可变访问转交给内部当前上下文。
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.presentations
|
||||
.get_mut(&self.current)
|
||||
@@ -135,6 +143,7 @@ impl std::ops::DerefMut for Session {
|
||||
type Shared = Arc<Mutex<Session>>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// ui/组件宿主入口的操作枚举;统一由调用适配器分派,不直接代表已经发生的业务变更。
|
||||
enum Operation {
|
||||
IsInstance,
|
||||
Implement,
|
||||
@@ -202,6 +211,7 @@ enum Operation {
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(self) -> endor_core::CallArity {
|
||||
use Operation::*;
|
||||
let (minimum, maximum) = match self {
|
||||
@@ -238,6 +248,7 @@ impl Operation {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 将一个宿主 Operation 及会话环境封装成 VM 可调用对象。
|
||||
struct Function {
|
||||
signature: Option<CallSignature>,
|
||||
operation: Operation,
|
||||
@@ -249,6 +260,7 @@ struct Function {
|
||||
/// One shared implementation on the nominal type. The VM binds receivers;
|
||||
/// individual Blocks no longer allocate owner-capturing method closures.
|
||||
#[derive(Debug)]
|
||||
/// 类型查询/能力方法的原生包装,保存调用所需的声明与会话。
|
||||
struct TypeMethod {
|
||||
signature: CallSignature,
|
||||
operation: Operation,
|
||||
@@ -256,6 +268,7 @@ struct TypeMethod {
|
||||
session: Shared,
|
||||
}
|
||||
|
||||
/// 判断方法是否仍为宿主提供的原生事件实现,区分它与用户源覆盖。
|
||||
pub(crate) fn is_intrinsic_event(
|
||||
store: &endor_core::ObjectStore,
|
||||
function: ObjectRef,
|
||||
@@ -274,9 +287,11 @@ pub(crate) fn is_intrinsic_event(
|
||||
}
|
||||
|
||||
impl NativeCallable for TypeMethod {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
let arity = self.operation.arity();
|
||||
Some(endor_core::CallArity::range(
|
||||
@@ -285,6 +300,7 @@ impl NativeCallable for TypeMethod {
|
||||
))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let owner = object(args.first())?;
|
||||
if !cx.store().is_instance(owner, self.expected)? {
|
||||
@@ -304,6 +320,7 @@ impl NativeCallable for TypeMethod {
|
||||
/// The Type dispatcher passes the requested subclass explicitly. Native
|
||||
/// constructor options precede the subclass's inherited/own field arguments.
|
||||
#[derive(Debug)]
|
||||
/// 为源代码调用原生类型构造器提供受检查的分配入口。
|
||||
struct NativeFactory {
|
||||
signature: Option<CallSignature>,
|
||||
options: Option<CallSignature>,
|
||||
@@ -312,9 +329,11 @@ struct NativeFactory {
|
||||
}
|
||||
|
||||
impl NativeCallable for NativeFactory {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
self.signature.clone()
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::range(
|
||||
self.operation.arity().required + 1,
|
||||
@@ -322,6 +341,7 @@ impl NativeCallable for NativeFactory {
|
||||
))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let requested = object(args.first())?;
|
||||
let fields = cx
|
||||
@@ -367,21 +387,25 @@ impl NativeCallable for NativeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||||
fn object(value: Option<&Value>) -> Result<ObjectRef, VmError> {
|
||||
match value {
|
||||
Some(Value::Object(value)) => Ok(*value),
|
||||
_ => Err(invalid("expected an object")),
|
||||
}
|
||||
}
|
||||
/// 把当前源值或文本选择解码成此调用需要的文本表示。
|
||||
fn text(value: Option<&Value>) -> Result<String, VmError> {
|
||||
match value {
|
||||
Some(Value::Text(value)) => Ok(value.clone()),
|
||||
_ => Err(invalid("expected text")),
|
||||
}
|
||||
}
|
||||
/// 读取整数参数,拒绝非整数或无法在目标范围表示的值。
|
||||
fn integer(value: Option<&Value>) -> Result<u64, VmError> {
|
||||
match value {
|
||||
Some(Value::Number(value)) => value
|
||||
@@ -391,6 +415,7 @@ fn integer(value: Option<&Value>) -> Result<u64, VmError> {
|
||||
_ => Err(invalid("expected a nonnegative integer")),
|
||||
}
|
||||
}
|
||||
/// 解析颜色值并检查其满足当前原生绘制约定。
|
||||
fn color(value: Option<&Value>, default: &str) -> Result<String, VmError> {
|
||||
let value = value
|
||||
.map(|v| text(Some(v)))
|
||||
@@ -399,6 +424,7 @@ fn color(value: Option<&Value>, default: &str) -> Result<String, VmError> {
|
||||
parse_color(&value).map_err(|_| invalid("color must use #RRGGBB or #RRGGBBAA"))?;
|
||||
Ok(value)
|
||||
}
|
||||
/// 读取布局尺寸参数,检查非负、有限和最大尺寸限制。
|
||||
fn dimension(value: Option<&Value>) -> Result<f32, VmError> {
|
||||
let Some(Value::Number(number)) = value else {
|
||||
return Err(invalid("expected a finite nonnegative dimension"));
|
||||
@@ -414,6 +440,7 @@ fn dimension(value: Option<&Value>) -> Result<f32, VmError> {
|
||||
Ok(value as f32)
|
||||
}
|
||||
|
||||
/// 为一个托管对象安装绑定到当前会话的原生方法,声明调用名与具体 Operation。
|
||||
fn bind(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -440,6 +467,7 @@ fn bind(
|
||||
cx.set_attr(owner, name, Value::Object(function))
|
||||
}
|
||||
|
||||
/// 使用默认原生类型创建 Block;需要源子类身份的构造走 new_typed_block。
|
||||
fn new_block(
|
||||
cx: &mut NativeContext<'_>,
|
||||
data: Data,
|
||||
@@ -448,6 +476,7 @@ fn new_block(
|
||||
new_typed_block(cx, data, session, None)
|
||||
}
|
||||
|
||||
/// 分配有名义类型和默认位置/缩放的独立相机,供新视口拥有自己的导航状态。
|
||||
fn new_camera2d(
|
||||
cx: &mut NativeContext<'_>,
|
||||
session: &Shared,
|
||||
@@ -473,6 +502,7 @@ fn new_camera2d(
|
||||
Ok(camera)
|
||||
}
|
||||
|
||||
/// 创建原生 Block 并附加请求的 Endor 类型;先验证继承关系,再登记到会话节点索引。
|
||||
fn new_typed_block(
|
||||
cx: &mut NativeContext<'_>,
|
||||
data: Data,
|
||||
@@ -528,6 +558,7 @@ fn new_typed_block(
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// 在当前 VM 中建立窗口、desktop 和独立 Presentation,实际 OS 窗口等命令发布后再创建。
|
||||
fn create_window(
|
||||
cx: &mut NativeContext<'_>,
|
||||
state: &mut Session,
|
||||
@@ -557,6 +588,7 @@ fn create_window(
|
||||
Ok(window)
|
||||
}
|
||||
|
||||
/// 从页面登记信息取得所属窗口,保持 Page 的归属与原生资源身份一致。
|
||||
fn page_window(
|
||||
store: &endor_core::ObjectStore,
|
||||
state: &Session,
|
||||
@@ -570,6 +602,7 @@ fn page_window(
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
/// 撤销页面在展示会话中的登记,配合模型层处理活动页面与生命周期。
|
||||
fn remove_page(
|
||||
cx: &mut NativeContext<'_>,
|
||||
view: &mut crate::presentation::Presentation,
|
||||
@@ -588,6 +621,7 @@ fn remove_page(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 在指定窗口安排页面展示,并维护目标 Presentation 的活动/待发布状态。
|
||||
fn show_in(
|
||||
cx: &mut NativeContext<'_>,
|
||||
page: ObjectRef,
|
||||
@@ -638,20 +672,24 @@ fn show_in(
|
||||
view.pending = Some(Some(page));
|
||||
Ok(())
|
||||
}
|
||||
/// 在当前窗口安排页面展示,实际几何和画面由后续刷新提交。
|
||||
fn show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result<(), VmError> {
|
||||
show_in(cx, page, session, None)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 接收源 present 结果并将其转换成宿主可展示内容的原生续接步骤。
|
||||
struct PresentResult {
|
||||
open: bool,
|
||||
session: Shared,
|
||||
}
|
||||
impl NativeCallable for PresentResult {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Object(root)] = args else {
|
||||
return Err(invalid("Presentable.present must return a Block"));
|
||||
@@ -676,6 +714,7 @@ impl NativeCallable for PresentResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据 Presentable 能力把任意源值转换成可展示内容,避免只允许某个原生 Block 类型。
|
||||
fn present_value(
|
||||
cx: &mut NativeContext<'_>,
|
||||
value: &Value,
|
||||
@@ -718,11 +757,13 @@ fn present_value(
|
||||
}
|
||||
|
||||
#[must_use = "pending layout must be resumed on its owning VM"]
|
||||
/// 桥接布局的挂起/完成结果;PendingLayout 必须与其窗口上下文一起继续处理。
|
||||
pub enum LayoutProgress {
|
||||
Pending(Box<PendingLayout>),
|
||||
Complete(Box<crate::document::Frame>),
|
||||
}
|
||||
|
||||
/// 一次未提交布局的上下文:目标窗口、根、布局/缓存、延后钩子以及用于失败恢复的检查点。
|
||||
struct LayoutContext {
|
||||
surface: crate::page::Surface,
|
||||
root: Arc<UiNode>,
|
||||
@@ -734,17 +775,20 @@ struct LayoutContext {
|
||||
checkpoint: crate::mount::Checkpoint,
|
||||
}
|
||||
|
||||
/// 挂起布局的拥有者。续体与上下文必须一起保留并最终恢复或取消。
|
||||
pub struct PendingLayout {
|
||||
work: crate::source_layout::Work,
|
||||
context: LayoutContext,
|
||||
}
|
||||
|
||||
/// 运行时持有的桥接能力句柄,通过共享 Session 协调托管对象与每窗口展示。
|
||||
pub struct UiBridge {
|
||||
pub ui: ObjectRef,
|
||||
session: Shared,
|
||||
tasks: crate::tasks::Scheduler,
|
||||
}
|
||||
|
||||
/// 解析对象的方法并在同一 VM 中调用,使源覆盖与原生成员共用方法查找规则。
|
||||
fn invoke_member(
|
||||
vm: &mut Vm,
|
||||
owner: ObjectRef,
|
||||
@@ -770,6 +814,7 @@ fn invoke_member(
|
||||
result
|
||||
}
|
||||
|
||||
/// 将托管页面注册表投影成稳定标签 ID、标题与活动状态,不把托管引用交给原生侧栏。
|
||||
fn project_tabs(
|
||||
vm: &Vm,
|
||||
state: &mut crate::presentation::Presentation,
|
||||
@@ -816,6 +861,7 @@ fn project_tabs(
|
||||
impl UiBridge {
|
||||
/// Detach all occurrences without running layout/paint. All windows are
|
||||
/// already closed before source unmount hooks run, so they cannot remount.
|
||||
/// 按窗口和展示生命周期执行有界退出清理,允许调用方根据故障状态决定是否执行回调。
|
||||
pub fn shutdown(&self, vm: &mut Vm, callbacks: bool) -> Result<(usize, usize, usize)> {
|
||||
let tasks = self.tasks.quiesce(vm)?;
|
||||
let mut drivers = Vec::new();
|
||||
@@ -885,16 +931,20 @@ impl UiBridge {
|
||||
result
|
||||
}
|
||||
|
||||
/// 查询任务系统是否仍有可推进的协作工作,供 worker 选择等待策略。
|
||||
pub fn has_cooperative_tasks(&self) -> bool {
|
||||
self.tasks.has_cooperative_work()
|
||||
}
|
||||
|
||||
/// 把当前 VM 交给任务调度器推进,限制一轮交付数量。
|
||||
pub fn poll_tasks(&self, vm: &mut Vm, limit: usize) -> Result<usize> {
|
||||
self.tasks.poll(vm, limit)
|
||||
}
|
||||
/// 取消当前域中的任务,撤销关闭/失败后不应再发生的交付。
|
||||
pub fn cancel_tasks(&self, vm: &mut Vm) -> Result<usize> {
|
||||
self.tasks.cancel_all(vm)
|
||||
}
|
||||
/// 返回当前仍有效的展示窗口身份,用于稳定遍历一次全窗口刷新。
|
||||
pub fn window_ids(&self) -> Result<Vec<u64>> {
|
||||
let state = self
|
||||
.session
|
||||
@@ -917,6 +967,7 @@ impl UiBridge {
|
||||
});
|
||||
Ok(ids)
|
||||
}
|
||||
/// 判断窗口是否仍登记且没有进入关闭状态。
|
||||
pub fn accepts_window(&self, id: u64) -> bool {
|
||||
self.session.lock().is_ok_and(|state| {
|
||||
state
|
||||
@@ -926,6 +977,7 @@ impl UiBridge {
|
||||
})
|
||||
}
|
||||
|
||||
/// 切换当前 Presentation 上下文;不会创建新窗口或复制 VM。
|
||||
pub fn select_window(&self, id: u64) -> Result<()> {
|
||||
let mut state = self
|
||||
.session
|
||||
@@ -937,6 +989,7 @@ impl UiBridge {
|
||||
state.current = id;
|
||||
Ok(())
|
||||
}
|
||||
/// 从托管展示域撤销窗口,退役 Mount 并清理该窗口的私有对象关系。
|
||||
pub fn retire_window(&self, vm: &mut Vm, id: u64) -> Result<()> {
|
||||
if id == crate::window::MAIN_WINDOW {
|
||||
bail!("the primary shell is closed by the OS window control");
|
||||
@@ -964,6 +1017,7 @@ impl UiBridge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 收尾已经关闭的展示资源,避免它们参与下一次窗口遍历。
|
||||
pub fn finish_closed(&self, vm: &mut Vm) -> Result<()> {
|
||||
let mut state = self
|
||||
.session
|
||||
@@ -988,6 +1042,7 @@ impl UiBridge {
|
||||
state.current = crate::window::MAIN_WINDOW;
|
||||
Ok(())
|
||||
}
|
||||
/// 从各托管窗口收集待发布命令;原生执行时还需校验 session。
|
||||
pub fn take_window_commands(&self, vm: &Vm) -> Result<Vec<crate::window::Command>> {
|
||||
let mut state = self
|
||||
.session
|
||||
@@ -1002,6 +1057,7 @@ impl UiBridge {
|
||||
Ok(commands)
|
||||
}
|
||||
|
||||
/// 校验并更新所属窗口的逻辑尺寸输入;尺寸不变时不制造新布局,改变时标记需要刷新。
|
||||
pub fn set_viewport(&self, vm: &mut Vm, viewport: Option<crate::layout::Size>) -> Result<bool> {
|
||||
if let Some(size) = viewport
|
||||
&& (!size.width.is_finite()
|
||||
@@ -1037,6 +1093,7 @@ impl UiBridge {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 将 ui 入口与兼容名称安装到本次程序的全局字典,保持所有调用处在同一个 VM 域。
|
||||
pub fn install_globals(&self, vm: &mut Vm, globals: ObjectRef) -> Result<()> {
|
||||
vm.dict_set(globals, Value::Text("ui".into()), Value::Object(self.ui))?;
|
||||
// Compatibility spelling used by the original module examples.
|
||||
@@ -1051,6 +1108,7 @@ impl UiBridge {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 检查当前会话是否已经安排或拥有可展示页面。
|
||||
pub fn has_page(&self) -> Result<bool> {
|
||||
Ok(self
|
||||
.session
|
||||
@@ -1060,6 +1118,7 @@ impl UiBridge {
|
||||
.values()
|
||||
.any(|view| view.pending.flatten().is_some() || view.active.is_some()))
|
||||
}
|
||||
/// 验证标签拖放仍指向有效页面和窗口关系,避免旧拖动操作跨域生效。
|
||||
pub fn accepts_tab_drop(&self, vm: &Vm, input: &crate::events::Input) -> bool {
|
||||
let Some(target) = &input.tab_drop else {
|
||||
return true;
|
||||
@@ -1097,12 +1156,14 @@ impl UiBridge {
|
||||
}
|
||||
true
|
||||
}
|
||||
/// 通过展示协议打开源值,必要时创建或选择承载它的页面。
|
||||
pub fn open_value(&self, vm: &mut Vm, value: &Value) -> Result<()> {
|
||||
let function = object(Some(&vm.get_attr(self.ui, "open")?))?;
|
||||
let page = vm.call(function, std::slice::from_ref(value))?;
|
||||
vm.release_value(page)?;
|
||||
Ok(())
|
||||
}
|
||||
/// 处理页面激活、关闭、移动和拆分等标签操作,保持窗口所有权与展示状态一致。
|
||||
fn dispatch_tab(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
|
||||
let page = {
|
||||
let state = self
|
||||
@@ -1214,6 +1275,7 @@ impl UiBridge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 在所属窗口的展示上下文中处理输入,区分页面管理、滚动、焦点、相机与普通事件。
|
||||
pub fn dispatch(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
|
||||
input
|
||||
.validate()
|
||||
@@ -1419,6 +1481,7 @@ impl UiBridge {
|
||||
vm.release(mount)?;
|
||||
result
|
||||
}
|
||||
/// 整理不再有效的弱索引与展示根,并配合 VM 回收,避免派生缓存延长业务对象生命周期。
|
||||
pub fn reclaim(&self, vm: &mut Vm, _roots: &mut Vec<ObjectRef>) -> Result<()> {
|
||||
vm.collect_cycles();
|
||||
let mut state = self
|
||||
@@ -1438,6 +1501,7 @@ impl UiBridge {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 丢弃未成功发布的展示请求,恢复后续输入可以使用的会话状态。
|
||||
pub fn discard_pending(&self, vm: &mut Vm) -> Result<()> {
|
||||
let mut state = self
|
||||
.session
|
||||
@@ -1488,6 +1552,7 @@ impl UiBridge {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 重新评估已失效的属性绑定,并把结果计入会话统计。
|
||||
pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> {
|
||||
let (nodes, desktops) = {
|
||||
let state = self
|
||||
@@ -1504,6 +1569,7 @@ impl UiBridge {
|
||||
state.binding_evaluations = state.binding_evaluations.saturating_add(evaluated);
|
||||
Ok(())
|
||||
}
|
||||
/// 排空一次 VM 变化记录并通知各窗口,提取活动页面快照,判断本次是否需要布局与发布。
|
||||
pub fn prepare_frame(&self, vm: &mut Vm) -> Result<Option<Option<Arc<UiNode>>>> {
|
||||
let changes = vm.take_changes();
|
||||
let mut state = self
|
||||
@@ -1548,6 +1614,7 @@ impl UiBridge {
|
||||
}
|
||||
Ok(changed.then_some(root))
|
||||
}
|
||||
/// 提交已经完成的出现几何与快照,并处理展示生命周期;不把挂起或失败的布局当成已发布帧。
|
||||
pub fn commit_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -1603,6 +1670,7 @@ impl UiBridge {
|
||||
result
|
||||
}
|
||||
|
||||
/// 在已完成的几何上启动绘制协议,保留绘制任务所需的宿主上下文。
|
||||
pub fn begin_paint_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -1633,6 +1701,7 @@ impl UiBridge {
|
||||
self.accept_paint_progress(progress)
|
||||
}
|
||||
|
||||
/// 恢复同一次源绘制调用,把完成的 Draft 交回发布流程。
|
||||
pub fn resume_paint_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -1642,6 +1711,7 @@ impl UiBridge {
|
||||
self.accept_paint_progress(progress)
|
||||
}
|
||||
|
||||
/// 区分挂起与完成的绘制进度;完成时保存真实读取依赖并接回发布流程。
|
||||
fn accept_paint_progress(
|
||||
&self,
|
||||
progress: crate::source_paint::Progress,
|
||||
@@ -1666,6 +1736,7 @@ impl UiBridge {
|
||||
Ok(progress)
|
||||
}
|
||||
|
||||
/// 取消尚未完成的源绘制,释放其续体和未发布上下文。
|
||||
pub fn cancel_paint_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -1684,6 +1755,7 @@ impl UiBridge {
|
||||
invalidated
|
||||
}
|
||||
|
||||
/// 为目标快照建立 Mount、检查点和布局上下文,再启动可挂起的只读布局求解。
|
||||
pub fn begin_layout_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -1806,6 +1878,7 @@ impl UiBridge {
|
||||
self.accept_layout_progress(vm, context, progress)
|
||||
}
|
||||
|
||||
/// 记录本次刷新主动让出 worker 的次数,用于诊断分片执行。
|
||||
pub fn note_refresh_yield(&self) -> Result<()> {
|
||||
let mut state = self
|
||||
.session
|
||||
@@ -1815,6 +1888,7 @@ impl UiBridge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 记录视口目标替换导致的刷新重启,便于区分正常挂起与取消重试。
|
||||
pub fn note_refresh_restart(
|
||||
&self,
|
||||
cancelled_layout: bool,
|
||||
@@ -1834,6 +1908,7 @@ impl UiBridge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 撤销未完成布局所保留的窗口、挂载与生命周期上下文。
|
||||
fn discard_layout_context(&self, vm: &mut Vm, context: LayoutContext) -> Result<()> {
|
||||
let LayoutContext {
|
||||
surface: _,
|
||||
@@ -1866,6 +1941,7 @@ impl UiBridge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 取消源布局续体并清理对应上下文,保持已发布帧可用。
|
||||
pub fn cancel_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> {
|
||||
let PendingLayout { work, context } = pending;
|
||||
let cancelled = work.cancel(vm);
|
||||
@@ -1874,10 +1950,12 @@ impl UiBridge {
|
||||
restored
|
||||
}
|
||||
|
||||
/// 关闭失败布局的挂起工作并恢复检查点;供错误或候选撤销路径使用。
|
||||
pub fn abort_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> {
|
||||
self.cancel_layout_frame(vm, pending)
|
||||
}
|
||||
|
||||
/// 继续同一次尚未发布的布局任务;完成时沿原上下文收尾,不能新建一轮来重放副作用。
|
||||
pub fn resume_layout_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -1895,6 +1973,7 @@ impl UiBridge {
|
||||
self.accept_layout_progress(vm, context, progress)
|
||||
}
|
||||
|
||||
/// 接收布局的 Pending/Complete 结果;保留上下文或执行最终投影和提交。
|
||||
fn accept_layout_progress(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -1918,6 +1997,7 @@ impl UiBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/// 依次投影滚动/视口、补页面元数据、保存读取版本、提交 Mount,并处理延后的生命周期调用。
|
||||
fn finish_layout_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -2018,6 +2098,7 @@ impl UiBridge {
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// 读取当前源程序已经选择的展示内容,为编译入口生成最终文档提供根。
|
||||
pub fn rendered(&self, vm: &Vm) -> Result<Option<Arc<UiNode>>> {
|
||||
let state = self
|
||||
.session
|
||||
@@ -2032,6 +2113,7 @@ impl UiBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从会话的类型注册表取得指定类型,再给原生分配的对象附加托管类型身份。
|
||||
fn attach_named_type(
|
||||
cx: &mut NativeContext<'_>,
|
||||
object: ObjectRef,
|
||||
@@ -2045,6 +2127,7 @@ fn attach_named_type(
|
||||
attach_desktop_type(cx, object, name, desktop)
|
||||
}
|
||||
|
||||
/// 按给定 desktop 查找类型并附加,支持不同窗口使用自己的展示上下文。
|
||||
pub fn attach_desktop_type(
|
||||
cx: &mut NativeContext<'_>,
|
||||
instance: ObjectRef,
|
||||
@@ -2055,6 +2138,7 @@ pub fn attach_desktop_type(
|
||||
cx.attach_type(instance, definition)
|
||||
}
|
||||
|
||||
/// 读取 desktop 的托管类型注册表;缺失或类型错误时返回 VM 错误。
|
||||
pub(crate) fn desktop_type(
|
||||
store: &endor_core::ObjectStore,
|
||||
name: &str,
|
||||
@@ -2064,6 +2148,7 @@ pub(crate) fn desktop_type(
|
||||
object(store.dict_get(types, &Value::Text(name.into()))?.as_ref())
|
||||
}
|
||||
|
||||
/// 将当前托管或可变状态提取为后续阶段使用的派生表示。
|
||||
pub fn snapshot(vm: &Vm, value: &Value) -> Result<Arc<UiNode>> {
|
||||
let Value::Object(root) = value else {
|
||||
bail!("main must return a Block");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! 安装一次 VM 域需要的宿主类型、对象和方法。类型注册表是源代码调用原生能力的入口。
|
||||
//! 先建立类型与桌面基础设施,再绑定构造器、视觉协议和开发工具工厂;返回的根负责保持这些对象存活。
|
||||
//!
|
||||
//! Allocate and connect the managed desktop, type tables and host globals.
|
||||
use super::*;
|
||||
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
let desktop = vm.new_object()?;
|
||||
roots.push(desktop);
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
//! 把 Endor 方法调用分派到对应宿主操作。这里处理构造、树修改、绑定、状态与页面能力。
|
||||
//! 先检查参数和只读阶段限制,再修改托管状态;对象引用的生命周期仍交给 NativeContext 和 Aster 管理。
|
||||
//!
|
||||
//! Native Block operations executed on the owning VM worker.
|
||||
use super::*;
|
||||
|
||||
impl NativeCallable for Function {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
self.signature.clone()
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(self.operation.arity())
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
use Operation::*;
|
||||
let arity = self.operation.arity();
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
//! 把内置开发工具和起始页接入托管 Page。工厂在所属 VM 中编译和执行,而不是在窗口线程另建业务状态。
|
||||
//!
|
||||
//! Developer workspaces are managed Pages, using the same roots and placement API.
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 工具打开能力,保存所属会话和 ui 对象;调用时校验工具名,再在该 VM 中展示页面。
|
||||
struct OpenTool {
|
||||
session: Shared,
|
||||
ui: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for OpenTool {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
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"));
|
||||
@@ -63,6 +68,7 @@ impl NativeCallable for OpenTool {
|
||||
Ok(NativeResult::Return(Value::Object(page)))
|
||||
}
|
||||
}
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub(super) fn install(vm: &mut Vm, ui: ObjectRef, session: &Shared) -> Result<()> {
|
||||
let function = vm.allocate(ContextNativeFunction::new(
|
||||
"developerTools",
|
||||
@@ -79,6 +85,7 @@ pub(super) fn install(vm: &mut Vm, ui: ObjectRef, session: &Shared) -> Result<()
|
||||
|
||||
/// Compile the bundled Endor page once per VM, in a private module namespace.
|
||||
/// Its functions/classes and callbacks remain ordinary managed VM objects.
|
||||
/// 编译并安装 VM 私有的起始页工厂,供新页面动作重复调用。
|
||||
pub(super) fn install_start_page(vm: &mut Vm, bridge: &UiBridge) -> Result<()> {
|
||||
let source = frontend::EndorReader::from_text(
|
||||
"tessera:start-page.endor",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//! 把页面、2D 视口和 3D 视口的相机输入转换成托管对象更新。
|
||||
//! 输入携带具体出现的 Mount;嵌套世界 UI 先做逆投影,避免把屏幕位移直接当成场景坐标。
|
||||
//!
|
||||
//! Camera mutations run on the owning VM worker, just like other page input.
|
||||
use super::*;
|
||||
|
||||
impl UiBridge {
|
||||
/// 解码指定 3D 视口的旋转/平移/缩放输入,再调用其托管相机控制器。
|
||||
pub(super) fn dispatch_scene_view(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -108,6 +112,7 @@ impl UiBridge {
|
||||
result
|
||||
}
|
||||
|
||||
/// 把具体 CanvasViewport 的位移或缩放锚点映射到其显式 Camera2D。
|
||||
pub(super) fn dispatch_viewport_view(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
@@ -197,6 +202,7 @@ impl UiBridge {
|
||||
result
|
||||
}
|
||||
|
||||
/// 处理旧 Page 模式的兼容相机事件;独立视口使用各自的输入路径。
|
||||
pub(super) fn dispatch_page_view(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! 声明宿主方法在 Endor 类型系统中的参数和返回契约。
|
||||
//! 签名用于构造/调用检查,应与 calls 和各 NativeCallable 实现保持一致;这里不执行实际业务操作。
|
||||
//!
|
||||
//! Canonical contracts for native Block methods and constructors.
|
||||
use super::*;
|
||||
|
||||
/// 根据宿主方法类别提供 Endor 方法的签名,供类型与调用检查使用。
|
||||
pub(super) fn method_signature(
|
||||
store: &endor_core::ObjectStore,
|
||||
desktop: ObjectRef,
|
||||
@@ -86,6 +90,7 @@ pub(super) fn method_signature(
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
/// 将具体宿主 Operation 映射到参数/返回类型契约。
|
||||
pub(super) fn operation_signature(
|
||||
store: &endor_core::ObjectStore,
|
||||
desktop: ObjectRef,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// The page, cards and actions belong to Endor. The host only owns its sidebar.
|
||||
// 默认页由真实 Endor 对象组成:宿主负责窗口和侧栏,卡片布局、绘制和按钮回调在源代码中定义。
|
||||
// Note 复用 Column 的内容测量,但固定卡片宽度;paint 只提供背景,孩子仍沿正常树绘制。
|
||||
class Note: Column {
|
||||
tint: String
|
||||
function measure(self: Note, mount: Mount, constraints: Constraints): MeasureResult {
|
||||
@@ -18,12 +20,15 @@ class Note: Column {
|
||||
}
|
||||
}
|
||||
class QuietButton: Button {
|
||||
// 返回空绘制列表保留 Button 的交互能力,同时让这个入口只显示孩子文字。
|
||||
function paint(self: QuietButton, mount: Mount, geometry: Bounds): DisplayList {
|
||||
return DisplayList(ObjectList())
|
||||
}
|
||||
}
|
||||
class StartActions {
|
||||
// 保存起始页面身份,子页面中的返回按钮激活同一 Page,而不是复制一份页面。
|
||||
home: Page
|
||||
// 窗口页面是普通有限 Column;可用区域来自宿主发布的逻辑视口。
|
||||
function blank(self: StartActions, event: Event): Unit {
|
||||
page = Page()
|
||||
page.title = "窗口页面"
|
||||
@@ -31,6 +36,7 @@ class StartActions {
|
||||
back.onClick(self.home.activate)
|
||||
page.render(Column(56, 20, TextBlock("恰好一个窗口", 3), TextBlock("页面跟随可用区域调整。内容和工具栏由 Endor 自己组织。"), back))
|
||||
}
|
||||
// 延伸方向属于容器与滚动组合:Column 安排内容,有限 ScrollBlock 控制可见范围。
|
||||
function vertical(self: StartActions, event: Event): Unit {
|
||||
page = Page()
|
||||
page.title = "纵向页面"
|
||||
@@ -45,6 +51,7 @@ class StartActions {
|
||||
page.crossAlign = "stretch"
|
||||
page.render(view)
|
||||
}
|
||||
// Row 内可继续放 Column 卡片;横向滚动不要求改变 Page 的身份或生命周期。
|
||||
function horizontal(self: StartActions, event: Event): Unit {
|
||||
page = Page()
|
||||
page.title = "横向页面"
|
||||
@@ -59,9 +66,11 @@ class StartActions {
|
||||
page.crossAlign = "stretch"
|
||||
page.render(view)
|
||||
}
|
||||
// 重新调用工厂得到独立页面与相机,已有画布继续保留自己的视角。
|
||||
function canvas(self: StartActions, event: Event): Unit {
|
||||
page = newTab()
|
||||
}
|
||||
// 两个文字绑定观察同一个状态对象,示范共享业务状态与多个展示位置的区别。
|
||||
function counter(self: StartActions, event: Event): Unit {
|
||||
count = ui.state(0)
|
||||
add = Button("增加计数")
|
||||
@@ -71,6 +80,7 @@ class StartActions {
|
||||
page.render(Column(48, 24, TextBlock("一个对象,两个视图", 2), Row(0, 32, ui.textOf(count, "value", 3), ui.textOf(count, "value", 3)), add))
|
||||
}
|
||||
}
|
||||
// 页面工厂:先建立业务对象/动作,再建立世界组成,最后用一个有限视口显示它。
|
||||
function newTab(): Page {
|
||||
page = Page()
|
||||
page.title = "自由画布"
|
||||
@@ -86,6 +96,7 @@ function newTab(): Page {
|
||||
canvas.on("click", actions.canvas)
|
||||
shared.on("click", actions.counter)
|
||||
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
|
||||
world = Scene2D(hero)
|
||||
@@ -109,6 +120,7 @@ function newTab(): Page {
|
||||
end.x = 716
|
||||
end.y = 614
|
||||
world.append(end)
|
||||
// 视口参加有限布局并占用剩余空间;移动它的 Camera2D 不改写卡片世界位置。
|
||||
view = CanvasViewport(1024, 800, world)
|
||||
view.flex = 1
|
||||
page.crossAlign = "stretch"
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
//! 定义从托管对象提取的不可变视觉树。共享业务对象可以有多次展示,树节点与 Mount 出现的身份不是同一概念。
|
||||
//! 节点保存排版、绘制和协议投影信息;浅比较用于决定是否复用已有快照。
|
||||
//!
|
||||
//! Owned immutable visual derivation; no managed VM handles.
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 不可变视觉节点。block_id 关联业务身份,slots 保留子项出现的稳定组成;kind 和盒模型字段用于派生排版与绘制。
|
||||
pub struct UiNode {
|
||||
// 只有页面根带兼容 Page 相机;其余字段是普通有限盒子的布局策略。
|
||||
pub surface: Option<crate::page::Surface>,
|
||||
pub flex: f32,
|
||||
pub flex_policy: crate::layout::FlexPolicy,
|
||||
@@ -15,14 +20,17 @@ pub struct UiNode {
|
||||
pub border: crate::layout::Insets,
|
||||
pub border_color: String,
|
||||
pub clip_children: bool,
|
||||
// 这些字符串只是派生身份/协议版本键,不是可以操作托管堆的 ObjectRef。
|
||||
pub block_id: String,
|
||||
pub protocol_key: String,
|
||||
// slot 保持组成身份;数组次序确定当前绘制/布局顺序,二者不是同一概念。
|
||||
pub slots: Vec<u64>,
|
||||
pub interactive: bool,
|
||||
pub kind: UiKind,
|
||||
}
|
||||
|
||||
impl UiNode {
|
||||
/// 按节点种类返回其视觉子项;叶节点使用空切片,避免为遍历另建组成。
|
||||
pub fn children(&self) -> &[Arc<UiNode>] {
|
||||
match &self.kind {
|
||||
UiKind::Column { children, .. }
|
||||
@@ -38,6 +46,7 @@ impl UiNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断是否需要对子项执行弹性主轴分配。
|
||||
pub fn has_flex(&self) -> bool {
|
||||
self.flex > 0.0
|
||||
|| self.flex_policy != crate::layout::FlexPolicy::default()
|
||||
@@ -45,6 +54,7 @@ impl UiNode {
|
||||
|| self.sizing.height == crate::box_model::Dimension::Fill
|
||||
}
|
||||
|
||||
/// 判断当前节点是否会为子项指定实际分配范围,供源布局选择安排路径。
|
||||
pub fn allocates_children(&self) -> bool {
|
||||
self.children().iter().any(|child| {
|
||||
child.has_flex()
|
||||
@@ -53,6 +63,7 @@ impl UiNode {
|
||||
})
|
||||
}
|
||||
|
||||
/// 比较节点自身配置和直接子项身份,决定能否复用旧 Arc;不是深拷贝业务图。
|
||||
pub fn shallow_eq(&self, other: &Self) -> bool {
|
||||
if self.surface != other.surface
|
||||
|| self.flex_policy != other.flex_policy
|
||||
@@ -198,6 +209,7 @@ impl UiNode {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 视觉内容的原生分类。叶节点保存绘制数据,容器保存子树,场景视口另外保存相机与世界投影。
|
||||
pub enum UiKind {
|
||||
Viewport3D {
|
||||
width: f32,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 应用组合入口。先创建原生事件循环和共享发布状态,再启动 API/VM worker,最后把窗口事件交给 TesseraApp。
|
||||
//! VM 操作由运行时线程串行处理;窗口线程只消费发布文档与纯数据通知。crate 私有别名用于保持模块内部引用路径稳定。
|
||||
//!
|
||||
//! Application composition root. Directory modules own implementation; these
|
||||
//! crate-private aliases keep existing internal paths stable during reorganization.
|
||||
mod bridge;
|
||||
@@ -31,12 +34,16 @@ use app::{AppEvent, TesseraApp};
|
||||
use document::NamespaceState;
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
|
||||
/// 按顺序建立事件循环、共享文档、API/VM worker 和应用窗口;事件循环结束后返回原生退出清理结果。
|
||||
fn main() -> Result<()> {
|
||||
// 原生窗口由事件循环所属线程驱动;Wait 让空闲时休眠,而不是持续重绘。
|
||||
let event_loop = EventLoop::<AppEvent>::with_user_event().build()?;
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
// 共享最近完整发布的文档。后台 worker 拥有 VM,GUI 不借用其托管堆。
|
||||
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
|
||||
let (activity, control) = api::start(namespace.clone(), event_loop.create_proxy())?;
|
||||
let mut app = TesseraApp::new(namespace, activity, event_loop.create_proxy(), control)?;
|
||||
// run_app 接管线程直至退出;退出后仍需检查清理过程中记录的错误。
|
||||
event_loop.run_app(&mut app)?;
|
||||
app.exit_result()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 属性绑定:在只读计算中收集依赖,将结果写回绑定目标。
|
||||
//! 缓存记录读取版本而不是复制业务模型;依赖变化才重新计算。托管连接对象追踪其拥有的引用。
|
||||
//!
|
||||
//! Target-owned managed bindings. Dependency versions are borrowed identities;
|
||||
//! the host's weak Block registry never roots an otherwise unreachable graph.
|
||||
use std::{
|
||||
@@ -13,27 +16,33 @@ use crate::{block, event_dispatch::Subscription, ui_bridge::attach_desktop_type}
|
||||
/// The output value is borrowed for equality only, including object handles.
|
||||
/// It never owns a reference or dereferences a possibly reclaimed cached object.
|
||||
#[derive(Debug, Clone)]
|
||||
/// 上次绑定计算的读取观察,用于判断是否需要重新执行源计算。
|
||||
struct Cached {
|
||||
reads: Vec<endor_core::ReadDependency>,
|
||||
output: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// 托管属性连接,强持有计算/目标所需引用,并保留只读计算的版本缓存。
|
||||
pub struct Binding {
|
||||
field: String,
|
||||
pub compute: Option<ObjectRef>,
|
||||
reads: Arc<Mutex<Option<Cached>>>,
|
||||
}
|
||||
impl ObjectPayload for Binding {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-binding"
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
if let Some(compute) = self.compute {
|
||||
visit(compute);
|
||||
@@ -41,6 +50,7 @@ impl ObjectPayload for Binding {
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查绑定目标与可调用计算,建立托管连接及其引用关系。
|
||||
pub fn connect(
|
||||
cx: &mut NativeContext<'_>,
|
||||
target: ObjectRef,
|
||||
@@ -114,6 +124,7 @@ pub fn connect(
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
/// 在只读调用中重算失效绑定,采集新的读取版本,再将成功结果应用到目标属性。
|
||||
pub fn refresh(
|
||||
vm: &mut Vm,
|
||||
nodes: &[WeakObjectRef],
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! Block 的共享赋值、底层 Data 与视觉属性实现。Block 通过 Cell 间接指向 Data,使别名能观察到同一次替换。
|
||||
//! Data 保存子组件 slot、布局配置、绑定和订阅。属性读写需要沿有效的类型/存储路径解析,不能把快照当成可变业务对象。
|
||||
//! 提取视觉数据时将托管属性覆盖到原生描述;校验负责拒绝循环、非法尺寸和不符合类型的组成。
|
||||
//!
|
||||
//! Live visual objects. All semantic references are owned by Aster's heap;
|
||||
//! native Arcs are reserved for immutable rendering snapshots.
|
||||
use std::sync::Arc;
|
||||
@@ -7,12 +11,14 @@ use endor_core::{NativeContext, ObjectPayload, ObjectRef, ObjectStore, Value, Vm
|
||||
use crate::ui_bridge::UiNode;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// 稳定组成 slot 与子 Block 的配对;slot 不依赖孩子当前的屏幕位置。
|
||||
pub struct Child {
|
||||
pub slot: u64,
|
||||
pub block: ObjectRef,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// 原生组件分类,只描述业务组件种类;每种展示几何由布局和 Mount 另行生成。
|
||||
pub enum Kind {
|
||||
Text {
|
||||
value: String,
|
||||
@@ -49,6 +55,7 @@ pub enum Kind {
|
||||
/// The VM owns the Block -> Cell -> Data relationship. Shared assignment
|
||||
/// changes the Cell to point at another Block, retaining the original identity.
|
||||
#[derive(Debug, Clone)]
|
||||
/// 对共享 Cell 的稳定包装。赋值可让所有别名一起改指向,而每次出现的 Mount 仍独立。
|
||||
pub struct Block {
|
||||
pub state: ObjectRef,
|
||||
pub page: bool,
|
||||
@@ -56,9 +63,11 @@ pub struct Block {
|
||||
}
|
||||
|
||||
impl ObjectPayload for Block {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 沿 Block 的共享 Cell 定位属性存储,使别名替换后读取到同一份新 Data。
|
||||
fn attribute_target(&self, store: &ObjectStore) -> Result<Option<ObjectRef>, VmError> {
|
||||
let Value::Object(value) = store.cell_get(self.state)? else {
|
||||
return Err(VmError::InvalidArgument("Block state is not an object"));
|
||||
@@ -76,6 +85,7 @@ impl ObjectPayload for Block {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 Block 当前 Data 解析其类型转发目标,使共享替换后的类型查询仍一致。
|
||||
fn type_target(&self, store: &ObjectStore) -> Result<Option<ObjectRef>, VmError> {
|
||||
let Value::Object(value) = store.cell_get(self.state)? else {
|
||||
return Err(VmError::InvalidArgument("Block state is not an object"));
|
||||
@@ -88,21 +98,26 @@ impl ObjectPayload for Block {
|
||||
}
|
||||
}
|
||||
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-block"
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
visit(self.state);
|
||||
if let Some(tasks) = self.tasks {
|
||||
visit(tasks);
|
||||
}
|
||||
}
|
||||
/// 向 VM 暴露共享赋值单元;替换的是指向关系,不是逐项复制组件业务字段。
|
||||
fn assignment_cell(
|
||||
&self,
|
||||
source: ObjectRef,
|
||||
@@ -122,6 +137,7 @@ impl ObjectPayload for Block {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// 组件当前业务描述与组成。与一次求出的屏幕几何分开保存,替换或属性变化会触发派生快照失效。
|
||||
pub struct Data {
|
||||
pub kind: Kind,
|
||||
pub children: Vec<Child>,
|
||||
@@ -155,6 +171,7 @@ pub struct Data {
|
||||
}
|
||||
|
||||
impl Data {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(kind: Kind, children: Vec<ObjectRef>, padding: f32, gap: f32) -> Self {
|
||||
let next_slot = children.len() as u64;
|
||||
Self {
|
||||
@@ -196,6 +213,7 @@ impl Data {
|
||||
properties: None,
|
||||
}
|
||||
}
|
||||
/// 检查原生组件种类是否承载子组成,供树修改和布局选择使用。
|
||||
pub fn is_container(&self) -> bool {
|
||||
matches!(
|
||||
self.kind,
|
||||
@@ -212,15 +230,19 @@ impl Data {
|
||||
}
|
||||
|
||||
impl ObjectPayload for Data {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-block-data"
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
if let Some(scene) = self.scene3d {
|
||||
visit(scene);
|
||||
@@ -255,6 +277,7 @@ impl ObjectPayload for Data {
|
||||
}
|
||||
}
|
||||
|
||||
/// 分配 Data、共享 Cell 和 Block 包装,并让托管图拥有必要引用。
|
||||
pub fn allocate(cx: &mut NativeContext<'_>, mut data: Data) -> Result<ObjectRef, VmError> {
|
||||
let page = matches!(data.kind, Kind::Page);
|
||||
for child in &data.children {
|
||||
@@ -276,11 +299,13 @@ pub fn allocate(cx: &mut NativeContext<'_>, mut data: Data) -> Result<ObjectRef,
|
||||
|
||||
/// Borrowed final data; forwarding is part of the managed graph, never an Arc
|
||||
/// ownership graph hidden from the VM's cycle collector.
|
||||
/// 解析一个 Block 当前指向的 Data,返回底层身份和借用描述。
|
||||
pub fn resolve(store: &ObjectStore, block: ObjectRef) -> Result<(ObjectRef, &Data), VmError> {
|
||||
let (reference, data, _) = resolve_tracked(store, block)?;
|
||||
Ok((reference, data))
|
||||
}
|
||||
|
||||
/// 解析 Block 的完整引用路径,并返回快照失效所需的依赖身份。
|
||||
pub fn resolve_tracked(
|
||||
store: &ObjectStore,
|
||||
block: ObjectRef,
|
||||
@@ -311,11 +336,13 @@ pub fn resolve_tracked(
|
||||
|
||||
/// Validate the entire finite expansion before establishing a new composition
|
||||
/// edge. The node count is an explicit rejected resource quota, not truncated UI.
|
||||
/// 检查当前输入或发布值的不变量,调用方据校验结果决定是否继续。
|
||||
pub fn validate(
|
||||
store: &ObjectStore,
|
||||
root: ObjectRef,
|
||||
forbidden_state: Option<ObjectRef>,
|
||||
) -> Result<(), VmError> {
|
||||
// 显式进入/退出栈模拟深度优先遍历;退出时移除祖先链标记,允许共享子树。
|
||||
enum Step {
|
||||
Enter(ObjectRef),
|
||||
Exit(ObjectRef),
|
||||
@@ -361,13 +388,16 @@ pub fn validate(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将当前托管或可变状态提取为后续阶段使用的派生表示。
|
||||
pub fn snapshot(store: &ObjectStore, root: ObjectRef) -> Result<Arc<UiNode>, VmError> {
|
||||
crate::snapshot::RenderCache::default().snapshot(store, root)
|
||||
}
|
||||
|
||||
/// Native visual parameters are ordinary managed attributes too. Composition,
|
||||
/// histories and connections remain structural payload edges.
|
||||
/// 列出本类型可投影到托管属性的原生字段及默认值。
|
||||
pub fn native_properties(data: &Data) -> Vec<(&'static str, Value)> {
|
||||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||||
fn number(value: f32) -> Value {
|
||||
Value::Number((value as f64).into())
|
||||
}
|
||||
@@ -486,6 +516,7 @@ pub fn native_properties(data: &Data) -> Vec<(&'static str, Value)> {
|
||||
fields
|
||||
}
|
||||
|
||||
/// 复制原生描述并用当前托管属性覆盖它;结果只用于展示,不反向修改业务对象。
|
||||
pub fn visual_data(store: &ObjectStore, data: &Data) -> Result<Data, VmError> {
|
||||
let mut data = data.clone();
|
||||
let properties = data
|
||||
@@ -544,6 +575,7 @@ pub fn visual_data(store: &ObjectStore, data: &Data) -> Result<Data, VmError> {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// 按属性名检查和解码源值,再更新对应视觉字段;非法尺寸、枚举或颜色被拒绝。
|
||||
pub fn apply_visual_property(data: &mut Data, name: &str, value: &Value) -> Result<(), VmError> {
|
||||
let bad = || VmError::InvalidArgument("invalid native visual property");
|
||||
let number = || -> Result<f32, VmError> {
|
||||
@@ -799,6 +831,7 @@ pub fn apply_visual_property(data: &mut Data, name: &str, value: &Value) -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 统一返回非法页面缩放错误,确保属性写入与输入路径使用相同限制。
|
||||
fn invalid_zoom() -> VmError {
|
||||
VmError::InvalidArgument("Page.zoom expects a number from 0.1 to 4")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 运行时与窗口线程之间的发布数据契约。NamespaceState 管理提交序号、版本和当前完整文档。
|
||||
//! RenderDocument/Frame 包含绘制命令、Mount 几何、页面标签和相机投影。发布前统一检查数量、坐标、颜色及索引一致性。
|
||||
//! 屏幕命中、逻辑坐标和世界 UI 的射线映射必须与绘制采用同一变换;这里的 ID 是数据身份,不是窗口线程可操作的托管引用。
|
||||
//!
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -13,18 +17,23 @@ use crate::{
|
||||
pub const DEFAULT_NAMESPACE: &str = "app.main";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// HTTP/GUI 共享的最近完整发布状态;submission 决定结果归属,revision 表示实际发布次数。
|
||||
pub struct NamespaceState {
|
||||
pub name: String,
|
||||
pub revision: u64,
|
||||
// 较新完整 PUT/DELETE 会改变提交身份;旧输入即使晚完成也不得覆盖新结果。
|
||||
pub submission: u64,
|
||||
pub document: RenderDocument,
|
||||
// viewport 是主窗口逻辑页面区域;window_viewports 同时记录仍存活的副窗口。
|
||||
pub viewport: Option<crate::layout::Size>,
|
||||
pub window_viewports: std::collections::BTreeMap<u64, crate::layout::Size>,
|
||||
// 原生命令与运行时世代一起发布,防止窗口 ID 在跨 VM 时被误用。
|
||||
pub runtime_session: u64,
|
||||
pub window_commands: Vec<crate::window::Envelope>,
|
||||
}
|
||||
|
||||
impl Default for NamespaceState {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: DEFAULT_NAMESPACE.to_owned(),
|
||||
@@ -40,6 +49,7 @@ impl Default for NamespaceState {
|
||||
}
|
||||
|
||||
impl NamespaceState {
|
||||
/// 仅接收当前 session 的纯数据窗口命令,并按种类合并同资源的重复请求。
|
||||
pub fn publish_window_commands(&mut self, session: u64, commands: Vec<crate::window::Command>) {
|
||||
if self.runtime_session != session {
|
||||
self.window_commands.clear();
|
||||
@@ -63,13 +73,16 @@ impl NamespaceState {
|
||||
.sort_by_key(|command| command.command.sequence);
|
||||
}
|
||||
|
||||
/// 计算下一个完整源码提交身份;计数溢出时返回错误而不是复用旧身份。
|
||||
pub fn next_submission(&self) -> Result<u64> {
|
||||
self.submission
|
||||
.checked_add(1)
|
||||
.context("submission counter exhausted")
|
||||
}
|
||||
|
||||
/// 仅当提交身份仍是最新时替换完整文档并递增版本;已被取代的结果不发布。
|
||||
pub fn commit(&mut self, submission: u64, document: RenderDocument) -> Result<Option<u64>> {
|
||||
// 先核对归属,再计算新版本;失败或过时的结果不会替换已显示文档。
|
||||
if submission != self.submission {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -82,6 +95,7 @@ impl NamespaceState {
|
||||
Ok(Some(revision))
|
||||
}
|
||||
|
||||
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
|
||||
pub fn clear(&mut self) -> Result<u64> {
|
||||
let submission = self.next_submission()?;
|
||||
let revision = self
|
||||
@@ -98,6 +112,7 @@ impl NamespaceState {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
/// 可以离开 VM 线程的完整展示值,包含源码投影、根帧和其他窗口的文档。
|
||||
pub struct RenderDocument {
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub windows: std::collections::BTreeMap<u64, RenderDocument>,
|
||||
@@ -112,6 +127,7 @@ pub struct RenderDocument {
|
||||
}
|
||||
|
||||
impl RenderDocument {
|
||||
/// 检查当前输入或发布值的不变量,调用方据校验结果决定是否继续。
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
let surface = &self.frame.surface;
|
||||
if !surface.zoom.is_finite()
|
||||
@@ -234,6 +250,7 @@ impl RenderDocument {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 校验文档后把逻辑绘制命令转换成给定设备比例的显示列表,应用变换与裁剪。
|
||||
pub fn display_list(&self, scale: f32) -> Result<DisplayList> {
|
||||
let frame = &self.frame;
|
||||
let mut list = DisplayList::default();
|
||||
@@ -321,12 +338,14 @@ impl RenderDocument {
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// 从源码投影根按 ID 查找节点,供 Sources 查看与定位使用。
|
||||
pub fn node(&self, id: &str) -> Option<&CodeNode> {
|
||||
self.root.as_ref()?.find(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
/// Sources 使用的源码层次节点;ID、名称、种类和行列范围都属于代码投影而不是运行时对象句柄。
|
||||
pub struct CodeNode {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
@@ -340,6 +359,7 @@ pub struct CodeNode {
|
||||
}
|
||||
|
||||
impl CodeNode {
|
||||
/// 检查当前输入或发布值的不变量,调用方据校验结果决定是否继续。
|
||||
fn validate<'a>(&'a self, depth: usize, ids: &mut HashSet<&'a str>) -> Result<()> {
|
||||
if depth > 64 {
|
||||
bail!("architecture tree exceeds 64 levels");
|
||||
@@ -359,6 +379,7 @@ impl CodeNode {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 按请求的代码节点身份或路径查找对应的源码投影。
|
||||
fn find(&self, id: &str) -> Option<&CodeNode> {
|
||||
if self.id == id {
|
||||
return Some(self);
|
||||
@@ -368,7 +389,9 @@ impl CodeNode {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
/// 一个窗口的一次完整视觉发布:有序命令及其裁剪/变换、出现几何、焦点和版本。平行索引表必须和命令数量一致。
|
||||
pub struct Frame {
|
||||
// 命中索引是可重建的本地优化,不作为序列化协议的一部分保存。
|
||||
#[serde(skip)]
|
||||
pub hit_index: Arc<crate::viewport::spatial::Index>,
|
||||
#[serde(default)]
|
||||
@@ -400,6 +423,7 @@ pub struct Frame {
|
||||
}
|
||||
|
||||
impl Default for Frame {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hit_index: Arc::default(),
|
||||
@@ -422,6 +446,7 @@ impl Default for Frame {
|
||||
|
||||
/// A position in a published frame. Identifiers are opaque data, never VM handles.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
/// 某次出现的几何与投影元数据。逻辑矩形不被相机直接改写,view/panel 保存通往视图空间的映射。
|
||||
pub struct MountGeometry {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub panel: Option<Arc<crate::scene3d::panel::Mapping>>,
|
||||
@@ -452,15 +477,18 @@ pub struct MountGeometry {
|
||||
}
|
||||
|
||||
impl MountGeometry {
|
||||
/// 先沿世界 UI 面板逆投影指针,按需要执行可见性和裁剪检查。
|
||||
pub fn view_point(&self, point: Point, clipped: bool) -> Option<Point> {
|
||||
self.panel
|
||||
.as_ref()
|
||||
.map_or(Some(point), |panel| panel.texture_point(point, clipped))
|
||||
}
|
||||
/// 把页面指针转换为该出现的布局坐标,供 hitTest 和局部事件处理使用。
|
||||
pub fn logical_point(&self, point: Point) -> Option<Point> {
|
||||
let point = self.view_point(point, true)?;
|
||||
Some(self.view.unwrap_or_default().inverse(point))
|
||||
}
|
||||
/// 把视图位移换算成目标出现的局部位移,处理缩放与世界 UI 投影差异。
|
||||
pub fn logical_delta(&self, point: Option<[f32; 2]>, delta: [f32; 2]) -> Option<[f32; 2]> {
|
||||
let zoom = self.view.unwrap_or_default().zoom;
|
||||
if self.panel.is_none() {
|
||||
@@ -475,6 +503,7 @@ impl MountGeometry {
|
||||
((previous.y - current.y) / zoom).clamp(-1_000_000., 1_000_000.),
|
||||
])
|
||||
}
|
||||
/// 判断给定点是否位于目标范围;需要相机转换的类型会先转换再判断。
|
||||
pub fn contains(&self, point: Point) -> bool {
|
||||
let Some(point) = self.view_point(point, true) else {
|
||||
return false;
|
||||
@@ -488,6 +517,7 @@ impl MountGeometry {
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// 从顶层绘制顺序向后查找可控制的视口,命中与裁剪均成立才接收相机手势。
|
||||
pub fn camera_at(&self, point: Point) -> Option<&MountGeometry> {
|
||||
self.hit_index
|
||||
.candidates(point, self.mounts.len())
|
||||
@@ -503,6 +533,7 @@ impl Frame {
|
||||
})
|
||||
})
|
||||
}
|
||||
/// 在无故障帧中按逆绘制顺序选中可交互的出现,空间索引只缩小候选。
|
||||
pub fn hit_test(&self, point: Point) -> Option<&MountGeometry> {
|
||||
if self.fault.is_some() {
|
||||
return None;
|
||||
@@ -515,16 +546,19 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回默认主窗口身份,供省略窗口字段的兼容输入使用。
|
||||
fn default_window() -> u64 {
|
||||
crate::window::MAIN_WINDOW
|
||||
}
|
||||
|
||||
/// 提供空帧或未指定背景时的默认页面底色。
|
||||
fn default_background() -> String {
|
||||
"#F7F7F5".to_owned()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
/// 可校验、可序列化的逻辑绘制命令;转换到设备像素在 display_list 阶段完成。
|
||||
pub enum RenderCommand {
|
||||
Scene3D {
|
||||
x: f32,
|
||||
@@ -566,6 +600,7 @@ pub enum RenderCommand {
|
||||
}
|
||||
|
||||
impl RenderCommand {
|
||||
/// 检查当前输入或发布值的不变量,调用方据校验结果决定是否继续。
|
||||
pub(crate) fn validate(&self) -> Result<()> {
|
||||
match self {
|
||||
Self::Scene3D {
|
||||
@@ -638,6 +673,7 @@ impl RenderCommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 把一条已验证的发布命令变成原生绘制原语,缩放几何和文字样式。
|
||||
fn to_draw_command(&self, scale: f32) -> Result<DrawCommand> {
|
||||
Ok(match self {
|
||||
Self::Scene3D {
|
||||
@@ -701,14 +737,17 @@ impl RenderCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// 提供未指定描边宽度时的兼容默认值。
|
||||
fn default_stroke_width() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// 提供未指定字号时的协议默认值。
|
||||
fn default_text_size() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
/// 统一检查有限矩形坐标、尺寸与可选圆角,阻止异常值进入光栅循环。
|
||||
fn validate_rect(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Result<()> {
|
||||
if !x.is_finite() || !y.is_finite() || !width.is_finite() || !height.is_finite() {
|
||||
bail!("rectangle values must be finite");
|
||||
@@ -719,10 +758,12 @@ fn validate_rect(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将逻辑矩形转换为设备比例下的矩形,保持位置与尺寸同比缩放。
|
||||
fn scaled_rect(x: f32, y: f32, width: f32, height: f32, scale: f32) -> Rect {
|
||||
Rect::new(x * scale, y * scale, width * scale, height * scale)
|
||||
}
|
||||
|
||||
/// 解析支持的十六进制 RGB/RGBA 文本,拒绝错误长度或非法字符。
|
||||
pub fn parse_color(value: &str) -> Result<Color> {
|
||||
let hex = value
|
||||
.strip_prefix('#')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 模型层模块入口。包含托管业务对象、每次展示上下文、依赖缓存,以及交给窗口线程的发布值。
|
||||
//!
|
||||
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
|
||||
pub(crate) mod binding;
|
||||
pub(crate) mod block;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 每次展示的托管上下文:同一个 Block 出现在两个位置时,有两个独立 Mount。
|
||||
//! Mount 保存父子关系、出现几何和布局期间的查询上下文;MountTable 用弱索引定位对象,不靠索引强行保活业务树。
|
||||
//! 同步会安排 onMount/onUnmount。布局取消时使用 Checkpoint 恢复未发布状态,避免半完成的出现关系影响下一次刷新。
|
||||
//!
|
||||
//! Managed presentation contexts. The active desktop owns the mount dictionary;
|
||||
//! reverse links and the host registry are weak, so shared content cannot retain a Page.
|
||||
use crate::document::{Frame, MountGeometry};
|
||||
@@ -11,16 +15,20 @@ use std::{
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// 一个组件在一个展示位置上的上下文。强持有 Block,反向页面/父子链接使用弱身份,避免共享组件把已关闭页面保活。
|
||||
pub struct Mount {
|
||||
// 组件是强引用;页面、父节点和兄弟链接是弱引用,防止反向链接形成保活环。
|
||||
pub block: ObjectRef,
|
||||
pub page: Option<WeakObjectRef>,
|
||||
pub parent: Option<WeakObjectRef>,
|
||||
pub first_child: Option<WeakObjectRef>,
|
||||
pub next_sibling: Option<WeakObjectRef>,
|
||||
// 已发布几何与每次出现的滚动位置分开保存,同一 Block 的多个位置互不覆盖。
|
||||
pub geometry: MountGeometry,
|
||||
pub scroll_offset: [f32; 2],
|
||||
pub version: u64,
|
||||
pub mounted: bool,
|
||||
// Mount 可以拥有任务;退役后任务交付还要检查 owner 是否仍有效。
|
||||
pub tasks: Option<ObjectRef>,
|
||||
// Private derived scratch, installed before the read-only layout call and
|
||||
// cleared by geometry commit. It contains no managed references or roots.
|
||||
@@ -28,18 +36,23 @@ pub struct Mount {
|
||||
}
|
||||
|
||||
impl ObjectPayload for Mount {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-mount"
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
visit(self.block);
|
||||
if let Some(tasks) = self.tasks {
|
||||
@@ -49,6 +62,7 @@ impl ObjectPayload for Mount {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
|
||||
enum Query {
|
||||
Bounds,
|
||||
ScrollTo,
|
||||
@@ -68,6 +82,7 @@ enum Query {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||||
struct Method {
|
||||
signature: endor_core::CallSignature,
|
||||
owner: ObjectRef,
|
||||
@@ -76,9 +91,11 @@ struct Method {
|
||||
}
|
||||
|
||||
impl NativeCallable for Method {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(
|
||||
if matches!(
|
||||
@@ -97,6 +114,7 @@ impl NativeCallable for Method {
|
||||
))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mount = cx.store().payload::<Mount>(self.owner, "Mount")?.clone();
|
||||
if !mount.mounted
|
||||
@@ -324,6 +342,7 @@ impl NativeCallable for Method {
|
||||
}
|
||||
}
|
||||
|
||||
/// 声明 Mount 查询方法的源类型契约,区分几何、层次与坐标操作。
|
||||
fn mount_signature(
|
||||
store: &endor_core::ObjectStore,
|
||||
desktop: ObjectRef,
|
||||
@@ -370,12 +389,14 @@ fn mount_signature(
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
/// 活动出现的弱索引。真实托管拥有关系在 desktop 的挂载字典中,索引不能充当隐藏 GC 根。
|
||||
pub struct MountTable {
|
||||
pub active: HashMap<u64, WeakObjectRef>,
|
||||
order: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 同步出现关系时的共享临时工作状态,保存本轮重用、建立和退役安排。
|
||||
struct Sync {
|
||||
desktop: ObjectRef,
|
||||
page: Option<ObjectRef>,
|
||||
@@ -388,6 +409,7 @@ struct Sync {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 将一个生命周期回调及参数接入同一 VM 调用序列。
|
||||
struct LifecycleCall {
|
||||
function: ObjectRef,
|
||||
block: ObjectRef,
|
||||
@@ -395,10 +417,12 @@ struct LifecycleCall {
|
||||
unmount: bool,
|
||||
}
|
||||
impl NativeCallable for LifecycleCall {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
if self.unmount {
|
||||
crate::tasks::cancel_owner(cx, self.mount)?;
|
||||
@@ -410,6 +434,7 @@ impl NativeCallable for LifecycleCall {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造生命周期调用所需的托管函数与参数,使回调遵循同一 VM 的引用和执行约束。
|
||||
fn lifecycle_call(
|
||||
cx: &mut NativeContext<'_>,
|
||||
mount: ObjectRef,
|
||||
@@ -437,10 +462,12 @@ fn lifecycle_call(
|
||||
}
|
||||
|
||||
impl NativeCallable for Sync {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mut blocks = HashMap::new();
|
||||
for weak in &self.nodes {
|
||||
@@ -630,6 +657,7 @@ impl NativeCallable for Sync {
|
||||
|
||||
/// Explicitly owned roots protect the old presentation while preparation
|
||||
/// temporarily replaces its desktop dictionary and Mount payloads.
|
||||
/// 布局准备前的挂载快照与临时保留,用于取消恢复;成功提交后必须释放其保留引用。
|
||||
pub struct Checkpoint {
|
||||
table: MountTable,
|
||||
dictionary: Option<ObjectRef>,
|
||||
@@ -638,6 +666,7 @@ pub struct Checkpoint {
|
||||
}
|
||||
|
||||
impl Checkpoint {
|
||||
/// 释放检查点为回滚临时保留的托管根,成功提交后不再需要这些引用。
|
||||
pub fn release(self, vm: &mut Vm) -> Result<(), VmError> {
|
||||
let mut result = Ok(());
|
||||
for root in self.roots.into_iter().rev() {
|
||||
@@ -648,6 +677,7 @@ impl Checkpoint {
|
||||
result
|
||||
}
|
||||
|
||||
/// 恢复检查点中尚未发布的出现关系与布局上下文,并释放临时保留。
|
||||
pub fn restore(
|
||||
self,
|
||||
vm: &mut Vm,
|
||||
@@ -707,6 +737,7 @@ impl Checkpoint {
|
||||
}
|
||||
|
||||
impl MountTable {
|
||||
/// 保留布局准备前的 Mount 状态,供失败或取消路径恢复。
|
||||
pub fn checkpoint(&self, vm: &mut Vm, desktop: ObjectRef) -> Result<Checkpoint, VmError> {
|
||||
let dictionary = match vm.get_attr(desktop, "mounts") {
|
||||
Ok(Value::Object(dictionary)) => Some(dictionary),
|
||||
@@ -762,6 +793,7 @@ impl MountTable {
|
||||
|
||||
/// A faulted domain cannot run user onUnmount callbacks. Still detach every
|
||||
/// context and release its owning dictionary through the normal heap barrier.
|
||||
/// 退役当前出现表,撤销挂载关系并准备不再展示的生命周期状态。
|
||||
pub fn retire(&mut self, vm: &mut Vm, desktop: ObjectRef) -> Result<(), VmError> {
|
||||
for weak in self.ordered().into_iter().rev() {
|
||||
if let Some(object) = vm.upgrade(weak)? {
|
||||
@@ -788,6 +820,7 @@ impl MountTable {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 按原始展示顺序返回弱 Mount 身份,供事件和协议驱动器保持稳定顺序。
|
||||
pub fn ordered(&self) -> Vec<WeakObjectRef> {
|
||||
self.order
|
||||
.iter()
|
||||
@@ -795,6 +828,7 @@ impl MountTable {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 根据新结构复用或建立每次出现的 Mount,连接父子关系并准备生命周期调用。
|
||||
pub fn synchronize(
|
||||
&mut self,
|
||||
vm: &mut Vm,
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
//! 给源协议返回的对象列表提供受控封装。列表内容通过托管引用被追踪,读方法只暴露检查后的条目。
|
||||
//! 用于儿童组成、布局结果和几何输入等需要保留对象身份的场景;不是窗口线程共享的可变容器。
|
||||
//!
|
||||
//! Managed indexed host results; values and the at method are traced edges.
|
||||
use endor_core::{
|
||||
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef,
|
||||
Value, VmError,
|
||||
};
|
||||
#[derive(Debug)]
|
||||
/// 托管对象序列,trace 负责拥有其中的对象引用;源代码通过受检查方法读取。
|
||||
struct ObjectList(Vec<ObjectRef>);
|
||||
impl ObjectPayload for ObjectList {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-object-list"
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
for mount in &self.0 {
|
||||
visit(*mount);
|
||||
@@ -26,8 +35,10 @@ impl ObjectPayload for ObjectList {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 绑定到某个列表对象的下标访问适配器。
|
||||
struct At(ObjectRef);
|
||||
impl NativeCallable for At {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
Some(endor_core::CallSignature::exact(
|
||||
vec![endor_core::AnnotationType::Number],
|
||||
@@ -35,10 +46,12 @@ impl NativeCallable for At {
|
||||
endor_core::CallEffect::ReadOnly,
|
||||
))
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Number(index)] = args else {
|
||||
return Err(VmError::InvalidArgument("object list at expects an index"));
|
||||
@@ -57,6 +70,7 @@ impl NativeCallable for At {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将已有对象引用包装为新的 ObjectList,并安装默认名义类型及下标查询方法。
|
||||
pub fn collection(
|
||||
cx: &mut NativeContext<'_>,
|
||||
mounts: Vec<ObjectRef>,
|
||||
@@ -65,6 +79,7 @@ pub fn collection(
|
||||
build(cx, mounts, desktop, None)
|
||||
}
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
fn build(
|
||||
cx: &mut NativeContext<'_>,
|
||||
mounts: Vec<ObjectRef>,
|
||||
@@ -96,23 +111,28 @@ fn build(
|
||||
Ok(set)
|
||||
}
|
||||
|
||||
/// 借用受检查列表的条目;返回值的有效期仍依赖原托管对象存活。
|
||||
pub fn items(store: &endor_core::ObjectStore, list: ObjectRef) -> Result<&[ObjectRef], VmError> {
|
||||
Ok(&store.payload::<ObjectList>(list, "ObjectList")?.0)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// ObjectList 原生类型的可调用构造器,负责检查传入子对象。
|
||||
pub struct Constructor {
|
||||
pub signature: Option<endor_core::CallSignature>,
|
||||
pub desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Constructor {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
self.signature.clone()
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::range(1, usize::MAX))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Object(definition), values @ ..] = args else {
|
||||
return Err(VmError::InvalidArgument("ObjectList type argument"));
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 一个原生窗口对应一份 Presentation:独立视口、Mount、测量缓存、焦点与依赖记录。
|
||||
//! 多个窗口仍使用同一个 VM 和共享业务对象。读取版本失效时只标记相关展示需要刷新,不能因此复制一份业务状态。
|
||||
//!
|
||||
//! Per-window rendering state. Business objects and code remain in one VM.
|
||||
//! Registries and caches borrow checked identities; the managed desktop owns
|
||||
//! presentation roots through its pending/mount dictionaries.
|
||||
@@ -6,6 +9,7 @@ use endor_core::{ChangeBatch, NativeContext, ObjectRef, Vm, VmError, WeakObjectR
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 一个窗口独立的展示状态:活动页面、待提交请求、Mount、布局与源读取缓存;共享 VM 的业务状态不放在这里复制。
|
||||
pub(crate) struct Presentation {
|
||||
pub desktop: ObjectRef,
|
||||
pub closing: bool,
|
||||
@@ -34,6 +38,7 @@ pub(crate) struct Presentation {
|
||||
}
|
||||
|
||||
impl Presentation {
|
||||
/// 比较布局/绘制读取版本和名称依赖,标记需要刷新并通知快照缓存。
|
||||
pub fn observe_changes(&mut self, vm: &Vm, changes: &ChangeBatch) {
|
||||
if let Some((phase, object, version)) = self
|
||||
.layout_reads
|
||||
@@ -82,6 +87,7 @@ impl Presentation {
|
||||
});
|
||||
}
|
||||
|
||||
/// 按尺寸、内容或绘制失效原因清理对应缓存,避免把所有变化都当成相同重建。
|
||||
pub fn invalidate(
|
||||
&mut self,
|
||||
cx: &mut NativeContext<'_>,
|
||||
@@ -112,6 +118,7 @@ impl Presentation {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(desktop: ObjectRef, tabs: ObjectRef) -> Self {
|
||||
Self {
|
||||
desktop,
|
||||
@@ -142,6 +149,7 @@ impl Presentation {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把读取记录拆成对象版本和精确名称依赖,便于按实际观察内容判断失效。
|
||||
pub(crate) fn split_reads(
|
||||
reads: Vec<endor_core::ReadDependency>,
|
||||
) -> (
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
//! 安装 Visual、EventTarget、Children 和 Presentable 等能力契约。
|
||||
//! 契约描述对象能响应什么源方法;具体测量、绘制、事件执行仍在各自协议驱动器中完成。
|
||||
//!
|
||||
//! Host capability definitions over Aster's explicit managed trait relations.
|
||||
use endor_core::{
|
||||
AnnotationType, CallArity, CallEffect, CallSignature, ObjectRef, TypedMethodRequirement, Value,
|
||||
Vm, VmError,
|
||||
};
|
||||
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub fn install(
|
||||
vm: &mut Vm,
|
||||
roots: &mut Vec<ObjectRef>,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 将 Endor 托管场景对象转换成可供渲染线程读取的 3D 数据。
|
||||
//! 构造器和字段仍属于 VM;快照复制必要的数值、不可变网格和依赖,渲染器不读取托管堆。
|
||||
//! 场景遍历组合局部变换、检查环和资源限制,并为 Panel3D 提取独立 UI 根。控制器更新相机;拾取结果再映射回托管节点。
|
||||
//!
|
||||
//! Traced 3D objects and checked immutable snapshots. No managed handle enters
|
||||
//! the renderer; cameras and node transforms remain editable Endor attributes.
|
||||
use crate::{
|
||||
@@ -11,6 +15,7 @@ use endor_core::{NativeContext, ObjectPayload, ObjectRef, ObjectStore, Value, Vm
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 源 3D 类型的构造元数据入口,统一管理名称、参数、数量和可变子项。
|
||||
pub enum Constructor {
|
||||
Vec3,
|
||||
Triangle,
|
||||
@@ -26,6 +31,7 @@ pub enum Constructor {
|
||||
Panel,
|
||||
}
|
||||
impl Constructor {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Vec3 => "Vec3",
|
||||
@@ -42,6 +48,7 @@ impl Constructor {
|
||||
Self::Panel => "Panel3D",
|
||||
}
|
||||
}
|
||||
/// 取得本构造器对应的源工厂函数名称。
|
||||
pub fn function(self) -> &'static str {
|
||||
match self {
|
||||
Self::Vec3 => "vec3",
|
||||
@@ -58,6 +65,7 @@ impl Constructor {
|
||||
Self::Panel => "panel3D",
|
||||
}
|
||||
}
|
||||
/// 反查源构造器名称对应的原生 3D 种类,不认识的名称返回空值。
|
||||
pub fn from_function(name: &str) -> Option<Self> {
|
||||
[
|
||||
Self::Vec3,
|
||||
@@ -76,6 +84,7 @@ impl Constructor {
|
||||
.into_iter()
|
||||
.find(|kind| kind.function() == name)
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
pub fn arity(self) -> (usize, usize) {
|
||||
match self {
|
||||
Self::Vec3 | Self::Triangle => (3, 3),
|
||||
@@ -87,6 +96,7 @@ impl Constructor {
|
||||
_ => (0, 0),
|
||||
}
|
||||
}
|
||||
/// 列出本构造器的参数类型名称,供签名安装与源调用检查使用。
|
||||
pub fn parameters(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::Vec3 | Self::Triangle => &["Number", "Number", "Number"],
|
||||
@@ -98,12 +108,14 @@ impl Constructor {
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
/// 标记构造器是否接收数量可变的子对象。
|
||||
pub fn variadic(self) -> bool {
|
||||
matches!(self, Self::Group | Self::Scene)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// 托管 3D 节点的名义种类,防止仅有同名字段的任意对象冒充场景节点。
|
||||
pub enum NodeKind {
|
||||
Vector,
|
||||
Triangle,
|
||||
@@ -117,40 +129,52 @@ pub enum NodeKind {
|
||||
Panel,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 允许普通属性的 3D 标记 payload;数值字段仍由托管属性系统拥有。
|
||||
pub struct Node(pub NodeKind);
|
||||
impl ObjectPayload for Node {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"scene-3d-object"
|
||||
}
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 托管不可变网格包装,顶点和三角形数值已经复制为原生 Arc 数据。
|
||||
pub struct Geometry(pub Arc<scene3d::Geometry>);
|
||||
impl ObjectPayload for Geometry {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"geometry-3d"
|
||||
}
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 把源 Value 转换为受检查的托管对象引用,拒绝非对象输入。
|
||||
fn object_ref(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
if let Value::Object(object) = value {
|
||||
Ok(*object)
|
||||
@@ -158,9 +182,11 @@ fn object_ref(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
Err(invalid("3D field requires an object"))
|
||||
}
|
||||
}
|
||||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||||
fn number(value: &Value) -> Result<f32, VmError> {
|
||||
crate::scroll::coordinate(value)
|
||||
}
|
||||
/// 把当前源值或文本选择解码成此调用需要的文本表示。
|
||||
fn text(value: &Value) -> Result<String, VmError> {
|
||||
if let Value::Text(text) = value {
|
||||
Ok(text.clone())
|
||||
@@ -168,6 +194,7 @@ fn text(value: &Value) -> Result<String, VmError> {
|
||||
Err(invalid("3D field requires text"))
|
||||
}
|
||||
}
|
||||
/// 把经过转换的数值写回指定托管字段,仍通过 VM/NativeContext 的检查入口。
|
||||
fn set_number(
|
||||
cx: &mut NativeContext<'_>,
|
||||
object: ObjectRef,
|
||||
@@ -176,6 +203,7 @@ fn set_number(
|
||||
) -> Result<(), VmError> {
|
||||
cx.set_attr(object, name, crate::native_visual::value(value))
|
||||
}
|
||||
/// 解析颜色值并检查其满足当前原生绘制约定。
|
||||
fn color(value: &str) -> Result<[f32; 3], VmError> {
|
||||
let color = crate::document::parse_color(value).map_err(|_| invalid("invalid 3D color"))?;
|
||||
if color.alpha != 255 {
|
||||
@@ -188,6 +216,7 @@ fn color(value: &str) -> Result<[f32; 3], VmError> {
|
||||
])
|
||||
}
|
||||
|
||||
/// 按构造器建立对应托管 3D 类型,检查几何输入并安装默认属性。
|
||||
pub fn allocate(
|
||||
cx: &mut NativeContext<'_>,
|
||||
desktop: ObjectRef,
|
||||
@@ -368,18 +397,22 @@ pub fn allocate(
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
/// 快照字段读取器,同时记录实际观察到的对象/属性依赖。
|
||||
struct Reader<'a> {
|
||||
store: &'a ObjectStore,
|
||||
reads: HashSet<(ObjectRef, String)>,
|
||||
}
|
||||
impl Reader<'_> {
|
||||
/// 读取字段并登记依赖,使后续字段变更能够使 3D 快照失效。
|
||||
fn value(&mut self, object: ObjectRef, name: &str) -> Result<Value, VmError> {
|
||||
self.reads.insert((object, name.into()));
|
||||
self.store.get_attr(object, name)
|
||||
}
|
||||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||||
fn number(&mut self, object: ObjectRef, name: &str) -> Result<f32, VmError> {
|
||||
number(&self.value(object, name)?)
|
||||
}
|
||||
/// 连续读取三个数值字段,组成位置、旋转或缩放向量。
|
||||
fn vector(&mut self, object: ObjectRef, names: [&str; 3]) -> Result<Vec3, VmError> {
|
||||
Ok(Vec3::new(
|
||||
self.number(object, names[0])?,
|
||||
@@ -387,6 +420,7 @@ impl Reader<'_> {
|
||||
self.number(object, names[2])?,
|
||||
))
|
||||
}
|
||||
/// 读取并校验透视或正交摄像机字段,生成不持有 VM 引用的投影描述。
|
||||
fn camera(&mut self, object: ObjectRef) -> Result<Camera, VmError> {
|
||||
let projection = match self.store.payload::<Node>(object, "3D camera")?.0 {
|
||||
NodeKind::Perspective => Projection::Perspective {
|
||||
@@ -416,6 +450,7 @@ impl Reader<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从场景中提取的有限 UI 根,携带稳定面板 key 和逻辑排版尺寸。
|
||||
pub struct PanelMount {
|
||||
pub key: u64,
|
||||
pub content: ObjectRef,
|
||||
@@ -426,6 +461,7 @@ pub type Snapshot = (
|
||||
Vec<(ObjectRef, String)>,
|
||||
Vec<PanelMount>,
|
||||
);
|
||||
/// 将当前托管或可变状态提取为后续阶段使用的派生表示。
|
||||
pub fn snapshot(
|
||||
store: &ObjectStore,
|
||||
scene: ObjectRef,
|
||||
@@ -433,6 +469,7 @@ pub fn snapshot(
|
||||
) -> Result<Snapshot, VmError> {
|
||||
snapshot_reusing(store, scene, camera, None)
|
||||
}
|
||||
/// 遍历场景并记录字段依赖;如果世界数据未变则复用不可变场景和 BVH,只替换摄像机视图。
|
||||
pub fn snapshot_reusing(
|
||||
store: &ObjectStore,
|
||||
scene: ObjectRef,
|
||||
@@ -615,6 +652,7 @@ pub fn snapshot_reusing(
|
||||
Ok((view, reader.reads.into_iter().collect(), panels))
|
||||
}
|
||||
|
||||
/// 把视口中的指针射线结果转换成 RayHit3D,关联实际托管网格和交点数据。
|
||||
pub fn pick(
|
||||
cx: &mut NativeContext<'_>,
|
||||
desktop: ObjectRef,
|
||||
@@ -706,6 +744,7 @@ pub fn pick(
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 指针拾取的中间结果,保留交点与所属实例信息供 Event/RayHit3D 使用。
|
||||
pub struct PointerPick {
|
||||
pub mount: ObjectRef,
|
||||
pub desktop: ObjectRef,
|
||||
@@ -713,6 +752,7 @@ pub struct PointerPick {
|
||||
}
|
||||
|
||||
/// The default controller is replaceable; camera data has no dependency on it.
|
||||
/// 为指定出现解码相机手势并调用控制器;嵌套面板输入先转换到所属视口坐标。
|
||||
pub fn control(
|
||||
cx: &mut NativeContext<'_>,
|
||||
controller: ObjectRef,
|
||||
@@ -809,6 +849,7 @@ pub fn control(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 执行托管 3D 节点的原生平移/旋转操作,供尚不支持普通数值算术表达式的源程序调用。
|
||||
pub fn transform(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -863,9 +904,11 @@ pub fn transform(
|
||||
Ok(())
|
||||
}
|
||||
impl endor_core::NativeCallable for PointerPick {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(
|
||||
&self,
|
||||
cx: &mut NativeContext<'_>,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 从可变托管对象图提取不可变 UiNode,并维护反向依赖与父子失效传播。
|
||||
//! 缓存只借用可检查的对象身份,不增加托管根。对象释放后必须清理缓存索引;快照仍可持有独立的原生数值数据。
|
||||
//! 干净子树直接复用 Arc;脏节点重新检查属性、方法、绑定和子项,循环与展开数量在发布前拒绝。
|
||||
//!
|
||||
//! Object-to-visual dependencies. Cached results never own VM references.
|
||||
use crate::{
|
||||
block::{self, Kind},
|
||||
@@ -10,6 +14,7 @@ use std::{
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 某个业务组件的视觉快照,以及使它失效的读取依赖和子项身份。
|
||||
struct Cached {
|
||||
node: Arc<UiNode>,
|
||||
dependencies: Vec<Dependency>,
|
||||
@@ -17,12 +22,14 @@ struct Cached {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
/// 反向依赖键:区分整个对象内容和精确属性名称,减少无关字段变化引起的失效。
|
||||
struct Dependency {
|
||||
object: ObjectRef,
|
||||
property: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 视觉快照缓存与反向依赖图。对象身份是借用索引,原生 Arc 数据不拥有 VM 根。
|
||||
pub struct RenderCache {
|
||||
scenes3d: HashMap<ObjectRef, Arc<crate::scene3d::Scene>>,
|
||||
entries: HashMap<ObjectRef, Cached>,
|
||||
@@ -35,6 +42,7 @@ pub struct RenderCache {
|
||||
}
|
||||
|
||||
impl RenderCache {
|
||||
/// 把 VM 变化映射到读取过这些属性/对象的视觉节点,再沿父链向上传播脏标记。
|
||||
pub fn invalidate(&mut self, batch: ChangeBatch) {
|
||||
let mut queue = VecDeque::new();
|
||||
if batch.rescan_required {
|
||||
@@ -58,6 +66,7 @@ impl RenderCache {
|
||||
self.invalidate_objects(queue);
|
||||
}
|
||||
|
||||
/// 迭代传播显式失效,使用集合避免共享父链重复遍历或循环入队。
|
||||
pub fn invalidate_objects(&mut self, objects: impl IntoIterator<Item = ObjectRef>) {
|
||||
let mut queue: VecDeque<_> = objects.into_iter().collect();
|
||||
while let Some(block) = queue.pop_front() {
|
||||
@@ -69,6 +78,7 @@ impl RenderCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// 撤销旧缓存的反向读取边和父子边,为重新登记该节点做准备。
|
||||
fn remove(&mut self, block: ObjectRef) -> Option<Cached> {
|
||||
let old = self.entries.remove(&block)?;
|
||||
for source in &old.dependencies {
|
||||
@@ -90,6 +100,7 @@ impl RenderCache {
|
||||
Some(old)
|
||||
}
|
||||
|
||||
/// 删除托管堆中已不存在的身份,缓存本身不负责保持业务对象存活。
|
||||
pub fn prune(&mut self, store: &ObjectStore) {
|
||||
self.scenes3d
|
||||
.retain(|object, _| store.object(*object).is_ok());
|
||||
@@ -105,6 +116,7 @@ impl RenderCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按显式栈遍历托管组成;复用干净子树,重建脏节点,并在完成前检查循环和资源上限。
|
||||
pub fn snapshot(
|
||||
&mut self,
|
||||
store: &ObjectStore,
|
||||
@@ -116,6 +128,7 @@ impl RenderCache {
|
||||
let mut path = HashSet::new();
|
||||
let mut values = Vec::<Arc<UiNode>>::new();
|
||||
while let Some((block, finish)) = work.pop() {
|
||||
// 入栈时先尝试复用;退出标记表示孩子已形成快照,可以组装父节点。
|
||||
if !finish {
|
||||
if !self.dirty.contains(&block)
|
||||
&& let Some(cached) = self.entries.get(&block)
|
||||
@@ -198,6 +211,7 @@ impl RenderCache {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// path 只追踪当前祖先链,因此允许同一业务组件在不同分支多次出现。
|
||||
if !finish {
|
||||
if !path.insert(data_ref) {
|
||||
return Err(VmError::InvalidArgument("cyclic Block composition"));
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 托管 Window 的页面所有权、原生命令与关闭生命周期。
|
||||
//! 源码 resize/focus/setTitle 先产生纯数据命令;只有所属输入成功发布后,GUI 线程才应用它们。
|
||||
//! 窗口 ID 和运行时 session 共同限定资源归属;切换 VM 或关闭窗口时不能让旧命令作用到新资源。
|
||||
//!
|
||||
//! Managed window resources and versioned, coalesced native projections.
|
||||
//! The VM owns Page roots; only scalar commands cross to the GUI thread.
|
||||
use crate::{native_visual, object_list, ui_bridge::desktop_type};
|
||||
@@ -10,6 +14,7 @@ use std::sync::{Arc, Mutex};
|
||||
pub const MAIN_WINDOW: u64 = 1;
|
||||
pub const MAX_WINDOW_DIMENSION: f64 = 4096.0;
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 可投影到原生窗口的动作类别,如尺寸、标题、焦点与关闭。
|
||||
pub enum Action {
|
||||
Resize { width: f64, height: f64 },
|
||||
SetTitle(String),
|
||||
@@ -18,23 +23,27 @@ pub enum Action {
|
||||
Close,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 成功发布后由 GUI 执行的一条原生窗口请求,含资源身份和动作顺序。
|
||||
pub struct Command {
|
||||
pub window: u64,
|
||||
pub sequence: u64,
|
||||
pub action: Action,
|
||||
}
|
||||
impl Command {
|
||||
/// 判断两条命令是否针对同一资源的同类动作,用于替换尚未执行的旧请求。
|
||||
pub fn same_kind(&self, other: &Self) -> bool {
|
||||
self.window == other.window
|
||||
&& std::mem::discriminant(&self.action) == std::mem::discriminant(&other.action)
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
/// 附带运行时 session 的命令,阻止旧域请求在新 VM 上生效。
|
||||
pub struct Envelope {
|
||||
pub session: u64,
|
||||
pub command: Command,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
/// 托管窗口资源状态与页面拥有关系;本对象存在不等于 OS 窗口已创建。
|
||||
pub struct ManagedWindow {
|
||||
pub id: u64,
|
||||
pub desktop: ObjectRef,
|
||||
@@ -48,18 +57,23 @@ pub struct ManagedWindow {
|
||||
outbox: Arc<Mutex<Vec<Command>>>,
|
||||
}
|
||||
impl ObjectPayload for ManagedWindow {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-window"
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
visit(self.desktop);
|
||||
if let Some(tasks) = self.tasks {
|
||||
@@ -71,9 +85,11 @@ impl ObjectPayload for ManagedWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 把源数值转换为有限屏幕坐标,拒绝越界或不可表示的值。
|
||||
fn screen_coordinate(value: &Value) -> Result<i32, VmError> {
|
||||
let Value::Number(value) = value else {
|
||||
return Err(invalid("screen position requires integer coordinates"));
|
||||
@@ -84,6 +100,7 @@ fn screen_coordinate(value: &Value) -> Result<i32, VmError> {
|
||||
.and_then(|number| i32::try_from(number).ok())
|
||||
.ok_or(invalid("screen position exceeds limits"))
|
||||
}
|
||||
/// 从源 Value 取得窗口操作需要的数值,保留错误类型诊断。
|
||||
fn scalar(value: &Value) -> Result<f64, VmError> {
|
||||
let Value::Number(value) = value else {
|
||||
return Err(invalid("window size requires numbers"));
|
||||
@@ -98,6 +115,7 @@ fn scalar(value: &Value) -> Result<f64, VmError> {
|
||||
Ok(number)
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 当前宿主 API 的操作分类,分派代码负责参数校验与状态更新。
|
||||
enum Operation {
|
||||
Id,
|
||||
Viewport,
|
||||
@@ -108,15 +126,18 @@ enum Operation {
|
||||
Focus,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||||
struct Method {
|
||||
signature: endor_core::CallSignature,
|
||||
operation: Operation,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Method {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(match self.operation {
|
||||
Operation::Resize | Operation::MoveTo => 3,
|
||||
@@ -125,6 +146,7 @@ impl NativeCallable for Method {
|
||||
}))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let Some(Value::Object(owner)) = args.first() else {
|
||||
return Err(invalid("Window method requires a Window"));
|
||||
@@ -196,6 +218,7 @@ impl NativeCallable for Method {
|
||||
Ok(NativeResult::Return(result))
|
||||
}
|
||||
}
|
||||
/// 把合法窗口操作追加到所属窗口的待发布命令,不能直接在 VM 线程操作 OS。
|
||||
pub fn enqueue(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -225,6 +248,7 @@ pub fn enqueue(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将页面登记到其窗口,维护托管所有权与页面顺序。
|
||||
pub fn register_page(
|
||||
cx: &mut NativeContext<'_>,
|
||||
window: ObjectRef,
|
||||
@@ -253,6 +277,7 @@ pub fn register_page(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 从窗口的页面集合撤销指定页面,保持后续展示索引一致。
|
||||
pub fn unregister_page(
|
||||
cx: &mut NativeContext<'_>,
|
||||
window: ObjectRef,
|
||||
@@ -276,6 +301,7 @@ pub fn unregister_page(
|
||||
Ok(())
|
||||
}
|
||||
/// Discard an unpublished Page registration without invoking guest callbacks.
|
||||
/// 宿主收尾路径撤销页面登记,用于已经由外层决定的关闭/退役流程。
|
||||
pub fn unregister_page_host(
|
||||
vm: &mut Vm,
|
||||
window: ObjectRef,
|
||||
@@ -290,6 +316,7 @@ pub fn unregister_page_host(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 在窗口之间移动页面的所有权和登记顺序,保留同一业务页面身份。
|
||||
pub fn move_page(
|
||||
cx: &mut NativeContext<'_>,
|
||||
window: ObjectRef,
|
||||
@@ -323,9 +350,11 @@ pub fn move_page(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 借用一个有效托管窗口的已登记页面集合。
|
||||
pub fn pages(store: &ObjectStore, window: ObjectRef) -> Result<&[ObjectRef], VmError> {
|
||||
Ok(&store.payload::<ManagedWindow>(window, "Window")?.pages)
|
||||
}
|
||||
/// 取走当前窗口累积的命令,后续由成功发布结果携带到 GUI。
|
||||
pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result<Vec<Command>, VmError> {
|
||||
let data = vm.payload::<ManagedWindow>(window, "Window")?;
|
||||
let mut queue = data
|
||||
@@ -336,6 +365,7 @@ pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result<Vec<Command>, VmError
|
||||
commands.sort_by_key(|command| command.sequence);
|
||||
Ok(commands)
|
||||
}
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub fn install(
|
||||
vm: &mut Vm,
|
||||
roots: &mut Vec<ObjectRef>,
|
||||
@@ -413,6 +443,7 @@ pub fn install(
|
||||
|
||||
/// Create a managed native-window resource. The GUI creates the OS resource only
|
||||
/// after the owning candidate/input publishes its immutable projection.
|
||||
/// 建立托管窗口、desktop 与页面容器;原生窗口通过命令发布后出现。
|
||||
pub fn create(
|
||||
cx: &mut NativeContext<'_>,
|
||||
master: ObjectRef,
|
||||
@@ -449,6 +480,7 @@ pub fn create(
|
||||
Ok((window, desktop, tabs))
|
||||
}
|
||||
|
||||
/// 发起窗口关闭和相应生命周期清理,拒绝重复或不再有效的操作。
|
||||
pub fn close(cx: &mut NativeContext<'_>, window: ObjectRef) -> Result<(), VmError> {
|
||||
if cx.store().is_read_only() {
|
||||
return Err(invalid(
|
||||
@@ -477,6 +509,7 @@ pub fn close(cx: &mut NativeContext<'_>, window: ObjectRef) -> Result<(), VmErro
|
||||
}
|
||||
|
||||
/// Host teardown for a stopped VM: no binding, dispatch or source execution.
|
||||
/// 撤销已失效窗口的托管资源关系,不重新执行源关闭请求。
|
||||
pub fn retire(vm: &mut Vm, window: ObjectRef) -> Result<(), VmError> {
|
||||
let mut data = vm.payload::<ManagedWindow>(window, "Window")?.clone();
|
||||
vm.dict_clear(data.tabs)?;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
//! 有限布局盒子的尺寸策略。区分 auto、固定值和 fill,并处理 min/max 对约束与最终尺寸的限制。
|
||||
//! margin、border、padding 的空间分配由布局层结合本模块完成;场景世界范围不被当成无限布局盒子。
|
||||
//!
|
||||
//! Native border-box sizing. An unbounded axis cannot resolve `fill`.
|
||||
use crate::{layout::Size, ui_bridge::MAX_DIMENSION};
|
||||
use anyhow::Result;
|
||||
use endor_core::{Value, VmError};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
/// 一个布局轴的尺寸策略:随内容、固定值或占用有限可用空间。
|
||||
pub enum Dimension {
|
||||
#[default]
|
||||
Auto,
|
||||
@@ -12,6 +16,7 @@ pub enum Dimension {
|
||||
}
|
||||
|
||||
impl Dimension {
|
||||
/// 把内部尺寸策略投影为相应的源属性表示。
|
||||
pub fn value(self) -> Value {
|
||||
match self {
|
||||
Self::Auto => Value::Text("auto".into()),
|
||||
@@ -20,6 +25,7 @@ impl Dimension {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(value: &Value) -> Result<Self, VmError> {
|
||||
match value {
|
||||
Value::Text(text) if text == "auto" => Ok(Self::Auto),
|
||||
@@ -43,6 +49,7 @@ impl Dimension {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// border-box 尺寸策略与双轴上下限,实际求解仍要结合父级约束。
|
||||
pub struct Sizing {
|
||||
pub width: Dimension,
|
||||
pub height: Dimension,
|
||||
@@ -53,6 +60,7 @@ pub struct Sizing {
|
||||
}
|
||||
|
||||
impl Default for Sizing {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: Dimension::Auto,
|
||||
@@ -67,6 +75,7 @@ impl Default for Sizing {
|
||||
|
||||
impl Sizing {
|
||||
/// Constrain descendants using the actual finite box before wrapping text.
|
||||
/// 根据固定/auto/fill 与 min/max 规则细化父级提供的可用约束。
|
||||
pub fn available(self, mut constraints: [f32; 4]) -> [f32; 4] {
|
||||
constraints[2] = constraints[2].min(self.max_width);
|
||||
constraints[3] = constraints[3].min(self.max_height);
|
||||
@@ -81,7 +90,9 @@ impl Sizing {
|
||||
constraints
|
||||
}
|
||||
|
||||
/// 结合内容期望尺寸、轴策略和父级约束求最终盒子尺寸;无界轴不能解析 fill。
|
||||
pub fn resolve(self, intrinsic: Size, constraints: [f32; 4]) -> Result<Size> {
|
||||
/// 针对单个尺寸轴应用约束与边界规则,供双轴尺寸求解复用。
|
||||
fn axis(
|
||||
policy: Dimension,
|
||||
intrinsic: f32,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//! 面向设备像素的绘制指令集合。它承载矩形、文字、裁剪与 3D 表面等原语,不直接操作 VM。
|
||||
//! 平移和缩放统一作用于命令几何;文字样式和裁剪也要使用一致的设备比例。
|
||||
//!
|
||||
use crate::geometry::{Point, Rect};
|
||||
use crate::text::TextStyle;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 设备绘制使用的 RGBA 字节通道,区别于源代码的颜色文本。
|
||||
pub struct Color {
|
||||
pub red: u8,
|
||||
pub green: u8,
|
||||
@@ -10,6 +14,7 @@ pub struct Color {
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// 从 RGB 通道构造不透明颜色值。
|
||||
pub const fn rgb(red: u8, green: u8, blue: u8) -> Self {
|
||||
Self {
|
||||
red,
|
||||
@@ -19,6 +24,7 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 RGBA 通道构造带透明度的颜色值。
|
||||
pub const fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
|
||||
Self {
|
||||
red,
|
||||
@@ -30,6 +36,7 @@ impl Color {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// 原生画布最终顺序执行的绘制原语,已经使用对应设备比例。
|
||||
pub enum DrawCommand {
|
||||
Scene3D {
|
||||
rect: Rect,
|
||||
@@ -64,6 +71,7 @@ pub enum DrawCommand {
|
||||
}
|
||||
|
||||
impl DrawCommand {
|
||||
/// 返回平移后的绘制命令,并同步移动命令中的相关几何。
|
||||
pub fn translated(mut self, delta: Point) -> Self {
|
||||
match &mut self {
|
||||
Self::WrappedText { rect, .. }
|
||||
@@ -85,11 +93,13 @@ impl DrawCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 有序绘制原语容器;命令顺序同时约束叠放和裁剪栈行为。
|
||||
pub struct DisplayList {
|
||||
commands: Vec<DrawCommand>,
|
||||
}
|
||||
|
||||
impl DisplayList {
|
||||
/// 按设备比例缩放命令,文字、边框和矩形不能分别使用不一致比例。
|
||||
pub fn scale(&mut self, scale: f32) {
|
||||
for command in &mut self.commands {
|
||||
let rect = match command {
|
||||
@@ -129,10 +139,12 @@ impl DisplayList {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 按绘制顺序追加一条原语,后加入的内容在合成顺序中位于后面。
|
||||
pub fn push(&mut self, command: DrawCommand) {
|
||||
self.commands.push(command);
|
||||
}
|
||||
|
||||
/// 平移当前显示列表中的命令,供整块页面或管理面板放入窗口坐标。
|
||||
pub fn translate(&mut self, delta: Point) {
|
||||
for command in &mut self.commands {
|
||||
match command {
|
||||
@@ -153,10 +165,12 @@ impl DisplayList {
|
||||
}
|
||||
}
|
||||
|
||||
/// 借用按绘制顺序排列的原生原语,不复制命令集合。
|
||||
pub fn commands(&self) -> &[DrawCommand] {
|
||||
&self.commands
|
||||
}
|
||||
|
||||
/// 按原顺序合并另一组绘制命令,不在这里重排层次。
|
||||
pub fn extend(&mut self, other: DisplayList) {
|
||||
self.commands.extend(other.commands);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
//! 共享的二维点和矩形值。命中判断在调用方完成坐标系转换后使用,矩形本身不隐式处理相机。
|
||||
//!
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 二维坐标值;具体处于逻辑、局部还是设备空间由调用上下文决定。
|
||||
pub struct Point {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
/// 轴对齐矩形,保存原点和宽高,用于布局、裁剪与命中。
|
||||
pub struct Rect {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
@@ -19,6 +24,7 @@ pub struct Rect {
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
|
||||
Self {
|
||||
x,
|
||||
@@ -28,6 +34,7 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断给定点是否位于目标范围;需要相机转换的类型会先转换再判断。
|
||||
pub fn contains(self, point: Point) -> bool {
|
||||
point.x >= self.x
|
||||
&& point.x <= self.x + self.width
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 原生布局求解器:自底向上测量期望尺寸,再自顶向下分配位置和实际尺寸。
|
||||
//! Row/Column/Overlay、边距、边框、内边距、对齐和弹性空间都在有限约束内计算;过大的尺寸和坐标返回错误。
|
||||
//! 缓存按组件形状、子项尺寸和约束判断可复用性。frame 先在克隆状态中求解,成功才替换运行状态,防止失败污染已发布几何。
|
||||
//!
|
||||
//! Iterative measure/arrange/paint over derived visual data. Every occurrence
|
||||
//! has its own geometry; measurement caches never become semantic object state.
|
||||
use crate::{
|
||||
@@ -11,6 +15,7 @@ use std::collections::{HashMap, HashSet};
|
||||
const MAX_LAYOUT_VISITS: usize = 10_000;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
/// 上右下左四边的空间占用,用于 margin、padding 和 border 计算。
|
||||
pub struct Insets {
|
||||
pub top: f32,
|
||||
pub right: f32,
|
||||
@@ -19,12 +24,15 @@ pub struct Insets {
|
||||
}
|
||||
|
||||
impl Insets {
|
||||
/// 返回该边距集合在水平方向占据的总空间。
|
||||
pub fn horizontal(self) -> f32 {
|
||||
self.left + self.right
|
||||
}
|
||||
/// 返回该边距集合在垂直方向占据的总空间。
|
||||
pub fn vertical(self) -> f32 {
|
||||
self.top + self.bottom
|
||||
}
|
||||
/// 在内容尺寸外加上对应四边占用,得到外层测量尺寸。
|
||||
fn outer(self, size: Size) -> Size {
|
||||
Size {
|
||||
width: size.width + self.horizontal(),
|
||||
@@ -36,6 +44,7 @@ impl Insets {
|
||||
/// Alignment inside a finite cross-axis content box. Overflow remains start
|
||||
/// aligned so that content cannot become inaccessible beyond the leading edge.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
/// 交叉轴对齐规则;Stretch 需要结合子项是否自动尺寸决定实际拉伸。
|
||||
pub enum CrossAlign {
|
||||
#[default]
|
||||
Start,
|
||||
@@ -45,6 +54,7 @@ pub enum CrossAlign {
|
||||
}
|
||||
|
||||
impl CrossAlign {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Start => "start",
|
||||
@@ -54,6 +64,7 @@ impl CrossAlign {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(value: &str) -> Result<Self, endor_core::VmError> {
|
||||
match value {
|
||||
"start" => Ok(Self::Start),
|
||||
@@ -66,6 +77,7 @@ impl CrossAlign {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据交叉轴对齐方式计算有限剩余空间中的偏移。
|
||||
fn offset(self, available: f32, occupied: f32) -> f32 {
|
||||
let spare = (available - occupied).max(0.0);
|
||||
match self {
|
||||
@@ -77,6 +89,7 @@ impl CrossAlign {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
/// 容器主轴的内容对齐或剩余间隔分配方式。
|
||||
pub enum MainAlign {
|
||||
#[default]
|
||||
Start,
|
||||
@@ -85,6 +98,7 @@ pub enum MainAlign {
|
||||
SpaceBetween,
|
||||
}
|
||||
impl MainAlign {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Start => "start",
|
||||
@@ -93,6 +107,7 @@ impl MainAlign {
|
||||
Self::SpaceBetween => "space-between",
|
||||
}
|
||||
}
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(value: &str) -> Result<Self, endor_core::VmError> {
|
||||
match value {
|
||||
"start" => Ok(Self::Start),
|
||||
@@ -108,6 +123,7 @@ impl MainAlign {
|
||||
|
||||
/// Main-axis constraints interpreted independently by each parent occurrence.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// 主轴伸缩策略与限值,不能用一个宽度常量替代完整分配规则。
|
||||
pub struct FlexPolicy {
|
||||
pub basis: Option<f32>,
|
||||
pub min: f32,
|
||||
@@ -115,6 +131,7 @@ pub struct FlexPolicy {
|
||||
pub shrink: f32,
|
||||
}
|
||||
impl Default for FlexPolicy {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
basis: None,
|
||||
@@ -126,12 +143,14 @@ impl Default for FlexPolicy {
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 逻辑宽高值,不隐含窗口像素比例或相机缩放。
|
||||
pub struct Size {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 影响原生测量的缓存形状,避免把颜色等纯绘制变化视为尺寸变化。
|
||||
enum Shape {
|
||||
Sized(Box<Shape>, crate::box_model::Sizing, [f32; 4]),
|
||||
Text(String, u32, f32),
|
||||
@@ -141,6 +160,7 @@ enum Shape {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
/// 结构出现身份键,区分同一业务对象在不同父位置的展示。
|
||||
struct Key {
|
||||
parent: Option<u64>,
|
||||
slot: u64,
|
||||
@@ -148,6 +168,7 @@ struct Key {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
/// 布局缓存、稳定出现身份与统计。构建在临时副本上完成,成功后才写回。
|
||||
pub struct LayoutRuntime {
|
||||
sizes: HashMap<String, (Shape, Size)>,
|
||||
ids: HashMap<Key, u64>,
|
||||
@@ -161,6 +182,7 @@ pub struct LayoutRuntime {
|
||||
pub page_mode: Option<crate::page::Mode>,
|
||||
}
|
||||
|
||||
/// 迭代遍历中的一个出现,保存父子索引、约束与稳定 ID。
|
||||
struct Item<'a> {
|
||||
node: &'a UiNode,
|
||||
parent: Option<usize>,
|
||||
@@ -169,6 +191,7 @@ struct Item<'a> {
|
||||
constraints: [f32; 4],
|
||||
}
|
||||
|
||||
/// 检查尺寸非负、有限且不超过布局上限。
|
||||
fn bounded(value: f32) -> Result<f32> {
|
||||
if !value.is_finite() || !(0.0..=MAX_DIMENSION as f32).contains(&value) {
|
||||
bail!("UI layout exceeds the 1000000 coordinate/size limit");
|
||||
@@ -176,6 +199,7 @@ fn bounded(value: f32) -> Result<f32> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// 检查允许有正负方向的坐标仍在安全布局范围内。
|
||||
fn coordinate(value: f32) -> Result<f32> {
|
||||
if !value.is_finite() || value.abs() > MAX_DIMENSION as f32 {
|
||||
bail!("UI coordinate exceeds the 1000000 limit");
|
||||
@@ -183,6 +207,7 @@ fn coordinate(value: f32) -> Result<f32> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// 从页面模式和当前视口生成根节点约束;无视口时使用明确的无界轴哨兵。
|
||||
pub fn root_constraints(viewport: Option<Size>) -> [f32; 4] {
|
||||
let size = viewport.unwrap_or(Size {
|
||||
width: MAX_DIMENSION as f32,
|
||||
@@ -191,6 +216,7 @@ pub fn root_constraints(viewport: Option<Size>) -> [f32; 4] {
|
||||
[0.0, 0.0, size.width, size.height]
|
||||
}
|
||||
|
||||
/// 从父级上限扣除内边距;无界哨兵保持无界,不因减法被误认为有限。
|
||||
fn content_limit(available: f32, inset: f32) -> f32 {
|
||||
if available >= MAX_DIMENSION as f32 {
|
||||
available
|
||||
@@ -198,6 +224,7 @@ fn content_limit(available: f32, inset: f32) -> f32 {
|
||||
(available - inset).max(0.0)
|
||||
}
|
||||
}
|
||||
/// 把父盒子的可用尺寸传递给子项,扣除容器内边距并考虑主轴是否可延伸。
|
||||
pub fn child_constraints(node: &UiNode, parent: [f32; 4]) -> [f32; 4] {
|
||||
let parent = node.sizing.available(parent);
|
||||
if let UiKind::Overlay { .. } = node.kind {
|
||||
@@ -239,6 +266,7 @@ pub fn child_constraints(node: &UiNode, parent: [f32; 4]) -> [f32; 4] {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从子项可用范围扣除外边距,并使最小值不超过调整后的最大值。
|
||||
pub fn margin_constraints(child: &UiNode, mut constraints: [f32; 4]) -> [f32; 4] {
|
||||
for (axis, margin) in [child.margin.horizontal(), child.margin.vertical()]
|
||||
.into_iter()
|
||||
@@ -254,6 +282,7 @@ pub fn margin_constraints(child: &UiNode, mut constraints: [f32; 4]) -> [f32; 4]
|
||||
|
||||
/// Main-axis fill first measures against a finite offer, then participates in
|
||||
/// weighted allocation. The unlimited-axis sentinel is never made finite by padding.
|
||||
/// 根据具体子项尺寸策略和所在位置细化约束;世界 UI 根使用面板的有限像素尺寸。
|
||||
pub fn occurrence_constraints(
|
||||
parent: &UiNode,
|
||||
child: &UiNode,
|
||||
@@ -281,6 +310,7 @@ pub fn occurrence_constraints(
|
||||
margin_constraints(child, base)
|
||||
}
|
||||
|
||||
/// 生成求解开始时的子项约束,供后续测量和分配迭代使用。
|
||||
pub fn initial_child_constraints(
|
||||
parent: &UiNode,
|
||||
child: &UiNode,
|
||||
@@ -301,6 +331,7 @@ pub fn initial_child_constraints(
|
||||
}
|
||||
/// Scroll cross-axis constraints come from this occurrence's actual viewport,
|
||||
/// including a source parent's allocation rather than only constructor size.
|
||||
/// 根据已分配父尺寸重新细化子项约束,使实际安排能够反向影响换行等测量。
|
||||
pub fn arranged_child_constraints(node: &UiNode, available: [f32; 4], bounds: Size) -> [f32; 4] {
|
||||
if let UiKind::Scroll { axis, .. } = node.kind {
|
||||
let padding = insets(node);
|
||||
@@ -322,6 +353,7 @@ pub fn arranged_child_constraints(node: &UiNode, available: [f32; 4], bounds: Si
|
||||
child_constraints(node, available)
|
||||
}
|
||||
|
||||
/// 将期望尺寸限制在调用方给出的最小/最大边界内。
|
||||
pub fn constrained(size: Size, constraints: [f32; 4]) -> Size {
|
||||
Size {
|
||||
width: size.width.clamp(constraints[0], constraints[2]),
|
||||
@@ -330,6 +362,7 @@ pub fn constrained(size: Size, constraints: [f32; 4]) -> Size {
|
||||
}
|
||||
|
||||
/// Effective content inset, including borders, in border-box coordinates.
|
||||
/// 计算布局使用的有效内边距和边框占用,兼容旧的统一 padding。
|
||||
pub fn insets(node: &UiNode) -> Insets {
|
||||
let fallback = match node.kind {
|
||||
UiKind::Row { padding, .. }
|
||||
@@ -347,6 +380,7 @@ pub fn insets(node: &UiNode) -> Insets {
|
||||
}
|
||||
}
|
||||
|
||||
/// 提取容器方向与间距等原生布局配置,叶节点不参与容器规则。
|
||||
fn container(node: &UiNode) -> Option<(bool, Insets, f32)> {
|
||||
match &node.kind {
|
||||
UiKind::Row { gap, .. } => Some((true, insets(node), *gap)),
|
||||
@@ -358,10 +392,12 @@ fn container(node: &UiNode) -> Option<(bool, Insets, f32)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据容器方向选择主轴,供 flex 和对齐共享同一判断。
|
||||
pub fn main_axis(node: &UiNode) -> Option<usize> {
|
||||
container(node).map(|(row, _, _)| usize::from(!row))
|
||||
}
|
||||
|
||||
/// 根据形状键计算期望尺寸,包括文字换行、容器子项和盒模型占用。
|
||||
fn measure_shape(shape: &Shape) -> Result<Size> {
|
||||
let size = match shape {
|
||||
Shape::Sized(shape, sizing, constraints) => {
|
||||
@@ -419,6 +455,7 @@ fn measure_shape(shape: &Shape) -> Result<Size> {
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// 建立影响测量的形状键,将布局属性、子项测量结果和约束组合起来。
|
||||
fn shape(node: &UiNode, children: Vec<Size>, constraints: [f32; 4]) -> Result<Shape> {
|
||||
let available = node.sizing.available(constraints);
|
||||
let shape = match &node.kind {
|
||||
@@ -481,6 +518,7 @@ fn shape(node: &UiNode, children: Vec<Size>, constraints: [f32; 4]) -> Result<Sh
|
||||
})
|
||||
}
|
||||
|
||||
/// 执行原生节点测量并应用布局约束,作为源协议的原生实现。
|
||||
pub fn measure_native(node: &UiNode, children: Vec<Size>, constraints: [f32; 4]) -> Result<Size> {
|
||||
measure_shape(&shape(node, children, constraints)?)
|
||||
}
|
||||
@@ -488,6 +526,7 @@ pub fn measure_native(node: &UiNode, children: Vec<Size>, constraints: [f32; 4])
|
||||
/// Private derived scratch space for one occurrence during layout. Contains no
|
||||
/// managed references; source code cannot mutate it through heap attributes.
|
||||
#[derive(Debug, Default)]
|
||||
/// 只读布局调用期间提供给 Mount 查询的派生上下文,不作为业务状态保留。
|
||||
pub struct Context {
|
||||
pub node: Option<std::sync::Arc<UiNode>>,
|
||||
pub children: Vec<Size>,
|
||||
@@ -495,6 +534,7 @@ pub struct Context {
|
||||
pub bounds: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
/// 为单个原生节点按已分配几何生成内容命令;容器背景/边框与整树遍历由外层组织。
|
||||
pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option<RenderCommand> {
|
||||
let size_for_text = size;
|
||||
let inset = insets(node);
|
||||
@@ -537,6 +577,7 @@ pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option<RenderC
|
||||
}
|
||||
}
|
||||
|
||||
/// 从一个节点及其分配几何生成原生绘制命令,包括背景、边框与内容。
|
||||
pub fn paint_commands(node: &UiNode, x: f32, y: f32, size: Size) -> Vec<RenderCommand> {
|
||||
let mut commands: Vec<_> = paint_native(node, x, y, size).into_iter().collect();
|
||||
let border = node.border;
|
||||
@@ -573,6 +614,7 @@ pub fn paint_commands(node: &UiNode, x: f32, y: f32, size: Size) -> Vec<RenderCo
|
||||
/// Allocate main-axis space with bounded growth and basis-weighted shrinking.
|
||||
/// Preferred sizes come from this occurrence's first measurement in the solve;
|
||||
/// remeasurement must not turn a prior allocation into a new intrinsic basis.
|
||||
/// 判断子项主轴是否采用 fill,从而参与剩余空间分配。
|
||||
fn main_fill(child: &UiNode, row: bool) -> bool {
|
||||
(if row {
|
||||
child.sizing.width
|
||||
@@ -581,6 +623,7 @@ fn main_fill(child: &UiNode, row: bool) -> bool {
|
||||
}) == crate::box_model::Dimension::Fill
|
||||
}
|
||||
|
||||
/// 取出主轴 flex/尺寸策略共同约束下的最小和最大分配范围。
|
||||
fn main_limits(child: &UiNode, row: bool) -> (f32, f32) {
|
||||
let sizing = child.sizing;
|
||||
let (min, max) = if row {
|
||||
@@ -594,6 +637,7 @@ fn main_limits(child: &UiNode, row: bool) -> (f32, f32) {
|
||||
)
|
||||
}
|
||||
|
||||
/// 按权重分配主轴剩余空间,尊重每项 min/max;达到上限的项退出后重新分配剩余量。
|
||||
pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size], preferred: &[Size]) -> Vec<Size> {
|
||||
if matches!(node.kind, UiKind::Overlay { .. }) {
|
||||
let inset = insets(node);
|
||||
@@ -752,6 +796,7 @@ pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size], preferred: &[S
|
||||
output
|
||||
}
|
||||
|
||||
/// 根据最终分配尺寸计算子项原点,处理容器方向、对齐、间距与外边距。
|
||||
pub fn arrange_native(
|
||||
node: &UiNode,
|
||||
x: f32,
|
||||
@@ -887,25 +932,31 @@ pub fn arrange_native(
|
||||
}
|
||||
|
||||
impl LayoutRuntime {
|
||||
/// 从页面模式和当前视口生成根节点约束;无视口时使用明确的无界轴哨兵。
|
||||
pub fn root_constraints(&self) -> [f32; 4] {
|
||||
self.page_mode.map_or_else(
|
||||
|| root_constraints(self.viewport),
|
||||
|mode| mode.constraints(self.viewport),
|
||||
)
|
||||
}
|
||||
/// 撤销指定业务组件的测量缓存,下一次遇到该节点重新求值。
|
||||
pub fn invalidate(&mut self, block: &str) {
|
||||
self.sizes.remove(block);
|
||||
}
|
||||
/// 提高新 Mount ID 的分配下限,避免取消/重试后复用已经暴露过的出现身份。
|
||||
pub fn reserve_identity_floor(&mut self, floor: u64) {
|
||||
self.next_id = self.next_id.max(floor);
|
||||
}
|
||||
/// 返回出现 ID 的当前分配高水位,供桥接层在取消/重试后继续保持唯一性。
|
||||
pub fn identity_high_watermark(&self) -> u64 {
|
||||
self.next_id
|
||||
}
|
||||
|
||||
/// 清空结构出现身份表;业务对象和源状态不因此被复制。
|
||||
pub fn clear_occurrences(&mut self) {
|
||||
self.ids.clear();
|
||||
}
|
||||
/// 在临时 LayoutRuntime 中生成完整几何,成功后才替换现有缓存和身份状态。
|
||||
pub fn frame(&mut self, root: &UiNode) -> Result<Frame> {
|
||||
// Stage all cache/identity changes; a rejected frame leaves the last
|
||||
// valid occurrence registry intact and never publishes partial geometry.
|
||||
@@ -916,6 +967,7 @@ impl LayoutRuntime {
|
||||
}
|
||||
|
||||
/// Allocate stable occurrence identities without measuring or publishing geometry.
|
||||
/// 先建立稳定的出现身份和父子关系,但不把此阶段当成最终测量结果。
|
||||
pub fn structure(&mut self, root: &UiNode) -> Result<Frame> {
|
||||
let mut next = self.clone();
|
||||
let frame = next.build(root, true)?;
|
||||
@@ -923,6 +975,7 @@ impl LayoutRuntime {
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// 以形状、子项尺寸和约束查缓存;未命中才执行测量并更新本轮统计。
|
||||
pub fn measure_node(
|
||||
&mut self,
|
||||
node: &UiNode,
|
||||
@@ -943,6 +996,7 @@ impl LayoutRuntime {
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
/// 迭代展开出现树、计算约束和几何、产生绘制命令;结构准备与完整布局共用此驱动。
|
||||
fn build(&mut self, root: &UiNode, structural: bool) -> Result<Frame> {
|
||||
self.measured = 0;
|
||||
self.arranged = 0;
|
||||
@@ -1091,10 +1145,12 @@ impl LayoutRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 在临时 LayoutRuntime 中生成完整几何,成功后才替换现有缓存和身份状态。
|
||||
pub fn frame(root: &UiNode) -> Result<Frame> {
|
||||
LayoutRuntime::default().frame(root)
|
||||
}
|
||||
|
||||
/// 为指定逻辑视口建立临时布局运行时并求完整帧。
|
||||
pub fn frame_with_viewport(root: &UiNode, viewport: Size) -> Result<Frame> {
|
||||
LayoutRuntime {
|
||||
viewport: Some(viewport),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 渲染模块入口。排版、源视觉协议、相机投影和最终光栅化各自独立,通过不可变帧衔接。
|
||||
//!
|
||||
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
|
||||
pub(crate) mod box_model;
|
||||
pub(crate) mod display_list;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 让原生组件也通过 Endor 的 measure/arrange/paint 协议被调用。
|
||||
//! 将 Constraints、Size、Bounds 与 PaintCommand 在托管值和 Rust 值之间转换;原生方法与源覆盖方法共用调用契约。
|
||||
//!
|
||||
//! Native visual protocol entry points. Values are ordinary managed Endor
|
||||
//! objects; geometry is scoped to a live occurrence, never stored on a Block.
|
||||
use crate::{
|
||||
@@ -12,9 +15,11 @@ use endor_core::{
|
||||
NativeContext, NativeResult, ObjectRef, ObjectStore, Value, Vm, VmError,
|
||||
};
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||||
fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
if let Value::Object(object) = value {
|
||||
Ok(*object)
|
||||
@@ -22,6 +27,7 @@ fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
Err(invalid("visual protocol requires objects"))
|
||||
}
|
||||
}
|
||||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||||
pub(crate) fn number(
|
||||
cx: &NativeContext<'_>,
|
||||
object: ObjectRef,
|
||||
@@ -40,9 +46,11 @@ pub(crate) fn number(
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
/// 将原生布局计算结果封装为协议需要的托管 Value。
|
||||
pub(crate) fn value(value: f32) -> Value {
|
||||
Value::Number((value as f64).into())
|
||||
}
|
||||
/// 新建指定名义类型的托管记录,并逐项设置字段,作为视觉协议的结果返回。
|
||||
pub(crate) fn record(
|
||||
cx: &mut NativeContext<'_>,
|
||||
desktop: ObjectRef,
|
||||
@@ -56,6 +64,7 @@ pub(crate) fn record(
|
||||
}
|
||||
Ok(object)
|
||||
}
|
||||
/// 在约束提供的视口内执行原生布局,从根出现提取宽高,再按协议约束限制结果。
|
||||
fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size, VmError> {
|
||||
let frame = layout::frame_with_viewport(
|
||||
node,
|
||||
@@ -75,6 +84,7 @@ fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size,
|
||||
})
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
|
||||
enum Query {
|
||||
Measure,
|
||||
Arrange,
|
||||
@@ -82,12 +92,14 @@ enum Query {
|
||||
HitTest,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||||
struct Method {
|
||||
query: Query,
|
||||
desktop: ObjectRef,
|
||||
signature: CallSignature,
|
||||
}
|
||||
/// Only this actual implementation can use the matching intrinsic fast path.
|
||||
/// 判断方法是否为匹配协议的宿主原生实现,决定能否安全使用快速路径。
|
||||
pub(crate) fn is_intrinsic(
|
||||
store: &ObjectStore,
|
||||
function: ObjectRef,
|
||||
@@ -122,13 +134,16 @@ pub(crate) fn is_intrinsic(
|
||||
}
|
||||
|
||||
impl NativeCallable for Method {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(3))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [owner, context, input] = args else {
|
||||
return Err(invalid("visual method expects a Mount and an input value"));
|
||||
@@ -352,15 +367,18 @@ impl NativeCallable for Method {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Point 的矩形/圆形命中辅助方法,保存结果类型和具体几何判定种类。
|
||||
struct PointMethod {
|
||||
expected: ObjectRef,
|
||||
circle: bool,
|
||||
signature: CallSignature,
|
||||
}
|
||||
impl NativeCallable for PointMethod {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(if self.circle {
|
||||
4
|
||||
@@ -369,6 +387,7 @@ impl NativeCallable for PointMethod {
|
||||
}))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let Some(Value::Object(point)) = args.first() else {
|
||||
return Err(invalid("Point method requires a Point"));
|
||||
@@ -419,6 +438,7 @@ impl NativeCallable for PointMethod {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub fn install(
|
||||
vm: &mut Vm,
|
||||
roots: &mut Vec<ObjectRef>,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 旧 Page.mode 的兼容空间策略,以及页面级的正逆相机变换。
|
||||
//! 新组合应优先使用普通布局、ScrollView 和独立场景视口;保留这里是为了兼容已有 window/vertical/horizontal/canvas 页面。
|
||||
//!
|
||||
//! Page space is independent of the native window. The camera is applied once
|
||||
//! for painting and inverted once for input; layout and Mounts stay in world units.
|
||||
use crate::{geometry::Point, layout::Size};
|
||||
@@ -6,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
/// 旧 Page 的有限或自由空间模式,新场景视口不依赖它表达布局方向。
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Window,
|
||||
@@ -15,6 +19,7 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Window => "window",
|
||||
@@ -24,6 +29,7 @@ impl Mode {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(name: &str) -> Result<Self, VmError> {
|
||||
match name {
|
||||
"window" => Ok(Self::Window),
|
||||
@@ -36,6 +42,7 @@ impl Mode {
|
||||
}
|
||||
}
|
||||
|
||||
/// 为旧页面模式生成有限轴和可延伸轴的根约束。
|
||||
pub fn constraints(self, viewport: Option<Size>) -> [f32; 4] {
|
||||
let Some(size) = viewport else {
|
||||
return crate::layout::root_constraints(None);
|
||||
@@ -51,6 +58,7 @@ impl Mode {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
/// 页面级兼容相机状态和有限内容范围,支持绘制与命中正逆变换。
|
||||
pub struct Surface {
|
||||
pub mode: Mode,
|
||||
pub page: String,
|
||||
@@ -60,11 +68,13 @@ pub struct Surface {
|
||||
pub extent: Size,
|
||||
}
|
||||
|
||||
/// 生成页面相机计算所需的单位比例或边界默认值。
|
||||
fn unit() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
impl Default for Surface {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode: Mode::Window,
|
||||
@@ -77,6 +87,7 @@ impl Default for Surface {
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(mode: Mode) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
@@ -85,6 +96,7 @@ impl Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把页面视图坐标反算为旧 Page 相机下的世界坐标。
|
||||
pub fn to_world(&self, point: Point) -> Point {
|
||||
Point::new(
|
||||
point.x / self.zoom + self.pan.x,
|
||||
@@ -92,6 +104,7 @@ impl Surface {
|
||||
)
|
||||
}
|
||||
|
||||
/// 把旧页面世界坐标变换到可见视图坐标。
|
||||
pub fn to_view(&self, point: Point) -> Point {
|
||||
Point::new(
|
||||
(point.x - self.pan.x) * self.zoom,
|
||||
@@ -99,6 +112,7 @@ impl Surface {
|
||||
)
|
||||
}
|
||||
|
||||
/// 判断给定点是否位于目标范围;需要相机转换的类型会先转换再判断。
|
||||
pub fn contains(&self, point: [f32; 2], viewport: Option<Size>) -> bool {
|
||||
let point = self.to_view(Point::new(point[0], point[1]));
|
||||
point.x >= 0.0
|
||||
@@ -106,6 +120,7 @@ impl Surface {
|
||||
&& viewport.is_none_or(|size| point.x < size.width && point.y < size.height)
|
||||
}
|
||||
|
||||
/// 只对有限页面按实际内容范围钳制偏移;自由画布不按内容边缘停止导航。
|
||||
pub fn clamp(&mut self, viewport: Size) {
|
||||
match self.mode {
|
||||
Mode::Window => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 合成整个原生窗口的显示列表:页面画面、宿主侧栏和开发工具绘制。
|
||||
//! 窗口逻辑尺寸与设备像素比例在这里衔接。页面内容先应用其视角并裁剪,宿主控件的区域和命中布局另外返回。
|
||||
//!
|
||||
use crate::{
|
||||
activity::Activity,
|
||||
developer_tools::{self, DeveloperToolsLayout, Workspace as DeveloperToolsState},
|
||||
@@ -7,10 +10,12 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 宿主管理区域的几何布局,供窗口事件分辨页面与工具命中。
|
||||
pub struct SceneLayout {
|
||||
pub developer_tools: DeveloperToolsLayout,
|
||||
}
|
||||
|
||||
/// 组合页面、开发工具和侧栏的设备像素显示列表,同时返回供命中使用的宿主布局。
|
||||
pub fn build_scene(
|
||||
width: u32,
|
||||
height: u32,
|
||||
@@ -84,6 +89,7 @@ pub fn build_scene(
|
||||
}
|
||||
}
|
||||
|
||||
/// 整窗合成结果:显示列表与同帧管理交互几何一起返回。
|
||||
pub struct DisplayScene {
|
||||
pub display_list: DisplayList,
|
||||
pub layout: SceneLayout,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 有界 CPU 3D 渲染器。使用相同摄像机与世界变换进行视锥裁剪、深度测试、纹理采样和射线拾取。
|
||||
//! 不可变 View 可以缓存光栅结果;相机变化会产生新视图。当前不是 GPU 后端,像素和光栅工作预算用于限制分配与循环工作量。
|
||||
//! 面板 UI 先渲染为有限纹理,再以透视校正 UV 映射进世界。与相机相关的投影宽高比由具体视口提供。
|
||||
//!
|
||||
//! Software 3D surfaces share the application's existing CPU compositor. Mesh
|
||||
//! visibility and picking use the same camera and transformed world triangles.
|
||||
pub mod math;
|
||||
@@ -11,6 +15,7 @@ pub const MAX_TRIANGLES: usize = 65_536;
|
||||
pub const MAX_VERTICES: usize = 32_768;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 不可变顶点、三角形和可选 UV;多个网格可共享同一数值缓冲。
|
||||
pub struct Geometry {
|
||||
pub vertices: Vec<Vec3>,
|
||||
pub triangles: Vec<[u32; 3]>,
|
||||
@@ -18,6 +23,7 @@ pub struct Geometry {
|
||||
pub uv: Vec<[f32; 2]>,
|
||||
}
|
||||
impl Geometry {
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(&self) -> bool {
|
||||
self.vertices.len() <= MAX_VERTICES
|
||||
&& (self.uv.is_empty()
|
||||
@@ -31,6 +37,7 @@ impl Geometry {
|
||||
.flatten()
|
||||
.all(|&index| (index as usize) < self.vertices.len())
|
||||
}
|
||||
/// 生成以原点为中心的盒体顶点和三角形索引;位置和旋转由网格世界矩阵另行描述。
|
||||
pub fn cuboid(width: f32, height: f32, depth: f32) -> Self {
|
||||
let (x, y, z) = (width / 2., height / 2., depth / 2.);
|
||||
Self {
|
||||
@@ -63,6 +70,7 @@ impl Geometry {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 一次场景实例:几何、世界矩阵、颜色与可选 UI 面板,身份用于拾取回关联。
|
||||
pub struct Mesh {
|
||||
pub identity: String,
|
||||
pub geometry: Arc<Geometry>,
|
||||
@@ -72,12 +80,14 @@ pub struct Mesh {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 面板内普通 UI 的绘制命令及其裁剪/二维变换,逻辑排版不随外部相机改变。
|
||||
pub struct UiSurface {
|
||||
pub commands: Vec<crate::document::RenderCommand>,
|
||||
pub clips: Vec<Option<[f32; 4]>>,
|
||||
pub views: Vec<Option<crate::viewport::Transform>>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 世界平面的有限 UI 纹理描述,包含像素尺寸、背景与内部显示内容。
|
||||
pub struct Panel {
|
||||
pub key: u64,
|
||||
pub width: u32,
|
||||
@@ -86,12 +96,14 @@ pub struct Panel {
|
||||
pub surface: Option<Arc<UiSurface>>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 方向光的原生数值,方向和强度在场景快照阶段校验。
|
||||
pub struct Light {
|
||||
pub direction: Vec3,
|
||||
pub color: [f32; 3],
|
||||
pub intensity: f32,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
/// 不含摄像机的世界实例和光照集合;BVH 是可丢弃的派生缓存。
|
||||
pub struct Scene {
|
||||
pub meshes: Vec<Mesh>,
|
||||
pub lights: Vec<Light>,
|
||||
@@ -101,6 +113,7 @@ pub struct Scene {
|
||||
pub acceleration: Arc<spatial::Index>,
|
||||
}
|
||||
impl PartialEq for Scene {
|
||||
/// 比较会影响场景结果的数据;加速索引属于派生缓存,不参与场景语义相等。
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.meshes == other.meshes
|
||||
&& self.lights == other.lights
|
||||
@@ -109,21 +122,26 @@ impl PartialEq for Scene {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// 显式相机与共享世界的组合,一个 Scene 可由多个独立 View 观察。
|
||||
pub struct View {
|
||||
pub scene: Arc<Scene>,
|
||||
pub camera: Camera,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 只读绘制结果使用的名义包装,让源 PaintCommand 可以携带不可变 3D 快照。
|
||||
pub struct PaintView(pub Arc<View>);
|
||||
impl endor_core::ObjectPayload for PaintView {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"immutable-3d-paint"
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 射线与实际三角形的精确交点,含实例身份、距离、法线和重心坐标。
|
||||
pub struct Hit {
|
||||
pub instance: usize,
|
||||
pub identity: String,
|
||||
@@ -135,10 +153,13 @@ pub struct Hit {
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(&self) -> bool {
|
||||
thread_local! { static DEPTH:std::cell::Cell<usize>=const { std::cell::Cell::new(0) }; }
|
||||
// 验证可能递归进入面板内的 3D;作用域结束时归还深度,错误返回也不泄漏计数。
|
||||
struct Guard;
|
||||
impl Drop for Guard {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
DEPTH.with(|depth| depth.set(depth.get() - 1));
|
||||
}
|
||||
@@ -219,6 +240,7 @@ impl View {
|
||||
&& (0.0..=32.0).contains(&light.intensity)
|
||||
})
|
||||
}
|
||||
/// 按包围盒候选发射射线,再检查三角形;距离相同时保留与绘制顺序一致的实例。
|
||||
pub fn pick(&self, u: f32, v: f32, aspect: f32) -> Option<Hit> {
|
||||
if !self.camera.valid()
|
||||
|| !(0.0..=1.0).contains(&u)
|
||||
@@ -278,13 +300,16 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把有限颜色通道转换成最终像素使用的字节。
|
||||
fn channel(value: f32) -> u32 {
|
||||
(value.clamp(0., 1.) * 255.).round() as u32
|
||||
}
|
||||
/// 将三个颜色通道打包为软件像素缓冲使用的 RGB 值。
|
||||
pub fn rgb(color: [f32; 3]) -> u32 {
|
||||
channel(color[0]) << 16 | channel(color[1]) << 8 | channel(color[2])
|
||||
}
|
||||
|
||||
/// 某个不可变视图在特定分辨率和宽高比下的光栅结果。
|
||||
struct CachedSurface {
|
||||
view: Arc<View>,
|
||||
width: u32,
|
||||
@@ -293,11 +318,13 @@ struct CachedSurface {
|
||||
pixels: Arc<[u32]>,
|
||||
}
|
||||
#[derive(Default)]
|
||||
/// 限制条目数和总像素数的表面缓存;不承担场景业务所有权。
|
||||
struct SurfaceCache {
|
||||
entries: std::collections::VecDeque<CachedSurface>,
|
||||
pixels: usize,
|
||||
}
|
||||
|
||||
/// 按不可变视图身份、分辨率和投影比例查光栅缓存;未命中时在锁外绘制,支持嵌套世界 UI。
|
||||
pub fn surface(
|
||||
view: &Arc<View>,
|
||||
width: u32,
|
||||
@@ -341,11 +368,13 @@ pub fn surface(
|
||||
/// Homogeneous frustum planes expressed in view space. Clipping before division
|
||||
/// handles triangles crossing the near plane without huge screen coordinates.
|
||||
#[derive(Clone, Copy)]
|
||||
/// 裁剪过程中的相机空间顶点,位置与 UV 必须共同插值。
|
||||
struct Vertex {
|
||||
position: Vec3,
|
||||
uv: [f32; 2],
|
||||
}
|
||||
impl Vertex {
|
||||
/// 按裁剪交点比例同时插值位置和 UV,保持新顶点的几何与纹理坐标一致。
|
||||
fn lerp(self, other: Self, t: f32) -> Self {
|
||||
Self {
|
||||
position: self.position + (other.position - self.position) * t,
|
||||
@@ -356,6 +385,7 @@ impl Vertex {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 生成相机空间六个视锥平面,供三角形裁剪与 BVH 可见性查询共用。
|
||||
fn frustum_planes(camera: Camera, aspect: f32) -> [[f32; 4]; 6] {
|
||||
let (x, y, slope) = match camera.projection {
|
||||
Projection::Perspective { fov } => {
|
||||
@@ -373,6 +403,7 @@ fn frustum_planes(camera: Camera, aspect: f32) -> [[f32; 4]; 6] {
|
||||
[0., -1., y * slope, y * (1. - slope)],
|
||||
]
|
||||
}
|
||||
/// 在透视除法前逐平面裁剪多边形,并同步插值 UV,避免跨近面顶点投影到极大坐标。
|
||||
fn clip(mut polygon: Vec<Vertex>, camera: Camera, aspect: f32) -> Vec<Vertex> {
|
||||
for [a, b, c, d] in frustum_planes(camera, aspect) {
|
||||
if polygon.is_empty() {
|
||||
@@ -402,9 +433,11 @@ fn clip(mut polygon: Vec<Vertex>, camera: Camera, aspect: f32) -> Vec<Vertex> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// 把当前绘制输入转换为输出表面;分配、裁剪和预算由本实现检查。
|
||||
pub fn render(view: &View, width: u32, height: u32) -> Result<Vec<u32>, &'static str> {
|
||||
render_aspect(view, width, height, width as f32 / height as f32)
|
||||
}
|
||||
/// 先校验并预留颜色/深度缓冲预算,再剔除、裁剪、投影和光栅化三角形;超预算返回错误。
|
||||
fn render_aspect(
|
||||
view: &View,
|
||||
width: u32,
|
||||
@@ -448,6 +481,7 @@ fn render_aspect(
|
||||
temporary = spatial::Index::build(&view.scene.meshes);
|
||||
&temporary
|
||||
};
|
||||
// 宽阶段只筛选候选:离屏网格不进入后续纹理分配和逐三角形处理。
|
||||
for instance in index.visible(camera, aspect) {
|
||||
let mesh = &view.scene.meshes[instance];
|
||||
let texture = mesh.panel.as_ref().map(panel::texture).transpose()?;
|
||||
@@ -464,6 +498,7 @@ fn render_aspect(
|
||||
}
|
||||
}
|
||||
let color = rgb(std::array::from_fn(|i| mesh.color[i] * light[i]));
|
||||
// 在透视除法之前裁剪,并插入具有正确 UV 的交点;不能先投影再裁剪近面。
|
||||
let polygon = clip(
|
||||
indices
|
||||
.map(|index| Vertex {
|
||||
@@ -485,6 +520,7 @@ fn render_aspect(
|
||||
if polygon.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
// 裁剪后可能多于三个顶点,用扇形重新三角化,保持同一平面的纹理参数。
|
||||
for index in 1..polygon.len() - 1 {
|
||||
let vertices = [polygon[0], polygon[index], polygon[index + 1]];
|
||||
let [a, b, c] = vertices.map(|vertex| project(vertex.position));
|
||||
@@ -515,16 +551,19 @@ fn render_aspect(
|
||||
if weights.iter().any(|weight| *weight < -1e-5) {
|
||||
continue;
|
||||
}
|
||||
// 透视投影下使用倒深度插值,正交投影下可直接线性插值。
|
||||
let depth = if perspective {
|
||||
1. / (weights[0] / a.z + weights[1] / b.z + weights[2] / c.z)
|
||||
} else {
|
||||
weights[0] * a.z + weights[1] * b.z + weights[2] * c.z
|
||||
};
|
||||
let index = y as usize * width as usize + x as usize;
|
||||
// 只让最近表面写像素;等深度保留先绘制实例,与拾取的排序约定一致。
|
||||
if depth < depths[index] {
|
||||
depths[index] = depth;
|
||||
pixels[index] =
|
||||
if let (Some(texture), Some(panel)) = (&texture, &mesh.panel) {
|
||||
// UV 也要除以深度再插值,否则倾斜面板会出现纹理拉伸失真。
|
||||
let interpolated = |axis| {
|
||||
if perspective {
|
||||
depth
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
//! 3D 数学基础:向量、仿射矩阵、摄像机基向量、投影射线和三角形相交。
|
||||
//! 矩阵组合顺序决定局部变换如何进入父坐标系;接近退化的逆矩阵、法线和射线使用 Option 拒绝无效计算。
|
||||
//!
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
|
||||
/// 三个有限浮点分量的数学向量,不是托管 Vec3 对象。
|
||||
pub struct Vec3 {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
}
|
||||
impl Vec3 {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub const fn new(x: f32, y: f32, z: f32) -> Self {
|
||||
Self { x, y, z }
|
||||
}
|
||||
/// 计算向量点积,用于方向投影、光照和相交判断。
|
||||
pub fn dot(self, b: Self) -> f32 {
|
||||
self.x * b.x + self.y * b.y + self.z * b.z
|
||||
}
|
||||
/// 计算右手叉积,用于摄像机基向量和三角形法线。
|
||||
pub fn cross(self, b: Self) -> Self {
|
||||
Self::new(
|
||||
self.y * b.z - self.z * b.y,
|
||||
@@ -21,13 +28,16 @@ impl Vec3 {
|
||||
self.x * b.y - self.y * b.x,
|
||||
)
|
||||
}
|
||||
/// 计算向量长度;归一化前还需检查是否接近零。
|
||||
pub fn length(self) -> f32 {
|
||||
self.dot(self).sqrt()
|
||||
}
|
||||
/// 仅在长度有效且不退化时返回单位向量,避免除零产生非有限结果。
|
||||
pub fn normalized(self) -> Option<Self> {
|
||||
let length = self.length();
|
||||
(length.is_finite() && length > 1e-6).then(|| self * (1.0 / length))
|
||||
}
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(self) -> bool {
|
||||
[self.x, self.y, self.z]
|
||||
.iter()
|
||||
@@ -36,26 +46,31 @@ impl Vec3 {
|
||||
}
|
||||
impl Add for Vec3 {
|
||||
type Output = Self;
|
||||
/// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。
|
||||
fn add(self, b: Self) -> Self {
|
||||
Self::new(self.x + b.x, self.y + b.y, self.z + b.z)
|
||||
}
|
||||
}
|
||||
impl Sub for Vec3 {
|
||||
type Output = Self;
|
||||
/// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。
|
||||
fn sub(self, b: Self) -> Self {
|
||||
Self::new(self.x - b.x, self.y - b.y, self.z - b.z)
|
||||
}
|
||||
}
|
||||
impl Mul<f32> for Vec3 {
|
||||
type Output = Self;
|
||||
/// 按分量执行向量加减或标量乘法,保持数学运算不依赖 VM 状态。
|
||||
fn mul(self, b: f32) -> Self {
|
||||
Self::new(self.x * b, self.y * b, self.z * b)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// 仿射 4×4 变换矩阵,支持局部到世界的组合与求逆。
|
||||
pub struct Matrix(pub [f32; 16]);
|
||||
impl Default for Matrix {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self([
|
||||
1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
|
||||
@@ -63,6 +78,7 @@ impl Default for Matrix {
|
||||
}
|
||||
}
|
||||
impl Matrix {
|
||||
/// 求仿射世界矩阵的逆;退化或不可逆时返回空值。
|
||||
pub fn inverse(self) -> Option<Self> {
|
||||
let m = self.0;
|
||||
let determinant = m[0] * (m[5] * m[10] - m[6] * m[9]) - m[1] * (m[4] * m[10] - m[6] * m[8])
|
||||
@@ -95,6 +111,7 @@ impl Matrix {
|
||||
out.0[11] = translation.z;
|
||||
Some(out)
|
||||
}
|
||||
/// 组合父子仿射变换,调用顺序决定局部坐标最终落在哪个世界位置。
|
||||
pub fn compose(self, local: Self) -> Self {
|
||||
let mut out = [0.; 16];
|
||||
for row in 0..4 {
|
||||
@@ -106,6 +123,7 @@ impl Matrix {
|
||||
}
|
||||
Self(out)
|
||||
}
|
||||
/// 对一个位置应用矩阵,包括平移项;不要用它代替纯方向变换。
|
||||
pub fn point(self, p: Vec3) -> Vec3 {
|
||||
Vec3::new(
|
||||
self.0[0] * p.x + self.0[1] * p.y + self.0[2] * p.z + self.0[3],
|
||||
@@ -113,6 +131,7 @@ impl Matrix {
|
||||
self.0[8] * p.x + self.0[9] * p.y + self.0[10] * p.z + self.0[11],
|
||||
)
|
||||
}
|
||||
/// 由平移、角度旋转和缩放构造局部仿射矩阵。
|
||||
pub fn trs(position: Vec3, rotation: Vec3, scale: Vec3) -> Self {
|
||||
let (sx, cx) = rotation.x.to_radians().sin_cos();
|
||||
let (sy, cy) = rotation.y.to_radians().sin_cos();
|
||||
@@ -140,12 +159,14 @@ impl Matrix {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// 投影策略:透视视场角或正交可见高度。
|
||||
pub enum Projection {
|
||||
Perspective { fov: f32 },
|
||||
Orthographic { height: f32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// 原生摄像机描述,包括观察基、近远面和投影参数;宽高比由视口提供。
|
||||
pub struct Camera {
|
||||
pub position: Vec3,
|
||||
pub target: Vec3,
|
||||
@@ -155,6 +176,7 @@ pub struct Camera {
|
||||
pub projection: Projection,
|
||||
}
|
||||
impl Default for Camera {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: Vec3::new(0., 0., 5.),
|
||||
@@ -167,18 +189,21 @@ impl Default for Camera {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 摄像机的右、上、前单位基向量。
|
||||
pub struct Basis {
|
||||
pub right: Vec3,
|
||||
pub up: Vec3,
|
||||
pub forward: Vec3,
|
||||
}
|
||||
impl Camera {
|
||||
/// 从观察位置、目标和上方向建立正交相机基;方向退化时拒绝。
|
||||
pub fn basis(self) -> Option<Basis> {
|
||||
let forward = (self.target - self.position).normalized()?;
|
||||
let right = forward.cross(self.up).normalized()?;
|
||||
let up = right.cross(forward).normalized()?;
|
||||
Some(Basis { right, up, forward })
|
||||
}
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(self) -> bool {
|
||||
self.position.valid()
|
||||
&& self.target.valid()
|
||||
@@ -196,10 +221,12 @@ impl Camera {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 把世界点投影到相机的右/上/前基向量坐标。
|
||||
pub fn view(self, basis: Basis, p: Vec3) -> Vec3 {
|
||||
let p = p - self.position;
|
||||
Vec3::new(p.dot(basis.right), p.dot(basis.up), p.dot(basis.forward))
|
||||
}
|
||||
/// 从视口归一化坐标生成透视或正交射线;宽高比由具体视口提供。
|
||||
pub fn ray(self, u: f32, v: f32, aspect: f32) -> Option<Ray> {
|
||||
let basis = self.basis()?;
|
||||
let (x, y) = (u * 2. - 1., 1. - v * 2.);
|
||||
@@ -224,11 +251,13 @@ impl Camera {
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 世界坐标中的射线原点与方向,供网格和面板求交。
|
||||
pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
}
|
||||
impl Ray {
|
||||
/// 求射线与三角形相交距离和重心坐标,拒绝退化三角形及射线起点后方的交点。
|
||||
pub fn triangle(self, a: Vec3, b: Vec3, c: Vec3) -> Option<(f32, [f32; 3])> {
|
||||
let ab = b - a;
|
||||
let ac = c - a;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 连接 3D 平面与普通 UI 的坐标和绘制桥梁。
|
||||
//! Mapping 沿嵌套面板逐层把页面指针变成纹理坐标;可见命中还须检查最近射线交点,防止点穿遮挡物。
|
||||
//! compose 把各 Mount 的绘制命令按所属面板分组并递归装配;texture 再调用普通软件渲染器绘制面板内容。
|
||||
//!
|
||||
//! Perspective UI mapping. The same world plane supplies texture coordinates
|
||||
//! during rendering and inverse pointer coordinates during event dispatch.
|
||||
use super::{
|
||||
@@ -11,6 +15,7 @@ use crate::{
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 一次面板出现的正逆投影信息,可以链接父面板形成嵌套映射。
|
||||
pub struct Mapping {
|
||||
pub owner: u64,
|
||||
pub key: u64,
|
||||
@@ -21,6 +26,7 @@ pub struct Mapping {
|
||||
pub parent: Option<Arc<Mapping>>,
|
||||
}
|
||||
impl Mapping {
|
||||
/// 沿父面板逆投影到当前平面的 UI 像素;要求可见时使用最近射线命中拒绝被遮挡的目标。
|
||||
pub fn texture_point(&self, point: Point, clipped: bool) -> Option<Point> {
|
||||
let point = if let Some(parent) = &self.parent {
|
||||
parent.texture_point(point, clipped)?
|
||||
@@ -47,6 +53,7 @@ impl Mapping {
|
||||
})?;
|
||||
let panel = mesh.panel.as_ref()?;
|
||||
if clipped {
|
||||
// 指针落在平面投影内还不够:最近三角形必须属于本面板,不能点穿前方网格。
|
||||
let hit = self.view.pick(u, v, width / height)?;
|
||||
if self.view.scene.meshes[hit.instance]
|
||||
.panel
|
||||
@@ -56,6 +63,7 @@ impl Mapping {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
// 捕获拖动可以不要求指针仍位于面板内,但仍使用原面板平面的连续逆映射。
|
||||
let ray = self.view.camera.ray(u, v, width / height)?;
|
||||
let center = mesh.world.point(Vec3::default());
|
||||
let normal = (mesh.world.point(Vec3::new(1., 0., 0.)) - center)
|
||||
@@ -81,6 +89,7 @@ impl Mapping {
|
||||
(point.x.is_finite() && point.y.is_finite()).then_some(point)
|
||||
}
|
||||
|
||||
/// 把当前面板的 UI 像素正向投影到外层页面空间,供坐标查询和交互测试使用。
|
||||
pub fn project_point(&self, point: Point) -> Option<Point> {
|
||||
let mesh = self.view.scene.meshes.iter().find(|mesh| {
|
||||
mesh.panel
|
||||
@@ -118,6 +127,7 @@ impl Mapping {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// 归属明确的一条内部 UI 绘制命令,装配时还保留裁剪、变换和 owner。
|
||||
struct Entry {
|
||||
command: crate::document::RenderCommand,
|
||||
clip: Option<[f32; 4]>,
|
||||
@@ -127,12 +137,14 @@ struct Entry {
|
||||
|
||||
/// Collect a panel's ordinary paint subtree into its texture. Nested 3D
|
||||
/// commands are assembled recursively before their containing texture.
|
||||
/// 按面板所属视口和稳定 key 分组绘制命令,递归将内部 UI 装配进 3D 面板表面。
|
||||
pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
use crate::document::RenderCommand;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
let mounts: HashMap<_, _> = frame.mounts.iter().map(|mount| (mount.id, mount)).collect();
|
||||
let mut groups = HashMap::<(u64, u64), Vec<Entry>>::new();
|
||||
let mut roots = Vec::new();
|
||||
// 每条命令都有出现归属;据此分流为窗口根命令或某个世界面板的内部命令。
|
||||
for (index, command) in frame.commands.iter().enumerate() {
|
||||
let owner = frame.command_mounts[index];
|
||||
let mount = mounts[&owner];
|
||||
@@ -172,6 +184,7 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
owner: mount.id,
|
||||
});
|
||||
}
|
||||
/// 递归装配一个绘制条目包含的世界 UI,检查嵌套环与深度,再生成独立不可变视图。
|
||||
fn assemble(
|
||||
entry: Entry,
|
||||
groups: &HashMap<(u64, u64), Vec<Entry>>,
|
||||
@@ -186,6 +199,7 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
depth < 8 && path.insert(entry.owner),
|
||||
"3D UI paint nesting cycle or depth budget exceeded"
|
||||
);
|
||||
// 只构造新的原生快照,不写回源 Scene3D;同一业务场景仍可被其他视口共享。
|
||||
let mut scene = (*view.scene).clone();
|
||||
for mesh in &mut scene.meshes {
|
||||
if let Some(panel) = &mut mesh.panel {
|
||||
@@ -226,6 +240,7 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 为一个面板分配有限像素表面,并调用普通 UI 软件渲染;当前实现不会按面板内容独立缓存纹理。
|
||||
pub fn texture(panel: &super::Panel) -> Result<Vec<u32>, &'static str> {
|
||||
crate::software_renderer::reserve_pixels(panel.width as usize * panel.height as usize)?;
|
||||
let mut pixels =
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 不可变世界空间 BVH。用网格变换后的包围盒快速筛选视锥与射线候选。
|
||||
//! 空间索引只做宽阶段筛选;最终拾取仍检查真实三角形。相机变化可复用索引,几何或变换变化需要新索引。
|
||||
//!
|
||||
//! Immutable world-space BVH. Camera changes reuse it; it contains no VM data.
|
||||
use super::{
|
||||
Mesh,
|
||||
@@ -5,11 +8,13 @@ use super::{
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 变换后网格的世界空间 AABB,做保守筛选而不是精确表面描述。
|
||||
pub struct Bounds {
|
||||
pub min: Vec3,
|
||||
pub max: Vec3,
|
||||
}
|
||||
impl Bounds {
|
||||
/// 变换网格顶点并求世界 AABB,作为空间索引的保守外包围。
|
||||
fn mesh(mesh: &Mesh) -> Option<Self> {
|
||||
let mut points = mesh
|
||||
.geometry
|
||||
@@ -36,6 +41,7 @@ impl Bounds {
|
||||
},
|
||||
))
|
||||
}
|
||||
/// 求两个包围盒的最小共同外包围,供 BVH 父节点使用。
|
||||
fn union(self, other: Self) -> Self {
|
||||
Self {
|
||||
min: Vec3::new(
|
||||
@@ -50,11 +56,13 @@ impl Bounds {
|
||||
),
|
||||
}
|
||||
}
|
||||
/// 取指定轴中心位置,供最长轴排序和分割使用。
|
||||
fn center(self, axis: usize) -> f32 {
|
||||
let min = [self.min.x, self.min.y, self.min.z];
|
||||
let max = [self.max.x, self.max.y, self.max.z];
|
||||
(min[axis] + max[axis]) / 2.
|
||||
}
|
||||
/// 用包围盒筛出射线候选;索引查询按进入距离排序,精确三角形求交在调用方完成。
|
||||
fn ray(self, ray: Ray) -> Option<f32> {
|
||||
let mut near = 0f32;
|
||||
let mut far = f32::INFINITY;
|
||||
@@ -79,6 +87,7 @@ impl Bounds {
|
||||
}
|
||||
Some(near)
|
||||
}
|
||||
/// 用相机六平面保守判断包围盒可见性,返回保持原场景顺序的候选。
|
||||
fn visible(self, camera: Camera, basis: Basis, planes: &[[f32; 4]; 6]) -> bool {
|
||||
let corners: [Vec3; 8] = std::array::from_fn(|index| {
|
||||
camera.view(
|
||||
@@ -110,6 +119,7 @@ impl Bounds {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// BVH 叶或分支;叶保存实例索引,分支保存两个子节点索引。
|
||||
enum Node {
|
||||
Leaf {
|
||||
bounds: Bounds,
|
||||
@@ -122,6 +132,7 @@ enum Node {
|
||||
},
|
||||
}
|
||||
impl Node {
|
||||
/// 读取叶/分支统一的包围盒,隐藏节点种类差异。
|
||||
fn bounds(&self) -> Bounds {
|
||||
match self {
|
||||
Self::Leaf { bounds, .. } | Self::Branch { bounds, .. } => *bounds,
|
||||
@@ -130,12 +141,14 @@ impl Node {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 由不可变场景建立的包围盒树,供相机视锥和射线查询复用。
|
||||
pub struct Index {
|
||||
bounds: Vec<Option<Bounds>>,
|
||||
nodes: Vec<Node>,
|
||||
root: Option<usize>,
|
||||
}
|
||||
impl Index {
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub fn build(meshes: &[Mesh]) -> Self {
|
||||
let bounds: Vec<_> = meshes.iter().map(Bounds::mesh).collect();
|
||||
let mut items: Vec<_> = bounds
|
||||
@@ -153,6 +166,7 @@ impl Index {
|
||||
}
|
||||
index
|
||||
}
|
||||
/// 按包围盒最长轴的中心排序并递归二分,叶节点保留少量网格候选。
|
||||
fn split(&mut self, items: &mut [usize]) -> usize {
|
||||
let bounds = items
|
||||
.iter()
|
||||
@@ -196,9 +210,11 @@ impl Index {
|
||||
}
|
||||
index
|
||||
}
|
||||
/// 检查索引覆盖的网格数量是否与场景相符;缺失缓存时允许重建。
|
||||
pub fn matches(&self, meshes: usize) -> bool {
|
||||
self.bounds.len() == meshes
|
||||
}
|
||||
/// 用包围盒筛出射线候选;索引查询按进入距离排序,精确三角形求交在调用方完成。
|
||||
pub fn ray(&self, ray: Ray) -> Vec<(f32, usize)> {
|
||||
let mut stack: Vec<_> = self.root.into_iter().collect();
|
||||
let mut output = Vec::new();
|
||||
@@ -223,6 +239,7 @@ impl Index {
|
||||
output.sort_unstable_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
|
||||
output
|
||||
}
|
||||
/// 用相机六平面保守判断包围盒可见性,返回保持原场景顺序的候选。
|
||||
pub fn visible(&self, camera: Camera, aspect: f32) -> Vec<usize> {
|
||||
let Some(basis) = camera.basis() else {
|
||||
return Vec::new();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 逐 Mount 的滚动状态、裁剪和输入目标选择。
|
||||
//! 共享组件的不同出现保留独立偏移。嵌套滚动先选择可接收位移的内层,偏移在实际内容范围内钳制。
|
||||
//!
|
||||
//! Per-occurrence scroll state and a single geometry/clip projection shared by
|
||||
//! native paint, source paint and input. No business object is copied.
|
||||
use crate::{
|
||||
@@ -12,6 +15,7 @@ use endor_core::{NativeContext, ObjectRef, Value, Vm, VmError, WeakObjectRef};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
/// 滚动容器允许响应的轴,不决定其业务内容组成方向。
|
||||
pub enum Axis {
|
||||
#[default]
|
||||
Both,
|
||||
@@ -19,6 +23,7 @@ pub enum Axis {
|
||||
Horizontal,
|
||||
}
|
||||
impl Axis {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Both => "both",
|
||||
@@ -26,6 +31,7 @@ impl Axis {
|
||||
Self::Horizontal => "horizontal",
|
||||
}
|
||||
}
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(name: &str) -> Result<Self, VmError> {
|
||||
match name {
|
||||
"both" => Ok(Self::Both),
|
||||
@@ -36,14 +42,17 @@ impl Axis {
|
||||
)),
|
||||
}
|
||||
}
|
||||
/// 判断滚动轴配置是否允许该方向移动,避免同时把禁用轴纳入滚动范围。
|
||||
pub fn horizontal(self) -> bool {
|
||||
self != Self::Vertical
|
||||
}
|
||||
/// 判断滚动轴配置是否允许该方向移动,避免同时把禁用轴纳入滚动范围。
|
||||
pub fn vertical(self) -> bool {
|
||||
self != Self::Horizontal
|
||||
}
|
||||
}
|
||||
|
||||
/// 求两个裁剪矩形的交集,空区域仍保持合法非负尺寸。
|
||||
fn intersection(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
let x = a[0].max(b[0]);
|
||||
let y = a[1].max(b[1]);
|
||||
@@ -54,6 +63,7 @@ fn intersection(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
(a[1] + a[3]).min(b[1] + b[3]).max(y) - y,
|
||||
]
|
||||
}
|
||||
/// 沿出现树累计滚动偏移和裁剪;遇到独立场景视口时按其有限边界重新建立投影范围。
|
||||
pub fn project(vm: &mut Vm, table: &MountTable, root: &UiNode, frame: &mut Frame) -> Result<()> {
|
||||
let mut nodes = HashMap::new();
|
||||
let mut work = vec![root];
|
||||
@@ -201,6 +211,7 @@ pub fn project(vm: &mut Vm, table: &MountTable, root: &UiNode, frame: &mut Frame
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 把源滚动值解码为有限坐标,阻止非数或越界值进入投影。
|
||||
pub fn coordinate(value: &Value) -> Result<f32, VmError> {
|
||||
let Value::Number(value) = value else {
|
||||
return Err(VmError::InvalidArgument(
|
||||
@@ -216,6 +227,7 @@ pub fn coordinate(value: &Value) -> Result<f32, VmError> {
|
||||
}
|
||||
Ok(n)
|
||||
}
|
||||
/// 更新指定出现的滚动偏移,并钳制到本轴实际可滚动的内容范围。
|
||||
pub fn set(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -249,6 +261,7 @@ pub fn set(
|
||||
cx.replace_payload(owner, mount)?;
|
||||
cx.set_attr(desktop, "scrollRequested", Value::Bool(true))
|
||||
}
|
||||
/// 沿祖先滚动容器调整偏移,让目标出现进入可见范围。
|
||||
pub fn into_view(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -302,6 +315,7 @@ pub fn into_view(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 按逆出现顺序选择指针处可处理当前滚动方向的最内层容器。
|
||||
pub fn wheel_target(frame: &Frame, point: Point, delta: [f32; 2]) -> Option<u64> {
|
||||
let indices: HashMap<_, _> = frame.mounts.iter().map(|mount| (mount.id, mount)).collect();
|
||||
let mut current = frame
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
//! 把 DisplayList 写入 CPU 像素缓冲。裁剪栈限定矩形、文字和 3D 表面的实际写入区域。
|
||||
//! RenderScope 约束递归深度;累计像素预算限制嵌套 3D 和世界 UI 的中间表面分配。错误向上传递,不把半幅结果当成成功帧呈现。
|
||||
//!
|
||||
use crate::{
|
||||
display_list::{Color, DisplayList, DrawCommand},
|
||||
geometry::{Point, Rect},
|
||||
text::{self, TextStyle},
|
||||
};
|
||||
|
||||
/// 显示列表的 CPU 执行入口,错误在原生 present 前向上传递。
|
||||
pub struct SoftwareRenderer;
|
||||
|
||||
thread_local! {
|
||||
static RENDER_DEPTH:std::cell::Cell<usize>=const { std::cell::Cell::new(0) };
|
||||
static PIXEL_UNITS:std::cell::Cell<usize>=const { std::cell::Cell::new(0) };
|
||||
}
|
||||
/// 递归渲染的 RAII 深度保护,最外层还重置累计像素工作预算。
|
||||
pub(crate) struct RenderScope;
|
||||
impl RenderScope {
|
||||
/// 进入一次嵌套渲染并检查深度上限;最外层调用重新开始累计像素预算。
|
||||
pub fn enter() -> Result<Self, &'static str> {
|
||||
RENDER_DEPTH.with(|depth| {
|
||||
let current = depth.get();
|
||||
@@ -27,10 +33,12 @@ impl RenderScope {
|
||||
}
|
||||
}
|
||||
impl Drop for RenderScope {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
RENDER_DEPTH.with(|depth| depth.set(depth.get() - 1));
|
||||
}
|
||||
}
|
||||
/// 累加本帧中间表面工作量,超过预算立即返回错误。
|
||||
pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> {
|
||||
PIXEL_UNITS.with(|pixels| {
|
||||
let next = pixels.get().saturating_add(units);
|
||||
@@ -43,6 +51,7 @@ pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> {
|
||||
}
|
||||
|
||||
impl SoftwareRenderer {
|
||||
/// 顺序执行显示列表并维护裁剪栈;3D 和面板可递归进入同一渲染预算域。
|
||||
pub fn render(
|
||||
list: &DisplayList,
|
||||
target: &mut [u32],
|
||||
@@ -145,6 +154,7 @@ impl SoftwareRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 借用目标像素切片并维护当前裁剪,所有像素写入都受该上下文限制。
|
||||
struct Canvas<'a> {
|
||||
pixels: &'a mut [u32],
|
||||
width: u32,
|
||||
@@ -153,10 +163,12 @@ struct Canvas<'a> {
|
||||
}
|
||||
|
||||
impl Canvas<'_> {
|
||||
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
|
||||
fn clear(&mut self, color: Color) {
|
||||
self.pixels.fill(pack(color));
|
||||
}
|
||||
|
||||
/// 只遍历裁剪后的矩形像素,按圆角覆盖决定填充区域。
|
||||
fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
|
||||
let bounds = self.clamped_bounds(rect);
|
||||
let radius = radius.max(0.0).min(rect.width / 2.0).min(rect.height / 2.0);
|
||||
@@ -169,6 +181,7 @@ impl Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 用外轮廓与内轮廓的差绘制边框,避免把边框当成整个矩形填充。
|
||||
fn stroke_rounded_rect(&mut self, rect: Rect, radius: f32, width: f32, color: Color) {
|
||||
let bounds = self.clamped_bounds(rect);
|
||||
let width = width.max(1.0).min(rect.width / 2.0).min(rect.height / 2.0);
|
||||
@@ -194,6 +207,7 @@ impl Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把文字原点转换为局部裁剪区域,交给有界字形绘制,再混合到目标缓冲。
|
||||
fn draw_text(&mut self, origin: Point, value: &str, style: TextStyle, color: Color) {
|
||||
let x = origin.x.round() as i32;
|
||||
let y = origin.y.round() as i32;
|
||||
@@ -208,6 +222,7 @@ impl Canvas<'_> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 把段落矩形与当前裁剪相交,按受限宽度绘制,再恢复外层裁剪。
|
||||
fn draw_wrapped_text(&mut self, rect: Rect, value: &str, style: TextStyle, color: Color) {
|
||||
if rect.width <= 0.0 || rect.height <= 0.0 {
|
||||
return;
|
||||
@@ -240,6 +255,7 @@ impl Canvas<'_> {
|
||||
self.clip = old;
|
||||
}
|
||||
|
||||
/// 把连续矩形转换成目标缓冲内的整数像素范围,避免在不可见区域进行光栅循环。
|
||||
fn clamped_bounds(&self, rect: Rect) -> (i32, i32, i32, i32) {
|
||||
(
|
||||
rect.x.max(self.clip.x).max(0.0).floor() as i32,
|
||||
@@ -255,6 +271,7 @@ impl Canvas<'_> {
|
||||
)
|
||||
}
|
||||
|
||||
/// 在画布与当前裁剪范围内进行透明度混合,防止文字/图形写出目标缓冲。
|
||||
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
|
||||
if x < 0
|
||||
|| y < 0
|
||||
@@ -288,6 +305,7 @@ impl Canvas<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试一个像素采样点是否位于圆角矩形内,角区域使用圆弧距离。
|
||||
fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
||||
if rect.width <= 0.0 || rect.height <= 0.0 {
|
||||
return false;
|
||||
@@ -306,6 +324,7 @@ fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
||||
dx * dx + dy * dy <= radius * radius
|
||||
}
|
||||
|
||||
/// 将颜色通道打包成目标缓冲的整数像素表示。
|
||||
fn pack(color: Color) -> u32 {
|
||||
(color.red as u32) << 16 | (color.green as u32) << 8 | color.blue as u32
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 通过真正的源/原生 hitTest 协议确定指针目标。
|
||||
//! 按逆绘制顺序筛选候选,并在同一只读 VM 调用与预算中比较按下、释放目标;透明或未命中的子项可以继续回退。
|
||||
//!
|
||||
//! Resolve pointer targets in paint order through source/native hitTest methods.
|
||||
//! Both press and release selectors share one read-only VM call and budget.
|
||||
use crate::{mount::Mount, ui_bridge::attach_desktop_type};
|
||||
@@ -7,12 +10,15 @@ use endor_core::{
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 验证 hitTest 返回的是 Bool,避免把任意真值隐式当成命中。
|
||||
struct Boolean;
|
||||
impl NativeCallable for Boolean {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Bool(value)] = args else {
|
||||
return Err(VmError::InvalidArgument("hitTest must return Bool"));
|
||||
@@ -21,12 +27,15 @@ impl NativeCallable for Boolean {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 比较按下与释放的选择结果,只有同一目标才能继续点击分发。
|
||||
struct SameTarget;
|
||||
impl NativeCallable for SameTarget {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(2))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let value = match args {
|
||||
[Value::Object(first), Value::Object(second)] if first == second => {
|
||||
@@ -37,6 +46,7 @@ impl NativeCallable for SameTarget {
|
||||
Ok(NativeResult::Return(value))
|
||||
}
|
||||
}
|
||||
/// 构造只读命中选择驱动所需的托管代码与函数对象。
|
||||
fn code(
|
||||
cx: &mut NativeContext<'_>,
|
||||
instructions: Vec<Instruction>,
|
||||
@@ -52,6 +62,7 @@ fn code(
|
||||
let globals = cx.new_dict()?;
|
||||
cx.new_function("pointer hitTest", code, globals)
|
||||
}
|
||||
/// 按逆绘制顺序构造候选 hitTest 调用,先进行几何与局部坐标筛选。
|
||||
fn selector(
|
||||
cx: &mut NativeContext<'_>,
|
||||
candidates: &[WeakObjectRef],
|
||||
@@ -137,6 +148,7 @@ fn selector(
|
||||
code(cx, instructions, constants, CodeLayout::default())
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 在一个只读调用中准备两次指针选择所需的候选与局部坐标。
|
||||
struct Prepare {
|
||||
candidates: Vec<WeakObjectRef>,
|
||||
press: [f32; 2],
|
||||
@@ -144,10 +156,12 @@ struct Prepare {
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Prepare {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let down = selector(cx, &self.candidates, self.press, self.desktop)?;
|
||||
let up = selector(cx, &self.candidates, self.release, self.desktop)?;
|
||||
@@ -192,6 +206,7 @@ impl NativeCallable for Prepare {
|
||||
}
|
||||
}
|
||||
/// Returned Mount owns one root. No-hit and mismatched targets return None.
|
||||
/// 解析输入所指向的实际目标,避免把包装身份与底层数据混淆。
|
||||
pub fn resolve(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 源 measure/arrange 协议的分片布局驱动器。自底向上的测量与自顶向下的安排共享一个只读 VM 调用域。
|
||||
//! Plan 只保存派生几何和求解过程;Measurement 缓存记录真实读取依赖。已有业务对象在只读阶段不能被修改。
|
||||
//! 较大树可在节点/指令量子之间挂起。Work 拥有尚未发布的执行状态,必须 resume 或 cancel;完成后才把 Draft 交给桥接层提交。
|
||||
//!
|
||||
//! Bottom-up measurement and top-down arrangement share one read-only VM call.
|
||||
//! The host scratch plan contains derived data only, never a second business heap.
|
||||
use crate::{
|
||||
@@ -22,9 +26,11 @@ use std::{
|
||||
// same per-occurrence VM driver so even intrinsic work can yield between nodes.
|
||||
const NATIVE_FAST_PATH_MAX_NODES: usize = 64;
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 读取当前 desktop 的类型字典,为布局协议结果提供名义类型检查。
|
||||
fn desktop_type_registry(cx: &NativeContext<'_>, desktop: ObjectRef) -> Result<ObjectRef, VmError> {
|
||||
match cx.store().get_attr(desktop, "types")? {
|
||||
Value::Object(object) => Ok(object),
|
||||
@@ -32,6 +38,7 @@ fn desktop_type_registry(cx: &NativeContext<'_>, desktop: ObjectRef) -> Result<O
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
/// 一次源测量的缓存输入、输出和真实读取依赖;不同约束或依赖版本不能复用。
|
||||
struct Measurement {
|
||||
owner: ObjectRef,
|
||||
method: ObjectRef,
|
||||
@@ -42,12 +49,14 @@ struct Measurement {
|
||||
}
|
||||
/// Borrowed identities and versions only: this cache adds no managed roots.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
/// 按 Mount 弱身份保存源测量结果,不为业务对象增加托管根。
|
||||
pub struct Cache {
|
||||
entries: HashMap<WeakObjectRef, Measurement>,
|
||||
pub hits: usize,
|
||||
pub misses: usize,
|
||||
}
|
||||
impl Cache {
|
||||
/// 从测量缓存依赖中移除宿主私有 desktop 记账对象,防止内部同步造成自我失效。
|
||||
pub(crate) fn exclude_host_desktops(&mut self, desktops: &HashSet<ObjectRef>) {
|
||||
for entry in self.entries.values_mut() {
|
||||
entry
|
||||
@@ -55,10 +64,12 @@ impl Cache {
|
||||
.retain(|read| !desktops.contains(&read.object()));
|
||||
}
|
||||
}
|
||||
/// 移除受影响 owner 的源测量缓存;其他出现继续按其读取版本检查。
|
||||
pub fn invalidate(&mut self, owners: &HashSet<ObjectRef>) {
|
||||
self.entries
|
||||
.retain(|_, entry| !owners.contains(&entry.owner));
|
||||
}
|
||||
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
self.hits = 0;
|
||||
@@ -66,6 +77,7 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 一个等待源测量返回的收集项,记录本次方法、约束和子项输入。
|
||||
struct Pending {
|
||||
token: usize,
|
||||
owner: ObjectRef,
|
||||
@@ -75,10 +87,13 @@ struct Pending {
|
||||
key: WeakObjectRef,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 未发布的布局求解工作区,包含出现树、期望尺寸、分配边界、收敛记录和缓存。
|
||||
struct Plan {
|
||||
// 使用出现索引保存树和几何,不为每个位置建立第二份业务对象。
|
||||
nodes: Vec<Arc<UiNode>>,
|
||||
children: Vec<Vec<usize>>,
|
||||
sizes: Vec<Size>,
|
||||
// preferred 是测量偏好,bounds 是父级实际分配;换行/flex 会使两者不同。
|
||||
preferred: Vec<Option<Size>>,
|
||||
bounds: Vec<Option<[f32; 4]>>,
|
||||
runtime: LayoutRuntime,
|
||||
@@ -92,16 +107,19 @@ struct Plan {
|
||||
native_arrange: Vec<bool>,
|
||||
custom_measure: Vec<bool>,
|
||||
measured_limits: Vec<[Option<f32>; 2]>,
|
||||
// 保存几何状态用于检测收敛和重复状态,防止相互依赖布局无限循环。
|
||||
previous: Option<Vec<u32>>,
|
||||
seen: BTreeSet<Vec<u32>>,
|
||||
}
|
||||
type Shared = Arc<Mutex<Plan>>;
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 源视觉调用当前处于测量还是安排阶段,决定调用方法与结果解码。
|
||||
enum Phase {
|
||||
Measure,
|
||||
Arrange,
|
||||
}
|
||||
impl Phase {
|
||||
/// 把当前布局阶段映射为 measure 或 arrange 方法名称。
|
||||
fn method(self) -> &'static str {
|
||||
match self {
|
||||
Self::Measure => "measure",
|
||||
@@ -110,6 +128,7 @@ impl Phase {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 为某次出现准备一个 measure/arrange 调用,选择原生快速结果或真正的源覆盖。
|
||||
struct Step {
|
||||
plan: Shared,
|
||||
index: usize,
|
||||
@@ -118,10 +137,12 @@ struct Step {
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Step {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mount = cx.store().payload::<Mount>(self.mount, "Mount")?.clone();
|
||||
let definition = cx
|
||||
@@ -293,6 +314,7 @@ impl NativeCallable for Step {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 接收并检查源协议返回结果,将其合入 Plan,而不是直接写成发布帧。
|
||||
struct Collect {
|
||||
plan: Shared,
|
||||
index: usize,
|
||||
@@ -300,10 +322,12 @@ struct Collect {
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Collect {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let pending = if matches!(self.phase, Phase::Measure) {
|
||||
self.plan
|
||||
@@ -496,14 +520,17 @@ impl NativeCallable for Collect {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 检查本轮布局是否已收敛,必要时安排下一次测量/分配迭代。
|
||||
struct Settle {
|
||||
plan: Shared,
|
||||
mounts: Arc<[WeakObjectRef]>,
|
||||
}
|
||||
impl NativeCallable for Settle {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let repeat = {
|
||||
let mut plan = self
|
||||
@@ -626,15 +653,18 @@ impl NativeCallable for Settle {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 准备当前求解步骤所需的约束和 Mount 临时上下文。
|
||||
struct Prepare {
|
||||
plan: Shared,
|
||||
mounts: Vec<WeakObjectRef>,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Prepare {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let advance = cx.allocate(ContextNativeFunction::new(
|
||||
"next layout occurrence",
|
||||
@@ -655,6 +685,7 @@ impl NativeCallable for Prepare {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 在同一驱动帧中安排下一项布局工作,使大树能够分片让出。
|
||||
struct Advance {
|
||||
plan: Shared,
|
||||
mounts: Arc<[WeakObjectRef]>,
|
||||
@@ -662,9 +693,11 @@ struct Advance {
|
||||
next: Mutex<usize>,
|
||||
}
|
||||
impl NativeCallable for Advance {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let count = self.mounts.len();
|
||||
let position = {
|
||||
@@ -741,6 +774,7 @@ impl NativeCallable for Advance {
|
||||
|
||||
/// An unpublished layout frame and derived caches. These are merged only after
|
||||
/// the driver completes; they are not another copy of application state.
|
||||
/// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。
|
||||
pub struct Draft {
|
||||
pub frame: Frame,
|
||||
pub runtime: LayoutRuntime,
|
||||
@@ -749,6 +783,7 @@ pub struct Draft {
|
||||
}
|
||||
|
||||
#[must_use = "pending layout work must be resumed or cancelled with its owning VM"]
|
||||
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
|
||||
pub enum Progress {
|
||||
Pending(Work),
|
||||
Complete(Draft),
|
||||
@@ -756,6 +791,7 @@ pub enum Progress {
|
||||
|
||||
/// Host-owned continuation. No borrowed frame/cache references cross a pause.
|
||||
/// The prepare handle owns one root until completion or explicit cancellation.
|
||||
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
|
||||
pub struct Work {
|
||||
state: Draft,
|
||||
plan: Shared,
|
||||
@@ -765,12 +801,14 @@ pub struct Work {
|
||||
}
|
||||
|
||||
impl Work {
|
||||
/// 恢复当前 Work 拥有的 VM 续体,继续同一轮求解而不是重放 measure/arrange。
|
||||
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
|
||||
self.slices += 1;
|
||||
let progress = vm.resume_read_only_call(slice);
|
||||
self.accept(vm, progress)
|
||||
}
|
||||
|
||||
/// 显式结束布局续体和只读域,释放本次尚未提交的临时根。
|
||||
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
|
||||
let cancelled = vm.cancel_read_only_call();
|
||||
let released = vm.release(self.prepare);
|
||||
@@ -779,6 +817,7 @@ impl Work {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 接收一次续体进度,挂起时保留工作,完成时将派生结果交给收尾阶段。
|
||||
fn accept(
|
||||
self,
|
||||
vm: &mut Vm,
|
||||
@@ -805,6 +844,7 @@ impl Work {
|
||||
}
|
||||
}
|
||||
|
||||
/// 建立只读布局 Plan 与托管驱动函数;小型原生树可走直接路径,大树保留分片推进能力。
|
||||
pub fn start(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
@@ -945,6 +985,7 @@ pub fn start(
|
||||
work.accept(vm, progress)
|
||||
}
|
||||
|
||||
/// 验证求解已收敛,收集最终几何、读取依赖和缓存,返回尚待桥接层提交的 Draft。
|
||||
fn finish(
|
||||
vm: &mut Vm,
|
||||
mut work: Work,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 执行源 paint 覆盖并解码受检查的 PaintCommand。
|
||||
//! 所有绘制回调位于只读域;临时返回对象可分配,但已有模型不可写。命令数量、类型、坐标和颜色在合入帧前检查。
|
||||
//! 源命令与原生命令按 Mount 绘制顺序合并,随后装配世界 UI 面板。挂起绘制同样需要显式恢复或取消。
|
||||
//!
|
||||
//! Source paint dispatch produces checked immutable commands. One read-only VM
|
||||
//! resumable call sequences all overrides, sharing one instruction/frame budget.
|
||||
use crate::{
|
||||
@@ -17,9 +21,11 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||||
fn object(value: Value) -> Result<ObjectRef, VmError> {
|
||||
if let Value::Object(object) = value {
|
||||
Ok(object)
|
||||
@@ -27,6 +33,7 @@ fn object(value: Value) -> Result<ObjectRef, VmError> {
|
||||
Err(invalid("paint must return managed objects"))
|
||||
}
|
||||
}
|
||||
/// 把当前源值或文本选择解码成此调用需要的文本表示。
|
||||
fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<String, VmError> {
|
||||
if let Value::Text(value) = store.get_attr(object, name)? {
|
||||
Ok(value)
|
||||
@@ -34,6 +41,7 @@ fn text(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<String, Vm
|
||||
Err(invalid("paint field must be text"))
|
||||
}
|
||||
}
|
||||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||||
fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmError> {
|
||||
let Value::Number(value) = store.get_attr(object, name)? else {
|
||||
return Err(invalid("paint field must be a Number"));
|
||||
@@ -50,6 +58,7 @@ fn number(store: &ObjectStore, object: ObjectRef, name: &str) -> Result<f32, VmE
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
/// 从当前 desktop 的类型注册表取得绘制结果必须符合的托管类型。
|
||||
fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<ObjectRef, VmError> {
|
||||
let registry = object(store.get_attr(desktop, "types")?)?;
|
||||
object(
|
||||
@@ -58,6 +67,7 @@ fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<Obj
|
||||
.ok_or(invalid("missing paint type"))?,
|
||||
)
|
||||
}
|
||||
/// 把源方法返回的托管绘制值解码成受限 Rust 命令,检查名义类型、字段和绘制预算。
|
||||
fn decode(
|
||||
store: &ObjectStore,
|
||||
command: ObjectRef,
|
||||
@@ -150,22 +160,26 @@ fn decode(
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 按出现 ID 收集已检查的源绘制覆盖,随后与原生默认命令合并。
|
||||
struct PaintOutput {
|
||||
commands: BTreeMap<u64, Vec<RenderCommand>>,
|
||||
count: usize,
|
||||
}
|
||||
type Output = Arc<Mutex<PaintOutput>>;
|
||||
#[derive(Debug)]
|
||||
/// 检查源 paint 返回列表并将命令保存到当前出现的绘制结果。
|
||||
struct Collect {
|
||||
desktop: ObjectRef,
|
||||
mount: u64,
|
||||
output: Output,
|
||||
}
|
||||
impl NativeCallable for Collect {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Object(result)] = args else {
|
||||
return Err(invalid("paint must return a DisplayList"));
|
||||
@@ -195,16 +209,19 @@ impl NativeCallable for Collect {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 为当前出现准备 paint 方法、几何和类型检查上下文。
|
||||
struct Prepare {
|
||||
desktop: ObjectRef,
|
||||
mounts: Vec<(u64, WeakObjectRef)>,
|
||||
output: Output,
|
||||
}
|
||||
impl NativeCallable for Prepare {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let advance = cx.allocate(ContextNativeFunction::new(
|
||||
"next paint occurrence",
|
||||
@@ -225,6 +242,7 @@ impl NativeCallable for Prepare {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 安排下一个出现的绘制调用,保持原始绘制顺序和共享 VM 预算。
|
||||
struct Advance {
|
||||
desktop: ObjectRef,
|
||||
mounts: Arc<[(u64, WeakObjectRef)]>,
|
||||
@@ -232,9 +250,11 @@ struct Advance {
|
||||
output: Output,
|
||||
}
|
||||
impl NativeCallable for Advance {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let occurrence = {
|
||||
let mut next = self
|
||||
@@ -304,6 +324,7 @@ impl NativeCallable for Advance {
|
||||
}
|
||||
}
|
||||
|
||||
/// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。
|
||||
pub struct Draft {
|
||||
pub frame: Frame,
|
||||
pub reads: Vec<endor_core::ReadDependency>,
|
||||
@@ -311,12 +332,14 @@ pub struct Draft {
|
||||
}
|
||||
|
||||
#[must_use = "pending paint must be resumed or cancelled on its owning VM"]
|
||||
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
|
||||
pub enum Progress {
|
||||
Pending(Box<Work>),
|
||||
Complete(Box<Draft>),
|
||||
}
|
||||
|
||||
/// Only unpublished native commands and an owning prepare root cross a pause.
|
||||
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
|
||||
pub struct Work {
|
||||
desktop: ObjectRef,
|
||||
prepare: ObjectRef,
|
||||
@@ -327,12 +350,14 @@ pub struct Work {
|
||||
}
|
||||
|
||||
impl Work {
|
||||
/// 继续当前绘制续体,保留其先前收集的命令和所属窗口上下文。
|
||||
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
|
||||
self.slices += 1;
|
||||
let progress = vm.resume_read_only_call(slice);
|
||||
self.accept(vm, progress)
|
||||
}
|
||||
|
||||
/// 关闭未完成绘制续体并释放其临时根,不发布半完成命令列表。
|
||||
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
|
||||
let cancelled = vm.cancel_read_only_call();
|
||||
let released = vm.release(self.prepare);
|
||||
@@ -341,6 +366,7 @@ impl Work {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 区分源绘制续体是挂起还是完成,保留工作或结束只读域并生成 Draft。
|
||||
fn accept(
|
||||
self,
|
||||
vm: &mut Vm,
|
||||
@@ -368,6 +394,7 @@ impl Work {
|
||||
}
|
||||
}
|
||||
|
||||
/// 建立本轮只读绘制驱动和结果收集器;源覆盖与原生默认绘制最终保持同一出现顺序。
|
||||
pub fn start(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
@@ -411,6 +438,7 @@ pub fn start(
|
||||
work.accept(vm, result)
|
||||
}
|
||||
|
||||
/// 按 Mount 合并源覆盖和原生命令,补齐投影/裁剪表,再递归装配世界 UI 面板。
|
||||
fn finish(vm: &mut Vm, mut work: Work, reads: Vec<endor_core::ReadDependency>) -> Result<Progress> {
|
||||
let output = work
|
||||
.output
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 统一字体发现、文字整形、测量与抗锯齿字形绘制。
|
||||
//! 测量与绘制使用同一字体系统和样式,避免布局宽度与屏幕文字不一致;中文与其他字符通过字体回退处理。
|
||||
//! 整形和字形缓存有数量/字节预算;极端放大采用有界字形 LOD,最终只遍历裁剪区域内的像素。
|
||||
//!
|
||||
//! Shared font discovery, shaping, measurement and antialiased rasterization.
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -9,12 +13,14 @@ use cosmic_text::{Align, Attrs, Buffer, Family, FontSystem, Metrics, Shaping, Sw
|
||||
use crate::display_list::Color;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
/// 界面比例字体与源码等宽字体的选择,不直接指定某个必然安装的系统字体。
|
||||
pub enum TextFamily {
|
||||
Interface,
|
||||
Code,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 字号、行高和字体类别,测量与绘制必须使用兼容样式。
|
||||
pub struct TextStyle {
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
@@ -22,6 +28,7 @@ pub struct TextStyle {
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
/// 生成界面文字的基准字号与行高,size 是协议使用的字号倍率。
|
||||
pub fn ui(size: u32) -> Self {
|
||||
Self {
|
||||
font_size: 14.0 * size as f32,
|
||||
@@ -30,6 +37,7 @@ impl TextStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成源码/Console 使用的等宽风格基础尺寸。
|
||||
pub fn code() -> Self {
|
||||
Self {
|
||||
font_size: 13.0,
|
||||
@@ -38,6 +46,7 @@ impl TextStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// 同比缩放字号与行高,保持设备比例变化后的排版比例一致。
|
||||
pub fn scaled(self, scale: f32) -> Self {
|
||||
Self {
|
||||
font_size: self.font_size * scale,
|
||||
@@ -48,12 +57,14 @@ impl TextStyle {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 整形后实际占用的逻辑文字尺寸。
|
||||
pub struct TextSize {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
/// 文字整形缓存键,包含文本、样式和可选换行宽度。
|
||||
struct TextKey {
|
||||
value: String,
|
||||
font_size: u32,
|
||||
@@ -62,11 +73,13 @@ struct TextKey {
|
||||
wrap_width: Option<u32>,
|
||||
}
|
||||
|
||||
/// 已整形 Buffer 与测量结果,后续测量/绘制复用同一布局。
|
||||
struct PreparedText {
|
||||
buffer: Buffer,
|
||||
size: TextSize,
|
||||
}
|
||||
|
||||
/// 共享字体系统、字形缓存和整形缓存;调用通过全局锁串行访问。
|
||||
struct TextEngine {
|
||||
fonts: FontSystem,
|
||||
code_family: Option<String>,
|
||||
@@ -76,6 +89,7 @@ struct TextEngine {
|
||||
}
|
||||
|
||||
impl TextEngine {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
fn new() -> Self {
|
||||
let mut fonts = FontSystem::new();
|
||||
// Select installed, readable families rather than assuming an OS font
|
||||
@@ -114,10 +128,12 @@ impl TextEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// 对不指定换行宽度的文本复用统一整形准备过程。
|
||||
fn prepare(&mut self, value: &str, style: TextStyle) -> TextKey {
|
||||
self.prepare_width(value, style, None)
|
||||
}
|
||||
|
||||
/// 为文本、字体样式和可选换行宽度建立整形缓存,超过缓存预算先清理旧结果。
|
||||
fn prepare_width(&mut self, value: &str, style: TextStyle, wrap_width: Option<f32>) -> TextKey {
|
||||
let key = TextKey {
|
||||
value: value.to_owned(),
|
||||
@@ -175,17 +191,20 @@ impl TextEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// 取得全局受锁保护的字体/整形引擎;共享缓存不等于可以并行修改字体状态。
|
||||
fn engine() -> &'static Mutex<TextEngine> {
|
||||
static ENGINE: OnceLock<Mutex<TextEngine>> = OnceLock::new();
|
||||
ENGINE.get_or_init(|| Mutex::new(TextEngine::new()))
|
||||
}
|
||||
|
||||
/// 返回不受换行宽度约束时的字体整形尺寸。
|
||||
pub fn measure(value: &str, style: TextStyle) -> TextSize {
|
||||
let mut engine = engine().lock().expect("text engine lock");
|
||||
let key = engine.prepare(value, style);
|
||||
engine.buffers[&key].size
|
||||
}
|
||||
|
||||
/// 按可用宽度整形并测量多行高度,供布局与实际段落绘制共用。
|
||||
pub fn measure_wrapped(value: &str, style: TextStyle, width: f32) -> TextSize {
|
||||
if width <= 0.0 {
|
||||
return TextSize {
|
||||
@@ -201,6 +220,7 @@ pub fn measure_wrapped(value: &str, style: TextStyle, width: f32) -> TextSize {
|
||||
/// Rasterize only visible glyph pixels. Large camera zooms use a bounded glyph
|
||||
/// bitmap and sample it at the requested size instead of allocating an enormous
|
||||
/// font image. Layout and wrapping still use proportionally identical metrics.
|
||||
/// 限制字形分辨率并只采样可见像素;保留文字布局比例,同时避免极端相机缩放产生巨型位图。
|
||||
pub fn draw_clipped(
|
||||
value: &str,
|
||||
style: TextStyle,
|
||||
@@ -300,6 +320,7 @@ pub fn draw_clipped(
|
||||
}
|
||||
|
||||
/// Fit a single displayed source line without slicing a UTF-8/grapheme cluster.
|
||||
/// 按真实字体测量截断到给定宽度,以字素簇边界拼接省略号,避免切开组合字符。
|
||||
pub fn fit(value: &str, width: f32, style: TextStyle) -> String {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 独立 2D 相机与可逆投影。视口尺寸属于布局;平移、缩放属于视图,不改变世界对象的逻辑位置。
|
||||
//! project 为每次出现附加组合变换、裁剪和嵌套面板映射;绘制与命中共用这些结果。fit 根据有限内容边界调整指定相机。
|
||||
//!
|
||||
//! Reversible projections keep camera movement independent of layout.
|
||||
pub mod spatial;
|
||||
use crate::geometry::{Point, Rect};
|
||||
@@ -6,21 +9,27 @@ use endor_core::{ObjectPayload, ObjectRef, ObjectStore, VmError};
|
||||
/// Marker payload gives cameras a native nominal identity while their fields
|
||||
/// remain ordinary traced, bindable VM attributes.
|
||||
#[derive(Debug)]
|
||||
/// 托管 2D 相机的名义标记,实际位置/缩放是被追踪的普通属性。
|
||||
pub struct Camera2D;
|
||||
impl ObjectPayload for Camera2D {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"camera-2d"
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, _: &mut dyn FnMut(ObjectRef)) {}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// 从 VM 提取的独立相机数值描述,渲染与布局投影不直接读取托管堆。
|
||||
pub struct CameraView {
|
||||
pub identity: String,
|
||||
pub x: f32,
|
||||
@@ -28,6 +37,7 @@ pub struct CameraView {
|
||||
pub zoom: f32,
|
||||
}
|
||||
|
||||
/// 读取并校验托管 Camera2D 的位置和缩放,生成原生投影描述。
|
||||
pub fn camera(store: &ObjectStore, object: ObjectRef) -> Result<CameraView, VmError> {
|
||||
store.payload::<Camera2D>(object, "Camera2D")?;
|
||||
let x = crate::scroll::coordinate(&store.get_attr(object, "x")?)?;
|
||||
@@ -48,6 +58,7 @@ pub fn camera(store: &ObjectStore, object: ObjectRef) -> Result<CameraView, VmEr
|
||||
|
||||
/// Fit finite scene bounds using this occurrence's allocation. Sharing a camera
|
||||
/// is explicit, so fitting one view then intentionally affects all its users.
|
||||
/// 根据这个 CanvasViewport 出现的有限内容范围调整其相机;显式共享相机会同步影响其他视口。
|
||||
pub fn fit(
|
||||
cx: &mut endor_core::NativeContext<'_>,
|
||||
mount: &crate::mount::Mount,
|
||||
@@ -127,12 +138,14 @@ pub fn fit(
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
/// 可组合的均匀缩放与平移;正向绘制和逆向命中必须使用同一结果。
|
||||
pub struct Transform {
|
||||
pub zoom: f32,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
impl Default for Transform {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
zoom: 1.0,
|
||||
@@ -142,15 +155,18 @@ impl Default for Transform {
|
||||
}
|
||||
}
|
||||
impl Transform {
|
||||
/// 将逻辑点按当前缩放和平移变换到外层视图。
|
||||
pub fn point(self, point: Point) -> Point {
|
||||
Point::new(point.x * self.zoom + self.x, point.y * self.zoom + self.y)
|
||||
}
|
||||
/// 将视图点反算回逻辑坐标,供命中和手势锚点计算使用。
|
||||
pub fn inverse(self, point: Point) -> Point {
|
||||
Point::new(
|
||||
(point.x - self.x) / self.zoom,
|
||||
(point.y - self.y) / self.zoom,
|
||||
)
|
||||
}
|
||||
/// 使用同一缩放和平移转换矩形的位置与尺寸。
|
||||
pub fn rect(self, rect: Rect) -> Rect {
|
||||
let point = self.point(Point::new(rect.x, rect.y));
|
||||
Rect::new(
|
||||
@@ -160,6 +176,7 @@ impl Transform {
|
||||
rect.height * self.zoom,
|
||||
)
|
||||
}
|
||||
/// 将当前变换再放入外层变换,保持绘制与逆向命中的组合顺序一致。
|
||||
pub fn then(self, outer: Self) -> Self {
|
||||
Self {
|
||||
zoom: self.zoom * outer.zoom,
|
||||
@@ -167,6 +184,7 @@ impl Transform {
|
||||
y: self.y * outer.zoom + outer.y,
|
||||
}
|
||||
}
|
||||
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
|
||||
pub fn valid(self) -> bool {
|
||||
self.zoom.is_finite()
|
||||
&& self.zoom > 0.0
|
||||
@@ -178,6 +196,7 @@ impl Transform {
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算视口与祖先裁剪的共同可见矩形。
|
||||
fn intersect(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
let x = a[0].max(b[0]);
|
||||
let y = a[1].max(b[1]);
|
||||
@@ -189,6 +208,7 @@ fn intersect(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
]
|
||||
}
|
||||
|
||||
/// 遍历出现树,组合相机与父视图变换,建立裁剪和世界 UI 映射,最后重建屏幕命中索引。
|
||||
pub fn project(
|
||||
root: &crate::ui_bridge::UiNode,
|
||||
frame: &mut crate::document::Frame,
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
//! 有界屏幕网格,用来减少普通二维命中和相机目标查询的候选。
|
||||
//! 巨大对象和 3D 面板进入后备集合,再由精确命中判断;索引预算不足不能导致遗漏目标。反序列化或索引长度不匹配时可退回完整候选。
|
||||
//!
|
||||
//! Bounded screen-space broad phase. Projective world UI uses the precise
|
||||
//! ray-based narrow phase rather than an unsafe screen rectangle approximation.
|
||||
use crate::{document::MountGeometry, geometry::Point};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 有限屏幕格子和后备集合。候选筛选允许保守多取,但不能因索引预算漏掉有效命中。
|
||||
pub struct Index {
|
||||
count: usize,
|
||||
cells: HashMap<(i32, i32), Vec<usize>>,
|
||||
fallback: Vec<usize>,
|
||||
}
|
||||
impl Index {
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub fn build(mounts: &[MountGeometry]) -> Self {
|
||||
let mut result = Self {
|
||||
count: mounts.len(),
|
||||
@@ -66,6 +71,7 @@ impl Index {
|
||||
}
|
||||
result
|
||||
}
|
||||
/// 返回当前网格格子与后备集合中的候选,去重并恢复逆绘制顺序。
|
||||
pub fn candidates(&self, point: Point, count: usize) -> Vec<usize> {
|
||||
if self.count != count {
|
||||
return (0..count).rev().collect();
|
||||
@@ -79,6 +85,7 @@ impl Index {
|
||||
output
|
||||
}
|
||||
}
|
||||
/// 把屏幕坐标映射到有界整数网格坐标,处理负方向与极端数值。
|
||||
fn cell(value: f32) -> i32 {
|
||||
(value / 128.)
|
||||
.floor()
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 本机 HTTP API 入口:路由请求、限制输入大小、观察活动,并将执行工作送入串行 worker。
|
||||
//! HTTP 线程不操作 VM;共享 NamespaceState 只保存最近完整发布文档和资源命令。提交序号用来拒绝已被较新 PUT/DELETE 取代的结果。
|
||||
//! 关闭流程先请求运行时有界清理,再通过完成通知让原生事件循环退出。
|
||||
//!
|
||||
mod guide;
|
||||
mod worker;
|
||||
|
||||
@@ -35,18 +39,21 @@ const MAX_CODE_SIZE: u64 = endor_host::MAX_SOURCE_BYTES as u64;
|
||||
type Notify = Arc<dyn Fn() + Send + Sync>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// 运行时清理的摘要与可选错误,供最终原生退出结果报告。
|
||||
pub struct ShutdownOutcome {
|
||||
pub summary: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
type ShutdownCompletion = Arc<(Mutex<Option<ShutdownOutcome>>, Condvar)>;
|
||||
#[derive(Clone)]
|
||||
/// 跨线程的退出控制句柄,使用有界通知和完成条件变量,不携带 VM。
|
||||
pub struct RuntimeControl {
|
||||
sender: SyncSender<()>,
|
||||
requested: Arc<AtomicBool>,
|
||||
completion: ShutdownCompletion,
|
||||
}
|
||||
impl RuntimeControl {
|
||||
/// 只发出一次退出请求,唤醒 worker 执行清理,并通过完成句柄通知原生循环。
|
||||
pub fn request_shutdown(&self) -> bool {
|
||||
if self.requested.swap(true, Ordering::AcqRel) {
|
||||
return true;
|
||||
@@ -58,9 +65,11 @@ impl RuntimeControl {
|
||||
false
|
||||
}
|
||||
}
|
||||
/// 读取退出标志,使窗口事件和新输入尽早停止进入运行时。
|
||||
pub fn is_exiting(&self) -> bool {
|
||||
self.requested.load(Ordering::Acquire)
|
||||
}
|
||||
/// 在明确超时内等待 worker 的清理结果,不能无限阻塞退出。
|
||||
pub fn wait_shutdown(&self, timeout: std::time::Duration) -> Option<ShutdownOutcome> {
|
||||
let (lock, ready) = &*self.completion;
|
||||
let result = lock.lock().ok()?;
|
||||
@@ -70,6 +79,7 @@ impl RuntimeControl {
|
||||
result.clone()
|
||||
}
|
||||
}
|
||||
/// worker 持有的关闭接收端、控制状态和 GUI 完成通知代理。
|
||||
struct Shutdown {
|
||||
receiver: Receiver<()>,
|
||||
control: RuntimeControl,
|
||||
@@ -77,6 +87,7 @@ struct Shutdown {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 送入执行队列的请求类别,区分完整源码、持久输入、事件与重置。
|
||||
enum RequestKind {
|
||||
Event,
|
||||
Code,
|
||||
@@ -85,6 +96,7 @@ enum RequestKind {
|
||||
ResetRepl,
|
||||
}
|
||||
|
||||
/// 保留原 HTTP 请求与提交身份的执行工作,完成或失败后由 worker 回应。
|
||||
struct BuildRequest {
|
||||
request: ObservedRequest,
|
||||
kind: RequestKind,
|
||||
@@ -93,6 +105,7 @@ struct BuildRequest {
|
||||
submission: u64,
|
||||
}
|
||||
|
||||
/// 建立本模块的执行/服务上下文,并把后续推进交给返回的句柄或驱动。
|
||||
pub fn start(
|
||||
namespace: Arc<RwLock<NamespaceState>>,
|
||||
proxy: EventLoopProxy<AppEvent>,
|
||||
@@ -127,6 +140,7 @@ pub fn start(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// 为本地监听服务建立请求处理线程,并把执行请求转交有界队列。
|
||||
fn start_server(
|
||||
server: Arc<Server>,
|
||||
namespace: Arc<RwLock<NamespaceState>>,
|
||||
@@ -142,6 +156,7 @@ fn start_server(
|
||||
)
|
||||
}
|
||||
|
||||
/// 启动带活动观察的 HTTP 服务,将请求状态变化通知管理界面。
|
||||
fn start_server_observed(
|
||||
server: Arc<Server>,
|
||||
namespace: Arc<RwLock<NamespaceState>>,
|
||||
@@ -283,6 +298,7 @@ fn start_server_observed(
|
||||
}
|
||||
|
||||
// Capture only allowlisted headers: credentials and cookies never enter the inspector.
|
||||
/// HTTP 请求的观察包装,在读取正文和响应时维护 Network 工具记录。
|
||||
struct ObservedRequest {
|
||||
inner: Request,
|
||||
activity: Activity,
|
||||
@@ -290,6 +306,7 @@ struct ObservedRequest {
|
||||
notify: Notify,
|
||||
}
|
||||
impl ObservedRequest {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
fn new(inner: Request, activity: Activity, notify: Notify) -> Self {
|
||||
let headers = inner
|
||||
.headers()
|
||||
@@ -322,10 +339,12 @@ impl ObservedRequest {
|
||||
notify,
|
||||
}
|
||||
}
|
||||
/// 将受限请求正文记录到活动预览,不在观察器中执行源码。
|
||||
fn record_body(&self, body: &str) {
|
||||
self.activity.lock().unwrap().body(self.id, body);
|
||||
(self.notify)();
|
||||
}
|
||||
/// 发送最终 HTTP 响应,并同步结束该条活动记录。
|
||||
fn respond(self, response: Response<std::io::Cursor<Vec<u8>>>) -> std::io::Result<()> {
|
||||
let status = response.status_code();
|
||||
let headers = response.headers().to_vec();
|
||||
@@ -348,16 +367,19 @@ impl ObservedRequest {
|
||||
}
|
||||
impl Deref for ObservedRequest {
|
||||
type Target = Request;
|
||||
/// 将包装类型的只读访问转交给内部当前上下文。
|
||||
fn deref(&self) -> &Request {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
impl DerefMut for ObservedRequest {
|
||||
/// 将包装类型的可变访问转交给内部当前上下文。
|
||||
fn deref_mut(&mut self) -> &mut Request {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 HTTP 方法和路径;只读路由读取发布值,执行路由交给 worker。
|
||||
fn handle(
|
||||
request: Request,
|
||||
namespace: &Arc<RwLock<NamespaceState>>,
|
||||
@@ -410,6 +432,7 @@ fn handle(
|
||||
let _ = request.respond(response);
|
||||
}
|
||||
|
||||
/// 处理源码资源的 GET/DELETE 等请求,返回对应版本头或撤销运行时所有权。
|
||||
fn code_request(
|
||||
method: Method,
|
||||
addressed_namespace: &str,
|
||||
@@ -510,6 +533,7 @@ fn code_request(
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取并检查请求体,分配提交身份,再把工作送进有界执行队列。
|
||||
fn submit(
|
||||
mut request: ObservedRequest,
|
||||
addressed: &str,
|
||||
@@ -576,6 +600,7 @@ fn submit(
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成较旧请求已被新提交取代的响应,拒绝晚到结果覆盖当前文档。
|
||||
fn superseded(submission: u64) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
json(
|
||||
StatusCode(409),
|
||||
@@ -584,15 +609,18 @@ fn superseded(submission: u64) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
)
|
||||
}
|
||||
|
||||
/// 从固定路由格式提取命名空间,避免把不匹配的路径当作代码资源。
|
||||
fn namespace_from_route<'a>(path: &'a str, suffix: &str) -> Option<&'a str> {
|
||||
path.strip_prefix("/v1/namespaces/")?
|
||||
.strip_suffix(suffix)
|
||||
.filter(|namespace| !namespace.is_empty() && !namespace.contains('/'))
|
||||
}
|
||||
/// 从固定路由格式提取命名空间,避免把不匹配的路径当作代码资源。
|
||||
fn namespace_from_path(path: &str) -> Option<&str> {
|
||||
namespace_from_route(path, "/code")
|
||||
}
|
||||
|
||||
/// 按源码字节上限读取 UTF-8 请求体,读取失败或超限时返回诊断。
|
||||
fn read_code(request: &mut Request) -> Result<String> {
|
||||
if request
|
||||
.body_length()
|
||||
@@ -612,6 +640,7 @@ fn read_code(request: &mut Request) -> Result<String> {
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
/// 将前端/编译/执行诊断编码为 API 错误响应,保留用户可理解的位置和消息。
|
||||
fn compile_error(error: anyhow::Error) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let mut diagnostic =
|
||||
serde_json::json!({ "severity": "error", "message": format!("{error:#}") });
|
||||
@@ -630,6 +659,7 @@ fn compile_error(error: anyhow::Error) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
)
|
||||
}
|
||||
|
||||
/// 生成统一 JSON 错误响应,使调用方能区分协议错误与成功结果。
|
||||
fn error_response(
|
||||
status: StatusCode,
|
||||
message: impl Into<String>,
|
||||
@@ -637,6 +667,7 @@ fn error_response(
|
||||
json(status, serde_json::json!({ "error": message.into() }))
|
||||
}
|
||||
|
||||
/// 以明确的内容类型和状态码构造响应,并应用本机 API 的公共响应头。
|
||||
fn json(status: StatusCode, value: serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
text(
|
||||
status,
|
||||
@@ -645,10 +676,12 @@ fn json(status: StatusCode, value: serde_json::Value) -> Response<std::io::Curso
|
||||
)
|
||||
}
|
||||
|
||||
/// 以明确的内容类型和状态码构造响应,并应用本机 API 的公共响应头。
|
||||
fn markdown(status: StatusCode, value: String) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
text(status, &value, "text/markdown; charset=utf-8")
|
||||
}
|
||||
|
||||
/// 以明确的内容类型和状态码构造响应,并应用本机 API 的公共响应头。
|
||||
fn text(status: StatusCode, value: &str, content_type: &str) -> Response<std::io::Cursor<Vec<u8>>> {
|
||||
let mut response = Response::from_string(value).with_status_code(status);
|
||||
add_header(&mut response, "Content-Type", content_type);
|
||||
@@ -670,6 +703,7 @@ fn text(status: StatusCode, value: &str, content_type: &str) -> Response<std::io
|
||||
response
|
||||
}
|
||||
|
||||
/// 向响应追加经过 HTTP 库检查的头字段,不直接拼接未验证的原始报文。
|
||||
fn add_header(response: &mut Response<std::io::Cursor<Vec<u8>>>, name: &str, value: &str) {
|
||||
if let Ok(header) = Header::from_bytes(name, value) {
|
||||
response.add_header(header);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! 从当前命名空间生成 /agent 使用指南。返回的示例、地址和版本描述来自运行时上下文;这里生成文档,不执行用户源码。
|
||||
//!
|
||||
//! Live agent-facing guide generated from the current namespace.
|
||||
use super::*;
|
||||
|
||||
/// 用当前命名空间、地址和版本填充在线指南,示例来自项目内置 Endor 源码。
|
||||
pub(super) fn agent_guide(namespace: &NamespaceState) -> String {
|
||||
format!(
|
||||
r##"# Tessera Agent API
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! API 集成回归。通过实际本地 HTTP 请求检查队列、候选替换、状态码、版本和完整文档发布。
|
||||
//! 测试服务使用独立监听地址和串行 worker;应结合断言理解成功输入、失败候选、过期事件和关闭的不同保留语义。
|
||||
//!
|
||||
use super::*;
|
||||
use std::{
|
||||
io::Write,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 唯一串行执行 VM 的 API worker。保留活跃运行时,另建 PUT 候选,并在候选完整成功后切换发布。
|
||||
//! 输入、视口刷新与候选布局可以跨 worker 轮次挂起;中途只公布完整文档,不能让新输入进入未结束的只读调用。
|
||||
//! 事件身份同时校验窗口、session、layout_version 和 Mount 能力。失败、替换与关闭分别处理,避免旧回调或旧命令泄漏到新 VM。
|
||||
//!
|
||||
//! Serial VM ownership, incremental requests, publication and shutdown.
|
||||
use super::*;
|
||||
|
||||
@@ -13,6 +17,7 @@ type CandidateOutcome = std::thread::Result<
|
||||
)>,
|
||||
>;
|
||||
|
||||
/// 已执行业务部分但仍在等待展示刷新的输入元数据,避免恢复时重放回调。
|
||||
pub(super) struct PendingInput {
|
||||
epoch: u64,
|
||||
id: u64,
|
||||
@@ -20,17 +25,20 @@ pub(super) struct PendingInput {
|
||||
result: endor_host::ReplResult,
|
||||
}
|
||||
|
||||
/// 挂起 HTTP 请求的阶段:等旧刷新、继续输入或继续隔离候选。
|
||||
pub(super) enum PendingRequest {
|
||||
BeforeInput,
|
||||
Input(Box<PendingInput>),
|
||||
Candidate(Box<endor_host::ReplSession>),
|
||||
}
|
||||
|
||||
/// 一次 worker 推进的结果:继续保留请求或交付最终响应。
|
||||
pub(super) enum RequestProgress {
|
||||
Pending(PendingRequest),
|
||||
Complete(HttpResponse),
|
||||
}
|
||||
|
||||
/// 只提取仍属于当前 session 的窗口尺寸,避免旧原生窗口给新候选提供错误约束。
|
||||
fn current_viewports(
|
||||
current: &NamespaceState,
|
||||
) -> std::collections::BTreeMap<u64, crate::layout::Size> {
|
||||
@@ -55,11 +63,13 @@ fn current_viewports(
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// 唯一活跃 VM 的串行拥有者,另行管理候选和下一 session 身份。
|
||||
pub(super) struct Worker {
|
||||
pub(super) repl: Option<(u64, endor_host::ReplSession)>,
|
||||
pub(super) next_session: u64,
|
||||
}
|
||||
impl Worker {
|
||||
/// 撤销活跃运行时并有界清理窗口、Mount 和任务;故障域不再执行新的业务回调。
|
||||
pub(super) fn shutdown_runtime(&mut self) -> ShutdownOutcome {
|
||||
let Some((_, mut runtime)) = self.repl.take() else {
|
||||
return ShutdownOutcome {
|
||||
@@ -90,6 +100,7 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理当前无法继续正常交互的窗口关闭,撤销原生资源与相应运行时记录。
|
||||
pub(super) fn close_inactive_window(
|
||||
&mut self,
|
||||
input: &crate::events::Input,
|
||||
@@ -162,6 +173,7 @@ impl Worker {
|
||||
)
|
||||
}
|
||||
|
||||
/// 在没有前台输入的 worker 轮次推进尺寸和任务引起的展示刷新。
|
||||
pub(super) fn refresh_viewport(
|
||||
&mut self,
|
||||
namespace: &Arc<RwLock<NamespaceState>>,
|
||||
@@ -170,12 +182,14 @@ impl Worker {
|
||||
self.refresh_viewport_mode(namespace, notify, false);
|
||||
}
|
||||
|
||||
/// 在输入之间推进视口/任务刷新;提交序号或 session 已变时拒绝旧结果发布。
|
||||
pub(super) fn refresh_viewport_mode(
|
||||
&mut self,
|
||||
namespace: &Arc<RwLock<NamespaceState>>,
|
||||
notify: &Notify,
|
||||
before_input: bool,
|
||||
) {
|
||||
// 在读锁内取得一个一致的提交身份和尺寸快照;耗时 VM 工作不占用共享状态写锁。
|
||||
let (epoch, viewports) = match namespace.read() {
|
||||
Ok(current) => {
|
||||
let viewports = current_viewports(¤t);
|
||||
@@ -216,6 +230,7 @@ impl Worker {
|
||||
}
|
||||
Ok((completed.or(resized), runtime.take_window_commands()?))
|
||||
}));
|
||||
// 计算结束后重新核对归属:期间若发生 PUT/DELETE,不能把旧域草稿发布出去。
|
||||
let mut current = match namespace.write() {
|
||||
Ok(current) => current,
|
||||
Err(_) => return,
|
||||
@@ -267,6 +282,7 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查请求种类和归属,必要时先等已有刷新结束,再建立输入或候选任务。
|
||||
pub(super) fn begin_request(
|
||||
&mut self,
|
||||
job: &BuildRequest,
|
||||
@@ -591,6 +607,7 @@ impl Worker {
|
||||
))
|
||||
}
|
||||
|
||||
/// 恢复已挂起的请求,保持原 HTTP 响应和运行时身份直到最终完成。
|
||||
pub(super) fn resume_request(
|
||||
&mut self,
|
||||
job: &BuildRequest,
|
||||
@@ -607,6 +624,7 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 继续同一 Console/事件输入的展示刷新,不再次执行其业务回调。
|
||||
fn resume_input(
|
||||
&mut self,
|
||||
job: &BuildRequest,
|
||||
@@ -671,6 +689,7 @@ impl Worker {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// 校验输入仍属于当前域,再一次性发布结果、元数据和原生窗口命令。
|
||||
fn finish_input(
|
||||
&mut self,
|
||||
job: &BuildRequest,
|
||||
@@ -768,6 +787,7 @@ impl Worker {
|
||||
|
||||
/// An accepted PUT changes the submission, but does not revoke the old
|
||||
/// runtime. DELETE clears runtime_session and does revoke it.
|
||||
/// 接受较新提交不等于立即销毁旧 VM;候选失败时仍需保留当前可用运行时。
|
||||
pub(super) fn preserve_live_owner(&mut self, current: &NamespaceState) {
|
||||
if let Some((epoch, runtime)) = &mut self.repl {
|
||||
if current.runtime_session == runtime.id {
|
||||
@@ -778,6 +798,7 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 为完整 PUT 建立隔离候选,保留旧运行时直到新候选完整成功。
|
||||
fn begin_program_request(
|
||||
&mut self,
|
||||
job: &BuildRequest,
|
||||
@@ -830,6 +851,7 @@ impl Worker {
|
||||
RequestProgress::Complete(self.finish_candidate(job, namespace, notify, id, result))
|
||||
}
|
||||
|
||||
/// 推进候选的布局与视口收敛,检查其是否已被新提交撤销。
|
||||
fn resume_candidate(
|
||||
&mut self,
|
||||
job: &BuildRequest,
|
||||
@@ -877,6 +899,7 @@ impl Worker {
|
||||
RequestProgress::Complete(self.finish_candidate(job, namespace, notify, id, result))
|
||||
}
|
||||
|
||||
/// 候选成功才切换活跃 VM 和文档;失败保留先前已发布程序。
|
||||
fn finish_candidate(
|
||||
&mut self,
|
||||
job: &BuildRequest,
|
||||
@@ -925,6 +948,7 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理隔离表达式或一次性文档编译;编译前后核对 submission,只发布仍有效的完整快照。
|
||||
fn build(
|
||||
job: &BuildRequest,
|
||||
namespace: &Arc<RwLock<NamespaceState>>,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 把 Aster 编译/执行与 Tessera 展示生命周期连接起来。Candidate 拥有 VM 与宿主根,ReplSession 保留实际运行中的全局环境。
|
||||
//! 完整源码 PUT 使用隔离候选;Console 输入则在已有 VM 上执行,先前发生的写入不等于可以回放或事务回滚。
|
||||
//! 刷新状态机遍历窗口并逐步推进布局/绘制。完成所有必要轮次才形成文档;取消会释放挂起调用并恢复未提交的 Mount。
|
||||
//!
|
||||
//! Compile complete source in a fresh VM or retain a worker-owned Console VM.
|
||||
//! Console retains mounted native references and publishes immutable frames after successful inputs.
|
||||
use anyhow::{Result, bail};
|
||||
@@ -22,6 +26,7 @@ const LIMITS: ExecutionLimits = ExecutionLimits {
|
||||
const MAX_AST_NODES: usize = 10_000;
|
||||
const MAX_AST_DEPTH: usize = 64;
|
||||
|
||||
/// 把源输入编译并执行为一次独立结果,最终将可展示对象转换为检查后的文档。
|
||||
pub fn compile(source: &str, namespace: &str, submission: u64) -> Result<RenderDocument> {
|
||||
if source.len() > MAX_SOURCE_BYTES {
|
||||
bail!("source exceeds the 1 MiB limit");
|
||||
@@ -106,6 +111,7 @@ pub fn compile(source: &str, namespace: &str, submission: u64) -> Result<RenderD
|
||||
}
|
||||
|
||||
/// Evaluate exactly one expression in a disposable VM; never touch the published page.
|
||||
/// 在隔离执行域求一个表达式结果;不会把结果作为持久 Console 环境发布。
|
||||
pub fn evaluate(source: &str) -> Result<String> {
|
||||
if source.len() > MAX_SOURCE_BYTES {
|
||||
bail!("expression exceeds the 1 MiB limit");
|
||||
@@ -143,6 +149,7 @@ pub fn evaluate(source: &str) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 Script/Module 计划编译各根节点,为后续执行准备可加载模板。
|
||||
fn compile_plan(source: &SourceUnit, plan: &SourcePlan) -> Result<Vec<compiler::CompiledCode>> {
|
||||
compile_plan_with_globals(
|
||||
source,
|
||||
@@ -155,6 +162,7 @@ fn compile_plan(source: &SourceUnit, plan: &SourcePlan) -> Result<Vec<compiler::
|
||||
)
|
||||
}
|
||||
|
||||
/// 结合已有全局信息编译本轮输入,保留持久 Console 的名称解析上下文。
|
||||
fn compile_plan_with_globals(
|
||||
source: &SourceUnit,
|
||||
plan: &SourcePlan,
|
||||
@@ -200,31 +208,39 @@ fn compile_plan_with_globals(
|
||||
|
||||
/// One real VM, owned only by the existing execution worker. No history replay.
|
||||
/// The UI bridge shares the same managed object heap and publishes atomic frames.
|
||||
/// 指定窗口尚未完成的绘制工作,文档与源续体必须保持配对。
|
||||
struct PendingPaint {
|
||||
window: u64,
|
||||
document: RenderDocument,
|
||||
work: Box<crate::source_paint::Work>,
|
||||
}
|
||||
|
||||
/// 跨 worker 轮次的全窗口刷新状态,保存已完成草稿和当前布局/绘制,直到稳定再发布。
|
||||
struct RefreshWork {
|
||||
// 已完成窗口的草稿先留在本轮;所有窗口收敛后才合成一次完整发布。
|
||||
documents: std::collections::BTreeMap<u64, RenderDocument>,
|
||||
windows: std::collections::VecDeque<u64>,
|
||||
current: Option<u64>,
|
||||
root: Option<std::sync::Arc<crate::ui_bridge::UiNode>>,
|
||||
// 续体必须保留其 owning VM;取消时显式关闭,不能靠丢弃 Rust 值。
|
||||
pending: Option<Box<crate::ui_bridge::PendingLayout>>,
|
||||
paint: Option<Box<PendingPaint>>,
|
||||
// pass_changed 要求再跑一轮确认稳定,changed 记录整次刷新是否有可发布变化。
|
||||
pass_start: bool,
|
||||
pass_changed: bool,
|
||||
changed: bool,
|
||||
work: usize,
|
||||
// 记录本轮尺寸目标,下一次 worker 轮次据此判断是否需要重定向。
|
||||
viewports: Option<std::collections::BTreeMap<u64, crate::layout::Size>>,
|
||||
}
|
||||
|
||||
/// 刷新仍在进行或已完成;完成也可能没有新文档需要发布。
|
||||
enum RefreshProgress {
|
||||
Pending,
|
||||
Complete(Option<Box<RenderDocument>>),
|
||||
}
|
||||
|
||||
/// 持久源运行时与其桥接会话,保存实际全局对象、代码投影、全窗口文档和挂起刷新。
|
||||
pub struct ReplSession {
|
||||
candidate: Candidate,
|
||||
bridge: ui_bridge::UiBridge,
|
||||
@@ -238,6 +254,7 @@ pub struct ReplSession {
|
||||
documents: std::collections::BTreeMap<u64, RenderDocument>,
|
||||
refresh: Option<RefreshWork>,
|
||||
}
|
||||
/// 一次 Console/事件操作的标量显示结果与可选完整文档,避免直接跨线程暴露托管返回值。
|
||||
pub struct ReplResult {
|
||||
pub value: String,
|
||||
pub document: Option<RenderDocument>,
|
||||
@@ -248,6 +265,7 @@ impl ReplSession {
|
||||
/// Compile once into an isolated candidate, then keep its actual heap and
|
||||
/// callback environments alive after the first successful publication.
|
||||
#[cfg(test)]
|
||||
/// 测试用同步装载入口:建立完整候选并排空初始刷新,返回真实运行时与完成文档。
|
||||
pub fn load(
|
||||
source: &str,
|
||||
namespace: &str,
|
||||
@@ -258,6 +276,7 @@ impl ReplSession {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// 测试用同步装载入口:建立完整候选并排空初始刷新,返回真实运行时与完成文档。
|
||||
pub fn load_with_viewport(
|
||||
source: &str,
|
||||
namespace: &str,
|
||||
@@ -275,6 +294,7 @@ impl ReplSession {
|
||||
Ok((runtime, document))
|
||||
}
|
||||
|
||||
/// 建立可由 worker 分轮推进的完整源码候选,初始布局不必在一次调用里排空。
|
||||
pub fn load_incremental(
|
||||
source: &str,
|
||||
namespace: &str,
|
||||
@@ -297,6 +317,7 @@ impl ReplSession {
|
||||
Ok((runtime, document))
|
||||
}
|
||||
|
||||
/// 建立隔离候选、安装宿主根并执行完整源码;候选尚未完成布局时不替换活跃程序。
|
||||
fn prepare_program(
|
||||
source: &str,
|
||||
namespace: &str,
|
||||
@@ -368,6 +389,7 @@ impl ReplSession {
|
||||
Ok(runtime)
|
||||
}
|
||||
|
||||
/// 建立空的持久 Console 运行时,并安装当前逻辑视口输入。
|
||||
pub fn new_with_viewport(id: u64, viewport: Option<crate::layout::Size>) -> Result<Self> {
|
||||
let mut runtime = Self::new(id)?;
|
||||
runtime
|
||||
@@ -376,6 +398,7 @@ impl ReplSession {
|
||||
Ok(runtime)
|
||||
}
|
||||
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(id: u64) -> Result<Self> {
|
||||
let mut candidate = Candidate::new()?;
|
||||
let globals = candidate.vm.new_dict()?;
|
||||
@@ -398,10 +421,12 @@ impl ReplSession {
|
||||
})
|
||||
}
|
||||
#[cfg(test)]
|
||||
/// 同步执行一个 Console 输入并排空刷新,返回标量结果及可选新文档。
|
||||
pub fn execute(&mut self, source: &str, namespace: &str) -> Result<ReplResult> {
|
||||
self.execute_mode(source, namespace, None)
|
||||
}
|
||||
|
||||
/// 执行一次输入的业务部分,再让展示刷新以可恢复工作跨 worker 轮次推进。
|
||||
pub fn execute_incremental(
|
||||
&mut self,
|
||||
source: &str,
|
||||
@@ -411,6 +436,7 @@ impl ReplSession {
|
||||
self.execute_mode(source, namespace, Some(viewports))
|
||||
}
|
||||
|
||||
/// 在当前真实 VM 中编译并执行新输入,再按同步或分片模式刷新展示;已发生的业务写入不会被源重放回滚。
|
||||
fn execute_mode(
|
||||
&mut self,
|
||||
source: &str,
|
||||
@@ -555,14 +581,17 @@ impl ReplSession {
|
||||
result
|
||||
}
|
||||
|
||||
/// 查询调度器是否仍有需要主动推进的源协程工作。
|
||||
pub fn has_cooperative_tasks(&self) -> bool {
|
||||
self.bridge.has_cooperative_tasks()
|
||||
}
|
||||
|
||||
/// 检查是否存在尚未结束的视觉刷新工作,决定能否接收新的业务变更。
|
||||
pub fn has_pending_refresh(&self) -> bool {
|
||||
self.refresh.is_some()
|
||||
}
|
||||
|
||||
/// 拒绝在未完成的只读刷新内部再次进行业务变更,防止同一 VM 重入。
|
||||
fn require_idle_refresh(&self) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
!self.has_pending_refresh(),
|
||||
@@ -571,6 +600,7 @@ impl ReplSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 在业务变化后开始或继续展示刷新,保证后续输入不会进入未结束的只读视觉调用。
|
||||
fn refresh_after_mutation(
|
||||
&mut self,
|
||||
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
|
||||
@@ -588,6 +618,7 @@ impl ReplSession {
|
||||
})
|
||||
}
|
||||
|
||||
/// 同步排空当前刷新状态机,直到全窗口收敛或出现错误。
|
||||
fn refresh_document(&mut self) -> Result<Option<RenderDocument>> {
|
||||
loop {
|
||||
match self.poll_refresh(false)? {
|
||||
@@ -599,6 +630,7 @@ impl ReplSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// 推进一次刷新状态机,保存挂起的 Work;只有全窗口收敛时才生成可发布文档。
|
||||
fn poll_refresh(&mut self, record_yield: bool) -> Result<RefreshProgress> {
|
||||
let mut work = self.refresh.take().unwrap_or_else(|| RefreshWork {
|
||||
documents: self.documents.clone(),
|
||||
@@ -654,7 +686,9 @@ impl ReplSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行一个刷新步骤:刷新绑定、选择窗口、准备快照、推进布局或绘制,并决定是否需要下一轮。
|
||||
fn step_refresh(&mut self, work: &mut RefreshWork) -> Result<bool> {
|
||||
// 阶段一:新一轮先刷新绑定,再取得稳定窗口遍历顺序。
|
||||
if work.pass_start {
|
||||
self.bridge.refresh_bindings(&mut self.candidate.vm)?;
|
||||
work.windows = self.bridge.window_ids()?.into();
|
||||
@@ -663,6 +697,7 @@ impl ReplSession {
|
||||
work.pass_start = false;
|
||||
return Ok(false);
|
||||
}
|
||||
// 阶段二:逐个选择窗口;一轮发生变化时再扫描一轮,直到依赖不再引起新帧。
|
||||
let Some(id) = work.current else {
|
||||
if let Some(id) = work.windows.pop_front() {
|
||||
work.current = Some(id);
|
||||
@@ -675,6 +710,7 @@ impl ReplSession {
|
||||
return Ok(true);
|
||||
};
|
||||
self.bridge.select_window(id)?;
|
||||
// 已进入 paint 的窗口直接恢复原绘制续体,不重放业务回调或布局。
|
||||
if let Some(paint) = work.paint.take() {
|
||||
let PendingPaint {
|
||||
window,
|
||||
@@ -686,6 +722,7 @@ impl ReplSession {
|
||||
.resume_paint_frame(&mut self.candidate.vm, *paint_work)?;
|
||||
return Self::accept_window_paint(work, window, document, progress);
|
||||
}
|
||||
// 阶段三:恢复旧布局,或在快照/依赖确实变化时建立新布局。
|
||||
let progress = if let Some(pending) = work.pending.take() {
|
||||
self.bridge
|
||||
.resume_layout_frame(&mut self.candidate.vm, *pending)?
|
||||
@@ -704,6 +741,7 @@ impl ReplSession {
|
||||
}
|
||||
crate::ui_bridge::LayoutProgress::Complete(frame) => *frame,
|
||||
};
|
||||
// 阶段四:几何完成后建立候选文档,再运行 paint;这里尚未向 GUI 发布。
|
||||
let mut document = RenderDocument {
|
||||
windows: Default::default(),
|
||||
source: self.source.clone(),
|
||||
@@ -732,6 +770,7 @@ impl ReplSession {
|
||||
Self::accept_window_paint(work, id, document, progress)
|
||||
}
|
||||
|
||||
/// 接收指定窗口的绘制进度,将挂起 Work 保留或把完整文档存入本轮结果。
|
||||
fn accept_window_paint(
|
||||
work: &mut RefreshWork,
|
||||
id: u64,
|
||||
@@ -757,6 +796,7 @@ impl ReplSession {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// 收敛后组合所有窗口文档,保留本轮完整结果;没有变化时不额外制造发布。
|
||||
fn finish_refresh(&mut self, mut work: RefreshWork) -> Result<Option<RenderDocument>> {
|
||||
self.bridge.finish_closed(&mut self.candidate.vm)?;
|
||||
let ids = self.bridge.window_ids()?;
|
||||
@@ -796,6 +836,7 @@ impl ReplSession {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// 同步更新多个窗口尺寸,复用同一运行时并完成必要刷新。
|
||||
pub fn resize_windows(
|
||||
&mut self,
|
||||
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
|
||||
@@ -803,6 +844,7 @@ impl ReplSession {
|
||||
self.resize_windows_incremental(viewports, true)
|
||||
}
|
||||
|
||||
/// 用最新视口替换旧目标,显式取消未完成布局,同时保留已完成窗口结果和已发生的业务效果。
|
||||
fn retarget_refresh(
|
||||
&mut self,
|
||||
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
|
||||
@@ -846,6 +888,7 @@ impl ReplSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 把各窗口最新尺寸送入同一运行时;视口更新可跨轮次挂起,也可由调用方同步排空。
|
||||
pub fn resize_windows_incremental(
|
||||
&mut self,
|
||||
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
|
||||
@@ -900,6 +943,7 @@ impl ReplSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// 在空闲轮次推进一个刷新步骤,完成时执行统一结果收尾。
|
||||
fn advance_idle_refresh(&mut self) -> Result<Option<RenderDocument>> {
|
||||
let result = self.poll_refresh(true).map(|progress| match progress {
|
||||
RefreshProgress::Pending => None,
|
||||
@@ -908,6 +952,7 @@ impl ReplSession {
|
||||
self.finish_refresh_update(result)
|
||||
}
|
||||
|
||||
/// 统一处理刷新失败、未发布状态清理和运行时引用回收。
|
||||
fn finish_refresh_update(
|
||||
&mut self,
|
||||
result: Result<Option<RenderDocument>>,
|
||||
@@ -925,11 +970,13 @@ impl ReplSession {
|
||||
|
||||
/// Deliver ready work on the owning VM worker, never from the GUI or a
|
||||
/// provider thread. A failed callback stops the same shared fault domain.
|
||||
/// 同步推进可交付任务和必要刷新,供不使用分片 worker 的调用方使用。
|
||||
pub fn poll_tasks(&mut self) -> Result<Option<RenderDocument>> {
|
||||
self.poll_tasks_mode(None)
|
||||
}
|
||||
|
||||
/// Idle delivery starts at most one callback, then parks its layout work.
|
||||
/// 按有界任务交付量推进,再以可挂起刷新处理变化。
|
||||
pub fn poll_tasks_incremental(
|
||||
&mut self,
|
||||
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
|
||||
@@ -937,6 +984,7 @@ impl ReplSession {
|
||||
self.poll_tasks_mode(Some(viewports))
|
||||
}
|
||||
|
||||
/// 按本轮预算交付已完成任务或推进协程,再对其业务变化执行相同的展示刷新流程。
|
||||
fn poll_tasks_mode(
|
||||
&mut self,
|
||||
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
|
||||
@@ -973,12 +1021,14 @@ impl ReplSession {
|
||||
result
|
||||
}
|
||||
|
||||
/// 取得本轮已稳定运行时的窗口命令,挂起刷新时禁止提前取走。
|
||||
pub fn take_window_commands(&self) -> Result<Vec<crate::window::Command>> {
|
||||
self.require_idle_refresh()?;
|
||||
self.bridge.take_window_commands(&self.candidate.vm)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// 测试用单窗口尺寸更新入口,遵循与原生尺寸发布相同的校验和刷新规则。
|
||||
pub fn resize(
|
||||
&mut self,
|
||||
viewport: Option<crate::layout::Size>,
|
||||
@@ -1000,6 +1050,7 @@ impl ReplSession {
|
||||
result
|
||||
}
|
||||
|
||||
/// 关闭尚未完成的刷新任务,释放续体并恢复未提交 Mount;不能只丢弃 Work 结构。
|
||||
fn cancel_pending_refresh(&mut self) -> Result<()> {
|
||||
if let Some(mut work) = self.refresh.take() {
|
||||
if let Some(paint) = work.paint.take() {
|
||||
@@ -1017,6 +1068,7 @@ impl ReplSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 有界关闭任务、挂载和窗口,释放运行时根,并报告清理结果。
|
||||
pub fn shutdown(&mut self) -> Result<(usize, usize, usize)> {
|
||||
self.cancel_pending_refresh()?;
|
||||
let callbacks = !self.faulted;
|
||||
@@ -1024,6 +1076,7 @@ impl ReplSession {
|
||||
self.bridge.shutdown(&mut self.candidate.vm, callbacks)
|
||||
}
|
||||
|
||||
/// 撤销指定窗口的运行时展示资源,防止后续刷新重新发布它。
|
||||
pub fn retire_window(&mut self, id: u64) -> Result<()> {
|
||||
self.require_idle_refresh()?;
|
||||
// This path is exclusively host cleanup after the domain has stopped.
|
||||
@@ -1036,23 +1089,28 @@ impl ReplSession {
|
||||
.reclaim(&mut self.candidate.vm, &mut self.candidate.roots)
|
||||
}
|
||||
|
||||
/// 查询当前运行时是否仍接受对应的窗口或拖放目标。
|
||||
pub fn accepts_tab_drop(&self, input: &crate::events::Input) -> bool {
|
||||
self.bridge.accepts_tab_drop(&self.candidate.vm, input)
|
||||
}
|
||||
|
||||
/// 查询当前运行时是否仍接受对应的窗口或拖放目标。
|
||||
pub fn accepts_window(&self, id: u64) -> bool {
|
||||
self.bridge.accepts_window(id)
|
||||
}
|
||||
|
||||
/// 报告该运行时是否已经停止正常业务执行,供 worker 选择故障清理路径。
|
||||
pub fn is_faulted(&self) -> bool {
|
||||
self.faulted
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// 同步分发一次已校验输入并完成展示刷新。
|
||||
pub fn dispatch(&mut self, input: &crate::events::Input) -> Result<ReplResult> {
|
||||
self.dispatch_mode(input, None)
|
||||
}
|
||||
|
||||
/// 分发一次输入回调,再把视觉刷新交给 worker 分片推进。
|
||||
pub fn dispatch_incremental(
|
||||
&mut self,
|
||||
input: &crate::events::Input,
|
||||
@@ -1061,6 +1119,7 @@ impl ReplSession {
|
||||
self.dispatch_mode(input, Some(viewports))
|
||||
}
|
||||
|
||||
/// 在正确窗口上下文执行输入回调,再刷新展示;故障时停止受影响域并清理未完成工作。
|
||||
fn dispatch_mode(
|
||||
&mut self,
|
||||
input: &crate::events::Input,
|
||||
@@ -1107,11 +1166,13 @@ impl ReplSession {
|
||||
}
|
||||
|
||||
impl Drop for ReplSession {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
let _ = self.cancel_pending_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归修正代码投影的源行号,使拼接输入仍可定位到原始文本。
|
||||
fn shift_node_lines(node: &mut CodeNode, offset: usize) {
|
||||
node.start_line += offset;
|
||||
node.end_line += offset;
|
||||
@@ -1119,6 +1180,7 @@ fn shift_node_lines(node: &mut CodeNode, offset: usize) {
|
||||
shift_node_lines(child, offset);
|
||||
}
|
||||
}
|
||||
/// 把 VM 返回值转换为 Console 的可显示表示,不让借用的托管引用跨线程。
|
||||
fn value_repr(vm: &Vm, value: &Value) -> Result<String> {
|
||||
Ok(match value {
|
||||
Value::Nil => "nil".into(),
|
||||
@@ -1129,6 +1191,7 @@ fn value_repr(vm: &Vm, value: &Value) -> Result<String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 拥有 VM、全局字典与宿主保留根;关闭时按顺序释放,失败也必须收尾。
|
||||
struct Candidate {
|
||||
vm: Vm,
|
||||
roots: Vec<ObjectRef>,
|
||||
@@ -1136,6 +1199,7 @@ struct Candidate {
|
||||
}
|
||||
|
||||
impl Candidate {
|
||||
/// 创建独立 VM,先不保留任何宿主根或全局字典;后续安装失败也由该候选负责清理。
|
||||
fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
vm: Vm::new()?,
|
||||
@@ -1144,6 +1208,7 @@ impl Candidate {
|
||||
})
|
||||
}
|
||||
|
||||
/// 释放候选拥有的宿主根并清理 VM;通过显式关闭保证错误也能被报告。
|
||||
fn close(&mut self) -> Result<()> {
|
||||
if let Some(globals) = self.globals.take() {
|
||||
self.vm.dict_clear(globals)?;
|
||||
@@ -1160,6 +1225,7 @@ impl Candidate {
|
||||
}
|
||||
|
||||
impl Drop for Candidate {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
// Explicit close reports accounting errors; dropping the fresh VM is
|
||||
// also the final cleanup boundary on every early return or unwind.
|
||||
@@ -1167,6 +1233,7 @@ impl Drop for Candidate {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 VM 错误与当前源位置/调用栈组合成用户可读的诊断。
|
||||
pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error {
|
||||
let locations = vm
|
||||
.last_traceback()
|
||||
@@ -1187,6 +1254,7 @@ pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// 编译前的语法节点数量/嵌套预算,限制输入结构复杂度。
|
||||
struct AstBudget {
|
||||
depth: usize,
|
||||
nodes: usize,
|
||||
@@ -1194,6 +1262,7 @@ struct AstBudget {
|
||||
|
||||
impl Visitor for AstBudget {
|
||||
type Error = anyhow::Error;
|
||||
/// 更新源结构遍历的深度/数量预算,拒绝超出允许范围的语法树。
|
||||
fn enter_node(&mut self, _span: Span) -> Result<()> {
|
||||
self.depth += 1;
|
||||
self.nodes += 1;
|
||||
@@ -1202,11 +1271,13 @@ impl Visitor for AstBudget {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// 离开当前语法节点时归还嵌套深度;累计节点数保留,不能通过退出子树绕过总量预算。
|
||||
fn leave_node(&mut self) {
|
||||
self.depth -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// 把输入语法结构转换为 Sources 工具使用的 CodeNode 层次,保存原始源码位置。
|
||||
fn architecture(
|
||||
source: &SourceUnit,
|
||||
plan: &SourcePlan,
|
||||
@@ -1246,6 +1317,7 @@ fn architecture(
|
||||
tree.node(namespace.to_owned(), kind, span, children)
|
||||
}
|
||||
|
||||
/// 建立源文本每行起点表,供字节 span 转换为行列位置。
|
||||
fn line_starts(source: &str) -> Vec<usize> {
|
||||
let bytes = source.as_bytes();
|
||||
let mut starts = vec![0];
|
||||
@@ -1257,6 +1329,7 @@ fn line_starts(source: &str) -> Vec<usize> {
|
||||
starts
|
||||
}
|
||||
|
||||
/// 构造 Sources 语法投影所需的源码与定位辅助状态。
|
||||
struct Architecture {
|
||||
line_starts: Vec<usize>,
|
||||
next_id: usize,
|
||||
@@ -1264,6 +1337,7 @@ struct Architecture {
|
||||
}
|
||||
|
||||
impl Architecture {
|
||||
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
|
||||
fn node(&mut self, name: String, kind: &str, span: Span, children: Vec<CodeNode>) -> CodeNode {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
@@ -1278,6 +1352,7 @@ impl Architecture {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
|
||||
fn declaration(&mut self, declaration: &Decl) -> CodeNode {
|
||||
match &declaration.kind {
|
||||
DeclKind::Function(function) => self.function(function),
|
||||
@@ -1293,6 +1368,7 @@ impl Architecture {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
|
||||
fn function(&mut self, function: &frontend::ast::Function) -> CodeNode {
|
||||
let children = function
|
||||
.body
|
||||
@@ -1308,6 +1384,7 @@ impl Architecture {
|
||||
)
|
||||
}
|
||||
|
||||
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
|
||||
fn statement(&mut self, statement: &Stmt) -> CodeNode {
|
||||
match &statement.kind {
|
||||
StmtKind::MemberAssignment(assignment) => {
|
||||
@@ -1351,6 +1428,7 @@ impl Architecture {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
|
||||
fn expression(&mut self, expr: &Expr) -> CodeNode {
|
||||
let (name, kind, children) = match &expr.kind {
|
||||
ExprKind::Name(name) => (name.clone(), "name", vec![]),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 运行时集成回归与示例验收。测试经真实 Aster 前端、编译器、VM 和展示协议执行,不是手工伪造布局结果。
|
||||
//! 覆盖共享对象/独立 Mount、源覆盖、绑定、窗口、任务、取消、二维/三维投影及像素。局部辅助函数负责取统计值、收敛刷新和构造可重复输入。
|
||||
//! 阅读时先看每个测试提交的 Endor 程序,再看后续变更与最终断言;编译成功、回调成功、文档发布成功是不同检查点。
|
||||
//!
|
||||
use super::*;
|
||||
use crate::document::{NamespaceState, RenderCommand};
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 区分完整 Module 与顶层 Script,并保留输入源码位置。
|
||||
//! 解析包装或片段拼接产生的 span 偏移要递归修正,保证诊断和 Sources 视图仍指向用户原始行列。
|
||||
//!
|
||||
//! Feed a source snapshot to Aster's existing module or interactive entry points.
|
||||
//! Statement syntax, tokens and ASTs all remain Aster-owned. The host only batches
|
||||
//! complete interactive inputs, just as a REPL feeds consecutive inputs to a VM.
|
||||
@@ -7,12 +10,14 @@ use frontend::{
|
||||
ast::{AstRoot, Decl, DeclKind, Expr, ExprKind, InteractiveInput, Stmt, StmtKind},
|
||||
};
|
||||
|
||||
/// 待编译输入是完整模块还是顶层脚本;两者保留不同根结构但使用统一后续接口。
|
||||
pub enum SourcePlan {
|
||||
Module(AstRoot),
|
||||
Script(Vec<AstRoot>),
|
||||
}
|
||||
|
||||
impl SourcePlan {
|
||||
/// 按输入计划返回需要编译的语法根,隐藏 Script 与 Module 组织差异。
|
||||
pub fn roots(&self) -> &[AstRoot] {
|
||||
match self {
|
||||
Self::Module(root) => std::slice::from_ref(root),
|
||||
@@ -21,6 +26,7 @@ impl SourcePlan {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
|
||||
pub fn parse(source: &SourceUnit) -> Result<SourcePlan> {
|
||||
if let Ok(module) = frontend::parse_source_ast(source, ParseMode::Module) {
|
||||
return Ok(SourcePlan::Module(module));
|
||||
@@ -78,11 +84,13 @@ pub fn parse(source: &SourceUnit) -> Result<SourcePlan> {
|
||||
Ok(SourcePlan::Script(roots))
|
||||
}
|
||||
|
||||
/// 递归调整对应语法节点的源码范围,保持片段解析后的错误位置仍指向原输入。
|
||||
fn shift_span(span: &mut Span, offset: usize) {
|
||||
span.start += offset;
|
||||
span.end += offset;
|
||||
}
|
||||
|
||||
/// 递归调整对应语法节点的源码范围,保持片段解析后的错误位置仍指向原输入。
|
||||
fn shift_input(input: &mut InteractiveInput, offset: usize) {
|
||||
shift_span(&mut input.span, offset);
|
||||
for statement in &mut input.statements {
|
||||
@@ -90,6 +98,7 @@ fn shift_input(input: &mut InteractiveInput, offset: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归调整对应语法节点的源码范围,保持片段解析后的错误位置仍指向原输入。
|
||||
fn shift_statement(statement: &mut Stmt, offset: usize) {
|
||||
shift_span(&mut statement.span, offset);
|
||||
match &mut statement.kind {
|
||||
@@ -120,6 +129,7 @@ fn shift_statement(statement: &mut Stmt, offset: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归调整对应语法节点的源码范围,保持片段解析后的错误位置仍指向原输入。
|
||||
fn shift_declaration(declaration: &mut Decl, offset: usize) {
|
||||
shift_span(&mut declaration.span, offset);
|
||||
match &mut declaration.kind {
|
||||
@@ -142,6 +152,7 @@ fn shift_declaration(declaration: &mut Decl, offset: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归调整对应语法节点的源码范围,保持片段解析后的错误位置仍指向原输入。
|
||||
fn shift_function(function: &mut frontend::ast::Function, offset: usize) {
|
||||
shift_span(&mut function.span, offset);
|
||||
shift_span(&mut function.name.span, offset);
|
||||
@@ -157,6 +168,7 @@ fn shift_function(function: &mut frontend::ast::Function, offset: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归调整对应语法节点的源码范围,保持片段解析后的错误位置仍指向原输入。
|
||||
fn shift_expression(expression: &mut Expr, offset: usize) {
|
||||
shift_span(&mut expression.span, offset);
|
||||
match &mut expression.kind {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 托管事件与订阅的传播实现。Connection 持有回调引用,Event 保存目标、阶段与停止传播状态。
|
||||
//! 分发沿出现关系执行 capture → target → bubble,并处理默认行为;只有同一 VM 中的回调可以接触托管对象。
|
||||
//!
|
||||
//! Managed subscriptions and occurrence-specific propagation. A validated VM
|
||||
//! function sequences calls, so all handlers share one execution budget.
|
||||
use crate::{block, mount::Mount, ui_bridge::attach_desktop_type};
|
||||
@@ -7,10 +10,12 @@ use endor_core::{
|
||||
};
|
||||
|
||||
const MAX_DELIVERIES: usize = 10_000;
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
|
||||
/// 检查订阅回调满足事件处理器调用契约,避免等到实际点击才暴露签名错误。
|
||||
pub fn validate_handler(
|
||||
store: &endor_core::ObjectStore,
|
||||
function: ObjectRef,
|
||||
@@ -30,6 +35,7 @@ pub fn validate_handler(
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
/// 本次传播的托管事件状态,包含目标、当前阶段和阻止默认/停止传播控制。
|
||||
pub struct Event {
|
||||
dispatch: Option<WeakObjectRef>,
|
||||
group: usize,
|
||||
@@ -38,36 +44,45 @@ pub struct Event {
|
||||
prevented: bool,
|
||||
}
|
||||
impl ObjectPayload for Event {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-event"
|
||||
}
|
||||
/// 声明此 payload 是否允许使用托管属性存储,具体字段仍由类型与调用检查约束。
|
||||
fn allows_attributes(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// 实际拥有回调引用的托管连接,挂在目标的订阅关系中参与 GC。
|
||||
pub struct Connection {
|
||||
pub handler: Option<ObjectRef>,
|
||||
pub kind: String,
|
||||
pub capture: bool,
|
||||
}
|
||||
impl ObjectPayload for Connection {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-event-connection"
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
if let Some(handler) = self.handler {
|
||||
visit(handler);
|
||||
@@ -77,22 +92,27 @@ impl ObjectPayload for Connection {
|
||||
|
||||
/// A token never retains its target or callback. The Block owns the connection.
|
||||
#[derive(Debug)]
|
||||
/// 源代码可保留的订阅能力包装,用于取消对应连接。
|
||||
pub struct Subscription {
|
||||
pub connection: WeakObjectRef,
|
||||
}
|
||||
impl ObjectPayload for Subscription {
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-subscription"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||||
enum Method {
|
||||
Stop,
|
||||
StopImmediate,
|
||||
@@ -102,10 +122,12 @@ enum Method {
|
||||
Connected,
|
||||
}
|
||||
impl NativeCallable for Method {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Object(owner)] = args else {
|
||||
return Err(invalid("event method receiver"));
|
||||
@@ -179,17 +201,21 @@ impl NativeCallable for Method {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 安装到托管类型上的原生构造调用适配器。
|
||||
pub struct Constructor {
|
||||
pub signature: Option<endor_core::CallSignature>,
|
||||
}
|
||||
impl NativeCallable for Constructor {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
self.signature.clone()
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::range(2, usize::MAX))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Object(definition), Value::Text(kind), values @ ..] = args else {
|
||||
return Err(invalid("Event requires an event kind"));
|
||||
@@ -212,6 +238,7 @@ impl NativeCallable for Constructor {
|
||||
}
|
||||
}
|
||||
|
||||
/// 建立本次分发拥有的托管 Event,保存目标、输入与传播控制状态。
|
||||
pub fn new_event(
|
||||
cx: &mut NativeContext<'_>,
|
||||
definition: ObjectRef,
|
||||
@@ -231,19 +258,23 @@ pub fn new_event(
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 带事件类型签名和 owner 的原生方法包装。
|
||||
struct TypedMethod {
|
||||
method: Method,
|
||||
signature: endor_core::CallSignature,
|
||||
}
|
||||
impl NativeCallable for TypedMethod {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
self.method.invoke(cx, args)
|
||||
}
|
||||
}
|
||||
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub fn install(
|
||||
vm: &mut Vm,
|
||||
roots: &mut Vec<ObjectRef>,
|
||||
@@ -292,12 +323,14 @@ pub fn install(
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 本次递送选择的对象方法或显式订阅处理器。
|
||||
enum Handler {
|
||||
Token(ObjectRef),
|
||||
Legacy(ObjectRef),
|
||||
ObjectMethod,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 事件传播过程中将要调用的一项处理器及其阶段/目标上下文。
|
||||
struct Delivery {
|
||||
event: ObjectRef,
|
||||
owner: ObjectRef,
|
||||
@@ -307,10 +340,12 @@ struct Delivery {
|
||||
handler: Handler,
|
||||
}
|
||||
impl NativeCallable for Delivery {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mut event = cx.store().payload::<Event>(self.event, "Event")?.clone();
|
||||
if event.immediate || event.stopped_at.is_some_and(|group| group != self.group) {
|
||||
@@ -363,12 +398,15 @@ impl NativeCallable for Delivery {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 传播结束后的默认行为收尾步骤。
|
||||
struct Finish(ObjectRef);
|
||||
impl NativeCallable for Finish {
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(0))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mut event = cx.store().payload::<Event>(self.0, "Event")?.clone();
|
||||
event.dispatch = None;
|
||||
@@ -383,6 +421,7 @@ impl NativeCallable for Finish {
|
||||
/// Snapshot the selected occurrence's route and listener order. Disconnection
|
||||
/// before a listener's turn skips it; newly registered listeners wait for the
|
||||
/// next dispatch. A code event without a Mount visits only its target object.
|
||||
/// 按 Mount 祖先路径执行捕获、目标与冒泡阶段,再处理对应默认行为。
|
||||
pub fn dispatch(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 原生窗口输入到执行 worker 的纯数据传输。Input 只含窗口/会话/版本/Mount 身份及有限坐标。
|
||||
//! 连续相机和滚动输入出队时可更新帧版本,但不改变捕获目标和所属 VM;离散点击仍保留严格的过期帧语义。
|
||||
//! 队列有界且当前逐项经本地 HTTP 发送;这不意味着输入已经按显示帧合并,也不是无损无限队列。
|
||||
//!
|
||||
//! Native input transport. Frame/mount identities and optional logical pointer positions cross to the VM worker.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -14,6 +18,7 @@ use winit::event_loop::EventLoopProxy;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
/// 拖放目标窗口、目标布局版本和插入位置,源标签身份由外层 Input 保存。
|
||||
pub struct TabDrop {
|
||||
pub window: u64,
|
||||
pub layout_version: u64,
|
||||
@@ -22,6 +27,7 @@ pub struct TabDrop {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
/// 原生或 HTTP 输入的纯数据报文;身份与坐标必须同时通过 worker 和能力校验。
|
||||
pub struct Input {
|
||||
#[serde(default = "default_window")]
|
||||
pub window: u64,
|
||||
@@ -41,11 +47,13 @@ pub struct Input {
|
||||
pub screen_position: Option<[i32; 2]>,
|
||||
}
|
||||
|
||||
/// 返回默认主窗口身份,供省略窗口字段的兼容输入使用。
|
||||
fn default_window() -> u64 {
|
||||
crate::window::MAIN_WINDOW
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// 检查当前输入或发布值的不变量,调用方据校验结果决定是否继续。
|
||||
pub fn validate(&self) -> Result<(), &'static str> {
|
||||
if self.window == 0 {
|
||||
return Err("window identity must be nonzero");
|
||||
@@ -154,8 +162,10 @@ impl Input {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// 原生输入的有界发送端,后台线程串行发往本地 API;所有窗口共享权威发布状态。
|
||||
pub struct Client(SyncSender<Input>);
|
||||
impl Client {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(
|
||||
proxy: EventLoopProxy<crate::app::AppEvent>,
|
||||
namespace: Arc<RwLock<crate::document::NamespaceState>>,
|
||||
@@ -181,6 +191,7 @@ impl Client {
|
||||
})?;
|
||||
Ok(Self(sender))
|
||||
}
|
||||
/// 通过有界原生输入客户端或本地 HTTP 发送纯数据事件;过期版本按既定协议处理。
|
||||
pub fn send(&self, input: Input) -> bool {
|
||||
self.0.try_send(input).is_ok()
|
||||
}
|
||||
@@ -189,6 +200,7 @@ impl Client {
|
||||
// Native gestures retain the captured Mount across publications. Refresh only
|
||||
// the version at dequeue, never session, window or target. HTTP callers and
|
||||
// discrete actions still receive strict stale-frame rejection.
|
||||
/// 连续手势出队时只更新有效目标的帧版本;保留原 session、窗口和 Mount,离散事件不自动重试。
|
||||
fn refresh_continuous_input(input: &mut Input, current: &crate::document::NamespaceState) -> bool {
|
||||
if !matches!(
|
||||
input.kind.as_str(),
|
||||
@@ -228,6 +240,7 @@ fn refresh_continuous_input(input: &mut Input, current: &crate::document::Namesp
|
||||
true
|
||||
}
|
||||
|
||||
/// 通过有界原生输入客户端或本地 HTTP 发送纯数据事件;过期版本按既定协议处理。
|
||||
fn send(input: &Input) -> anyhow::Result<()> {
|
||||
let body = serde_json::to_string(input)?;
|
||||
let mut stream =
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! 后台文件/网络 provider。线程池只携带纯数据、取消控制与弱 Task 身份,不持有 VM。
|
||||
//! Lease 由托管任务拥有,释放会撤销交付资格并取消可取消的后台工作;已经完成的结果还要由 VM worker 检查归属后才能交付。
|
||||
//! 并发、队列、读取字节数、超时和 URL 协议均有边界,避免 provider 绕过运行时资源限制。
|
||||
//!
|
||||
//! Background providers carry plain data and weak task identities, never a VM.
|
||||
use endor_core::{VmError, WeakObjectRef};
|
||||
use std::{
|
||||
@@ -21,20 +25,24 @@ const QUEUE: usize = 64;
|
||||
type Completion = Result<String, String>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// provider 共享的就绪结果与并发计数;内容只有纯数据和弱任务身份。
|
||||
struct Shared {
|
||||
ready: Mutex<HashMap<WeakObjectRef, Completion>>,
|
||||
active: AtomicUsize,
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 某个运行时拥有的后台 provider 能力,负责提交和取走完成结果。
|
||||
pub struct Provider {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
/// 后台工作取消标记与可取消子进程,不拥有托管 VM。
|
||||
struct Control {
|
||||
cancelled: AtomicBool,
|
||||
process: Mutex<Option<Child>>,
|
||||
}
|
||||
impl Control {
|
||||
/// 撤销当前操作的后续交付资格,并执行对应取消清理。
|
||||
fn cancel(&self) {
|
||||
self.cancelled.store(true, Ordering::Release);
|
||||
if let Ok(mut process) = self.process.lock()
|
||||
@@ -46,19 +54,23 @@ impl Control {
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 后台工作种类及其纯文本参数,源调用的类型检查在提交前完成。
|
||||
pub enum Request {
|
||||
ReadText(String),
|
||||
FetchText(String),
|
||||
}
|
||||
/// Only Task payloads own this lease. Provider threads hold plain control data.
|
||||
#[derive(Debug)]
|
||||
/// 仅托管 Task 拥有的交付租约;释放它会取消后台资格并清理晚到结果。
|
||||
pub struct Lease {
|
||||
shared: Arc<Shared>,
|
||||
task: WeakObjectRef,
|
||||
control: Arc<Control>,
|
||||
}
|
||||
impl Drop for Lease {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
// 租约释放立即取消交付资格;后台 OS 读取即使稍后结束,也不能再次唤醒旧 Task。
|
||||
self.control.cancel();
|
||||
if let Ok(mut ready) = self.shared.ready.lock() {
|
||||
ready.remove(&self.task);
|
||||
@@ -66,6 +78,7 @@ impl Drop for Lease {
|
||||
self.shared.active.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
/// 线程池队列中的纯数据工作,关联弱任务身份、控制和共享完成表。
|
||||
struct Job {
|
||||
request: Request,
|
||||
task: WeakObjectRef,
|
||||
@@ -73,6 +86,7 @@ struct Job {
|
||||
shared: Arc<Shared>,
|
||||
}
|
||||
impl Provider {
|
||||
/// 检查并发额度并取得任务 Lease,再把纯数据工作放入有界线程池队列。
|
||||
pub fn submit(
|
||||
&self,
|
||||
task: WeakObjectRef,
|
||||
@@ -101,6 +115,7 @@ impl Provider {
|
||||
.map_err(|_| VmError::ExecutionLimit("I/O provider queue full or unavailable"))?;
|
||||
Ok(lease)
|
||||
}
|
||||
/// 取走指定弱任务身份的 provider 完成结果;实际托管写入仍由 VM worker 执行。
|
||||
pub fn take_ready(&self) -> Result<Vec<(WeakObjectRef, Completion)>, VmError> {
|
||||
Ok(self
|
||||
.shared
|
||||
@@ -111,6 +126,7 @@ impl Provider {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
/// 懒初始化共享工作线程和有界任务队列;初始化失败反馈给提交方。
|
||||
fn pool() -> Result<&'static SyncSender<Job>, &'static str> {
|
||||
static POOL: OnceLock<Result<SyncSender<Job>, String>> = OnceLock::new();
|
||||
POOL.get_or_init(|| {
|
||||
@@ -150,6 +166,7 @@ fn pool() -> Result<&'static SyncSender<Job>, &'static str> {
|
||||
.as_ref()
|
||||
.map_err(|_| "I/O provider unavailable")
|
||||
}
|
||||
/// 读取受字节上限限制的 UTF-8 文件内容,错误作为任务结果交付。
|
||||
fn read_text(path: &str) -> Completion {
|
||||
let metadata = std::fs::metadata(path).map_err(|error| error.to_string())?;
|
||||
if !metadata.is_file() {
|
||||
@@ -178,6 +195,7 @@ fn read_text(path: &str) -> Completion {
|
||||
|
||||
/// No shell interpretation, curlrc, URL globbing, or non-HTTP redirects.
|
||||
/// Authentication/custom headers are deliberately outside this text GET API.
|
||||
/// 检查网络请求的 URL 协议、长度与凭据等约束,拒绝不受支持的输入。
|
||||
pub fn valid_url(url: &str) -> bool {
|
||||
if url.len() > 8192 || url.chars().any(char::is_control) {
|
||||
return false;
|
||||
@@ -193,8 +211,10 @@ pub fn valid_url(url: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Created only on an I/O worker. All early exits kill and reap the child.
|
||||
/// 子进程作用域收尾保护,避免取消/失败路径遗留未回收进程。
|
||||
struct Reaper<'a>(&'a Control);
|
||||
impl Drop for Reaper<'_> {
|
||||
/// 在 Rust 所有者离开作用域时执行本类型的配套清理;不能用它代替需要报告错误的显式关闭。
|
||||
fn drop(&mut self) {
|
||||
let child = self.0.process.lock().ok().and_then(|mut slot| slot.take());
|
||||
if let Some(mut child) = child {
|
||||
@@ -203,6 +223,7 @@ impl Drop for Reaper<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 在取消、超时、协议和大小限制下取得 HTTP 文本;不把网络执行放在 VM 线程。
|
||||
fn fetch_text(url: &str, control: &Control) -> Completion {
|
||||
if !valid_url(url) {
|
||||
return Err("invalid HTTP(S) URL".into());
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 运行时模块入口。VM 生命周期、API、源程序组织、事件与异步任务在这里组合。
|
||||
//!
|
||||
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
|
||||
pub(crate) mod api;
|
||||
pub(crate) mod endor_host;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! owner/key 管理的异步任务与协作式源函数调度。弱索引用于查找,回调、等待对象和协程帧是托管图中的真实边。
|
||||
//! 任务可能处于等待 I/O、定时器、可执行协程或等待另一任务的状态;取消 owner/key 不能让晚到结果再次执行。
|
||||
//! 调度使用有界量子和公平推进;await 挂起后保留操作数与帧,不重放此前副作用。失败和依赖环有明确处理路径。
|
||||
//!
|
||||
//! Owner-managed asynchronous work. Only weak identities enter the scheduler;
|
||||
//! callbacks and scopes are ordinary traced edges in the Aster object graph.
|
||||
use std::{
|
||||
@@ -24,6 +28,7 @@ const MAX_READY: usize = 64;
|
||||
type Index = Arc<Mutex<Vec<WeakObjectRef>>>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// 任务对源代码可见的生命周期状态,完成/取消/失败各有不同 await 语义。
|
||||
enum State {
|
||||
Pending,
|
||||
Running,
|
||||
@@ -32,6 +37,7 @@ enum State {
|
||||
Failed,
|
||||
}
|
||||
impl State {
|
||||
/// 返回内部枚举或对象对应的稳定源名称。
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Pending => "pending",
|
||||
@@ -44,6 +50,7 @@ impl State {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// 托管异步对象,保存 owner/key、结果与真正拥有的回调/协程/等待引用。
|
||||
struct Task {
|
||||
owner: WeakObjectRef,
|
||||
key: String,
|
||||
@@ -60,15 +67,19 @@ struct Task {
|
||||
result: Option<Value>,
|
||||
}
|
||||
impl ObjectPayload for Task {
|
||||
/// 提供受检查的 Any 下转型入口,让宿主按具体 payload 类型读取数据。
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
/// 返回运行时诊断/类型检查使用的稳定 payload 名称。
|
||||
fn kind_name(&self) -> &'static str {
|
||||
"tessera-task"
|
||||
}
|
||||
/// 声明此 payload 的赋值遵循引用身份语义,避免把内部宿主状态当作可复制值。
|
||||
fn reference_assignment_only(&self) -> bool {
|
||||
true
|
||||
}
|
||||
/// 把 Task 状态投影成 await 的就绪、等待、取消或失败结果,不在等待期间主动轮询执行。
|
||||
fn poll_await(&self) -> Result<endor_core::AwaitPoll, VmError> {
|
||||
match self.state {
|
||||
State::Pending | State::Running => Ok(endor_core::AwaitPoll::Pending),
|
||||
@@ -86,6 +97,7 @@ impl ObjectPayload for Task {
|
||||
)),
|
||||
}
|
||||
}
|
||||
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
|
||||
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
|
||||
if let Some(callback) = self.callback {
|
||||
visit(callback);
|
||||
@@ -101,15 +113,18 @@ impl ObjectPayload for Task {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||||
fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
match value {
|
||||
Value::Object(object) => Ok(*object),
|
||||
_ => Err(invalid("task owner and callback must be managed objects")),
|
||||
}
|
||||
}
|
||||
/// 取得 owner 关联的托管任务作用域,供 key 替换和取消使用。
|
||||
fn scope(store: &ObjectStore, owner: ObjectRef) -> Result<Option<ObjectRef>, VmError> {
|
||||
let payload = store.object(owner)?.payload();
|
||||
if let Some(value) = payload.as_any().downcast_ref::<Block>() {
|
||||
@@ -132,6 +147,7 @@ fn scope(store: &ObjectStore, owner: ObjectRef) -> Result<Option<ObjectRef>, VmE
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
/// 为可拥有任务的对象安装作用域,真实引用仍由托管图追踪。
|
||||
fn install_scope(
|
||||
cx: &mut NativeContext<'_>,
|
||||
owner: ObjectRef,
|
||||
@@ -155,6 +171,7 @@ fn install_scope(
|
||||
}
|
||||
cx.set_attr(owner, SCOPE, Value::Object(scope))
|
||||
}
|
||||
/// 检查任务的 owner 仍有效且任务具备交付条件,避免晚到结果复活已关闭页面。
|
||||
fn eligible(store: &ObjectStore, owner: ObjectRef, desktop: ObjectRef) -> Result<bool, VmError> {
|
||||
let payload = store.object(owner)?.payload();
|
||||
if let Some(mount) = payload.as_any().downcast_ref::<Mount>() {
|
||||
@@ -194,6 +211,7 @@ fn eligible(store: &ObjectStore, owner: ObjectRef, desktop: ObjectRef) -> Result
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
/// 检查弱索引对应的任务是否仍是 owner/key 当前登记的那一项。
|
||||
fn current(
|
||||
store: &ObjectStore,
|
||||
owner: ObjectRef,
|
||||
@@ -210,6 +228,7 @@ fn current(
|
||||
|
||||
/// Called at the actual close/detach boundary, including close-then-reopen in
|
||||
/// one language input. A later appearance never revives these identities.
|
||||
/// 取消某个 owner 的全部任务,防止页面/窗口生命周期结束后继续执行回调。
|
||||
pub fn cancel_owner(cx: &mut NativeContext<'_>, owner: ObjectRef) -> Result<(), VmError> {
|
||||
let Some(scope) = scope(cx.store(), owner)? else {
|
||||
return Ok(());
|
||||
@@ -233,6 +252,7 @@ pub fn cancel_owner(cx: &mut NativeContext<'_>, owner: ObjectRef) -> Result<(),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// 当前宿主 API 的操作分类,分派代码负责参数校验与状态更新。
|
||||
enum Operation {
|
||||
Timer,
|
||||
Sleep,
|
||||
@@ -241,6 +261,7 @@ enum Operation {
|
||||
Spawn,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 创建定时/I/O/协作任务时的原生调用适配器。
|
||||
struct After {
|
||||
operation: Operation,
|
||||
provider: crate::io_tasks::Provider,
|
||||
@@ -251,13 +272,16 @@ struct After {
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for After {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(self.signature.arity)
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
if self.quiescing.load(Ordering::Acquire) {
|
||||
return Err(invalid("runtime is exiting; new tasks are forbidden"));
|
||||
@@ -459,6 +483,7 @@ impl NativeCallable for After {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
|
||||
enum Query {
|
||||
Cancel,
|
||||
Status,
|
||||
@@ -467,19 +492,23 @@ enum Query {
|
||||
Error,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||||
struct Method {
|
||||
signature: endor_core::CallSignature,
|
||||
query: Query,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Method {
|
||||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||||
fn signature(&self) -> Option<endor_core::CallSignature> {
|
||||
Some(self.signature.clone())
|
||||
}
|
||||
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
||||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||||
Some(endor_core::CallArity::exact(1))
|
||||
}
|
||||
|
||||
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [value] = args else {
|
||||
return Err(invalid("Task methods accept no arguments"));
|
||||
@@ -561,6 +590,7 @@ impl NativeCallable for Method {
|
||||
}
|
||||
}
|
||||
|
||||
/// 弱任务索引、公平调度位置和 provider,索引不会单独把任务保活。
|
||||
pub struct Scheduler {
|
||||
index: Index,
|
||||
runnable: Arc<AtomicBool>,
|
||||
@@ -569,15 +599,18 @@ pub struct Scheduler {
|
||||
provider: crate::io_tasks::Provider,
|
||||
}
|
||||
impl Scheduler {
|
||||
/// 停止接收该域的新任务工作,为关闭/替换运行时做准备。
|
||||
pub fn quiesce(&self, vm: &mut Vm) -> Result<usize> {
|
||||
self.quiescing.store(true, Ordering::Release);
|
||||
self.cancel_all(vm)
|
||||
}
|
||||
/// 检查是否有可推进的协程,而不是仅仅存在正在等待外部结果的任务。
|
||||
pub fn has_cooperative_work(&self) -> bool {
|
||||
self.runnable.load(Ordering::Acquire)
|
||||
}
|
||||
/// Mutation boundaries collect unreachable owner/callback cycles before
|
||||
/// this weak lookup. Idle timer polling does not rescan the entire heap.
|
||||
/// 在当前 VM 中有界交付就绪任务、执行回调或推进协程;开始布局后由外层刷新流程继续。
|
||||
pub fn poll(&self, vm: &mut Vm, limit: usize) -> Result<usize> {
|
||||
anyhow::ensure!((1..=MAX_READY).contains(&limit), "invalid task poll limit");
|
||||
// Provider threads only fill the mailbox. Only this VM worker updates
|
||||
@@ -731,6 +764,7 @@ impl Scheduler {
|
||||
self.prune(vm)?;
|
||||
Ok(work)
|
||||
}
|
||||
/// 取消当前域全部任务,切断回调、协程和等待交付资格。
|
||||
pub fn cancel_all(&self, vm: &mut Vm) -> Result<usize> {
|
||||
let snapshot = self
|
||||
.index
|
||||
@@ -753,6 +787,7 @@ impl Scheduler {
|
||||
self.runnable.store(false, Ordering::Release);
|
||||
Ok(count)
|
||||
}
|
||||
/// 移除已释放的弱任务索引,索引本身不让任务或其 owner 存活。
|
||||
fn prune(&self, vm: &mut Vm) -> Result<()> {
|
||||
let mut index = self
|
||||
.index
|
||||
@@ -789,6 +824,7 @@ impl Scheduler {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// 沿等待链检查任务依赖环,防止 await 永远无法被满足。
|
||||
fn cyclic_wait(vm: &Vm, source: ObjectRef, mut target: ObjectRef) -> Result<bool> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
loop {
|
||||
@@ -805,6 +841,7 @@ fn cyclic_wait(vm: &Vm, source: ObjectRef, mut target: ObjectRef) -> Result<bool
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复已保存的源函数帧,处理完成、软量子挂起或 await 等待,保留已发生的副作用。
|
||||
fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> {
|
||||
let mut task = vm.payload::<Task>(reference, "Task")?.clone();
|
||||
let coroutine = if let Some(coroutine) = task.coroutine {
|
||||
@@ -897,6 +934,7 @@ fn advance_coroutine(vm: &mut Vm, reference: ObjectRef) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录回调执行失败并沿运行时故障规则停止后续不安全的交付。
|
||||
fn fail_callback(vm: &mut Vm, reference: ObjectRef, message: &str) -> Result<()> {
|
||||
let mut task = vm.payload::<Task>(reference, "Task")?.clone();
|
||||
task.error = Some(message.chars().take(1024).collect::<String>().into());
|
||||
@@ -904,6 +942,7 @@ fn fail_callback(vm: &mut Vm, reference: ObjectRef, message: &str) -> Result<()>
|
||||
finish(vm, reference, State::Failed)
|
||||
}
|
||||
|
||||
/// 将任务转入终态并释放回调、provider 租约、协程和等待引用;取消还会清除保存的结果。
|
||||
fn finish(vm: &mut Vm, reference: ObjectRef, state: State) -> Result<()> {
|
||||
let mut task = vm.payload::<Task>(reference, "Task")?.clone();
|
||||
if !matches!(task.state, State::Pending | State::Running) {
|
||||
@@ -934,6 +973,7 @@ fn finish(vm: &mut Vm, reference: ObjectRef, state: State) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||||
pub fn install(
|
||||
vm: &mut Vm,
|
||||
roots: &mut Vec<ObjectRef>,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
//! 构造共享 VM 调用序列与同帧循环驱动的字节码。
|
||||
//! 用托管函数串联步骤,避免为每个布局节点反复创建独立解释器调用;重复驱动会保留同一个执行帧和预算域。
|
||||
//!
|
||||
//! Validated call sequences share the current VM budget and explicit frames.
|
||||
use endor_core::{
|
||||
CodeLayout, EndorCode, EndorCodeObject, Instruction, NativeContext, ObjectRef, Opcode, Value,
|
||||
VmError,
|
||||
};
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub fn build(
|
||||
cx: &mut NativeContext<'_>,
|
||||
calls: Vec<ObjectRef>,
|
||||
@@ -36,6 +40,7 @@ pub fn build(
|
||||
|
||||
/// A fixed-size driver. Each callback returns whether another occurrence is
|
||||
/// needed; delegated source calls and all iterations share this VM budget.
|
||||
/// 构造重复调用同一托管函数的驱动,保持共享参数与引用生命周期。
|
||||
pub fn repeat_call(
|
||||
cx: &mut NativeContext<'_>,
|
||||
callback: ObjectRef,
|
||||
@@ -63,6 +68,7 @@ pub fn repeat_call(
|
||||
|
||||
/// Prepare only the current occurrence, collect its result, then continue the
|
||||
/// outer driver. The helper frame and its managed captures die before the next.
|
||||
/// 将一步调用结果收集到驱动状态,再安排下一步调用。
|
||||
pub fn collect_and_continue(
|
||||
cx: &mut NativeContext<'_>,
|
||||
collector: ObjectRef,
|
||||
@@ -94,6 +100,7 @@ pub fn collect_and_continue(
|
||||
driver(cx, instructions, constants, name, desktop)
|
||||
}
|
||||
|
||||
/// 建立承载整个步骤序列的托管函数,使续体可以在同一个 VM 帧中恢复。
|
||||
fn driver(
|
||||
cx: &mut NativeContext<'_>,
|
||||
instructions: Vec<Instruction>,
|
||||
@@ -112,6 +119,7 @@ fn driver(
|
||||
|
||||
/// Consume the condition on the stack: true repeats this same frame from zero,
|
||||
/// false falls through. Operand widths include EXTENDED_ARG code units.
|
||||
/// 构造由继续条件决定是否再执行一轮的同帧循环。
|
||||
pub fn repeat_while(instructions: &mut Vec<Instruction>) -> Result<(), VmError> {
|
||||
let body = Instruction::encode_all(instructions)?.len() / 2;
|
||||
let mut back = Instruction::new(Opcode::JumpBackward, 0);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 有界 HTTP 活动记录。保存请求/响应的可显示预览供 Network 工具读取,不负责执行请求。
|
||||
//! 正文预览和允许显示的头部受限制,避免活动面板成为无限日志或暴露不必要数据的通道。
|
||||
//!
|
||||
//! Bounded observation of this process's local HTTP API, never external traffic.
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
@@ -10,6 +13,7 @@ pub const BODY_PREVIEW: usize = 32 * 1024;
|
||||
const MAX_RECORDS: usize = 80;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// 一次 HTTP 请求/响应的有界观察记录,供 Network 工具查看。
|
||||
pub struct Exchange {
|
||||
pub id: u64,
|
||||
pub method: String,
|
||||
@@ -26,11 +30,13 @@ pub struct Exchange {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 按容量维护近期 HTTP 活动与请求身份,防止无限日志积累。
|
||||
pub struct ActivityLog {
|
||||
pub records: VecDeque<Exchange>,
|
||||
next_id: u64,
|
||||
}
|
||||
impl ActivityLog {
|
||||
/// 为请求分配活动 ID 并登记其方法、来源和受限头部。
|
||||
pub fn begin(&mut self, method: String, path: String, origin: String, headers: String) -> u64 {
|
||||
self.next_id += 1;
|
||||
let id = self.next_id;
|
||||
@@ -63,11 +69,13 @@ impl ActivityLog {
|
||||
}
|
||||
id
|
||||
}
|
||||
/// 更新请求正文预览,内容长度仍由活动记录策略限制。
|
||||
pub fn body(&mut self, id: u64, body: &str) {
|
||||
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
||||
record.request = preview(body);
|
||||
}
|
||||
}
|
||||
/// 完成本模块当前阶段的结果整理,把后续工作交回调用方。
|
||||
pub fn finish(&mut self, id: u64, status: u16, body: &str, headers: String) {
|
||||
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
||||
record.status = Some(status);
|
||||
@@ -78,6 +86,7 @@ impl ActivityLog {
|
||||
}
|
||||
}
|
||||
|
||||
/// 截取可显示的受限文本预览,避免日志无限保留完整大正文。
|
||||
pub fn preview(value: &str) -> String {
|
||||
if value.len() <= BODY_PREVIEW {
|
||||
return value.to_owned();
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! winit 原生应用生命周期和窗口事件入口。这里创建窗口、发布逻辑视口、转发输入,并把不可变文档画到原生表面。
|
||||
//! 窗口线程不执行 Endor 业务回调;共享状态通过锁读取,实际 VM 更新由 runtime worker 串行完成。
|
||||
//! 侧栏、页面与开发工具有各自命中区域。拖动捕获必须保留最初的视口目标,不能随指针经过其他组件而跳转。
|
||||
//!
|
||||
use std::{
|
||||
num::NonZeroU32,
|
||||
sync::{Arc, RwLock},
|
||||
@@ -24,6 +28,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// worker 到原生事件循环的通知,携带更新/错误/退出状态而不是可执行 VM 引用。
|
||||
pub enum AppEvent {
|
||||
RuntimeFailed { message: String },
|
||||
DocumentUpdated,
|
||||
@@ -31,6 +36,7 @@ pub enum AppEvent {
|
||||
ConsoleFailed { message: String, source: String },
|
||||
}
|
||||
|
||||
/// 一个原生窗口的宿主状态;主窗口还协调子窗口资源,共用同一运行时与输入通道。
|
||||
pub struct TesseraApp {
|
||||
resource: u64,
|
||||
control: crate::api::RuntimeControl,
|
||||
@@ -63,6 +69,7 @@ pub struct TesseraApp {
|
||||
}
|
||||
|
||||
impl TesseraApp {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(
|
||||
namespace: Arc<RwLock<NamespaceState>>,
|
||||
activity: Activity,
|
||||
@@ -79,6 +86,7 @@ impl TesseraApp {
|
||||
))
|
||||
}
|
||||
|
||||
/// 建立窗口宿主状态并复用权威输入客户端,避免子窗口使用滞后的独立事件队列。
|
||||
fn with_event_client(
|
||||
namespace: Arc<RwLock<NamespaceState>>,
|
||||
activity: Activity,
|
||||
@@ -118,6 +126,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将已发布窗口文档与原生资源对齐;session 更换时丢弃旧域的子窗口。
|
||||
fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.resource != crate::window::MAIN_WINDOW {
|
||||
return;
|
||||
@@ -183,6 +192,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 只应用当前运行时身份的窗口命令;执行前从共享队列取走,避免重复执行。
|
||||
fn apply_window_commands(&mut self) {
|
||||
let Ok(mut namespace) = self.namespace.write() else {
|
||||
return;
|
||||
@@ -236,6 +246,7 @@ impl TesseraApp {
|
||||
self.publish_viewport();
|
||||
}
|
||||
|
||||
/// 将真实原生窗口尺寸换算成扣除侧栏的逻辑页面尺寸,并发布给 worker。
|
||||
fn publish_viewport(&self) {
|
||||
let Some(window) = &self.window else {
|
||||
return;
|
||||
@@ -280,12 +291,14 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅请求原生事件循环安排重绘,不在调用处立即递归绘制。
|
||||
fn request_redraw(&self) {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过托管开发工具入口打开指定工具页面,保留对应窗口上下文。
|
||||
fn open_developer_tool(&mut self, name: &str) {
|
||||
let input = self.namespace.read().ok().map(|namespace| {
|
||||
let frame = &namespace.document.frame;
|
||||
@@ -313,6 +326,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 接收完整文档更新,协调子窗口和原生命令,再更新输入焦点并请求重绘。
|
||||
fn document_updated(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.reconcile_windows(event_loop);
|
||||
self.apply_window_commands();
|
||||
@@ -343,6 +357,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 先处理宿主管理输入、选择与快捷键,再按焦点决定是否交给页面。
|
||||
fn handle_keyboard(&mut self, event: &winit::event::KeyEvent) {
|
||||
let command = self.modifiers.super_key() || self.modifiers.control_key();
|
||||
if command
|
||||
@@ -486,6 +501,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 判断指针是否被侧栏或管理面板覆盖,阻止一次点击同时落入页面。
|
||||
fn guest_covered(&self, point: Point) -> bool {
|
||||
if self.sidebar.covers(point) {
|
||||
return true;
|
||||
@@ -501,6 +517,7 @@ impl TesseraApp {
|
||||
.contains(point)
|
||||
}
|
||||
|
||||
/// 排除宿主管理区域后,把原生指针转换到页面使用的坐标系。
|
||||
fn guest_point(&self) -> Option<Point> {
|
||||
if self.guest_covered(self.pointer) {
|
||||
return None;
|
||||
@@ -518,6 +535,7 @@ impl TesseraApp {
|
||||
Some(current.document.frame.surface.to_world(point))
|
||||
}
|
||||
|
||||
/// 从页面坐标与当前帧命中结果构造带版本的点击输入。
|
||||
fn guest_hit(&self) -> Option<crate::events::Input> {
|
||||
let point = self.guest_point()?;
|
||||
let current = self.namespace.read().ok()?;
|
||||
@@ -540,6 +558,7 @@ impl TesseraApp {
|
||||
})
|
||||
}
|
||||
|
||||
/// 将标签快捷键转换为当前窗口/帧中的明确页面动作。
|
||||
fn send_tab_shortcut(&mut self, close: bool) -> bool {
|
||||
let input = self.namespace.read().ok().and_then(|namespace| {
|
||||
let frame = &namespace.document.frame;
|
||||
@@ -577,6 +596,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理侧栏展开、工具入口和页面管理,尺寸变化还需重新发布可用视口。
|
||||
fn sidebar_action(&mut self, action: crate::sidebar::Action) {
|
||||
use crate::sidebar::Action;
|
||||
match action {
|
||||
@@ -619,6 +639,7 @@ impl TesseraApp {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// 在手势开始时捕获具体相机视口 ID,后续拖动保持同一目标。
|
||||
fn begin_pan(&mut self) -> bool {
|
||||
self.pan_viewport = None;
|
||||
let Some(point) = self.guest_point() else {
|
||||
@@ -632,6 +653,7 @@ impl TesseraApp {
|
||||
self.pan_viewport.is_some() || frame.surface.mode != crate::page::Mode::Window
|
||||
}
|
||||
|
||||
/// 把窗口位移/缩放转换为捕获视口的纯数据相机事件,处理侧栏和页面视角的尺度差异。
|
||||
fn page_view(&mut self, kind: &str, delta: [f32; 2]) {
|
||||
if !self.panning && self.guest_covered(self.pointer) {
|
||||
return;
|
||||
@@ -724,6 +746,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把已经构造的页面输入交给有界客户端,保持 GUI 线程不直接调用 VM。
|
||||
fn send_guest(&mut self, event: crate::events::Input) {
|
||||
if !self.event_client.send(event) {
|
||||
self.developer_tools.input_error = "Native event queue is unavailable or full".into();
|
||||
@@ -731,6 +754,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把页面拥有焦点时的键盘动作转换为托管事件,保留按下/释放配对。
|
||||
fn handle_guest_key(&mut self, event: &winit::event::KeyEvent) -> bool {
|
||||
let Key::Named(key) = &event.logical_key else {
|
||||
return false;
|
||||
@@ -800,6 +824,7 @@ impl TesseraApp {
|
||||
false
|
||||
}
|
||||
|
||||
/// 按平台窗口属性创建原生窗口、软件表面和输入法支持,再公布实际逻辑视口。
|
||||
fn create_native(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
|
||||
if self.window.is_some() {
|
||||
return Ok(());
|
||||
@@ -847,6 +872,7 @@ impl TesseraApp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止新的交互并请求运行时清理;原生事件循环等待完成通知后退出。
|
||||
fn request_exit(&mut self) {
|
||||
if self.exit_requested {
|
||||
return;
|
||||
@@ -863,6 +889,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 把原生应用记录的最终错误反馈给 main 的退出结果。
|
||||
pub fn exit_result(&self) -> anyhow::Result<()> {
|
||||
if let Some(error) = &self.native_error {
|
||||
anyhow::bail!("{error}");
|
||||
@@ -870,6 +897,7 @@ impl TesseraApp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 包装一次实际绘制,统一记录故障并避免对同一错误反复发送通知。
|
||||
fn redraw(&mut self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
@@ -891,6 +919,7 @@ impl TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取完整发布文档,生成整窗显示列表,写入原生像素表面并呈现;错误交由外层记录。
|
||||
fn try_redraw(&mut self) -> anyhow::Result<()> {
|
||||
let guest_cursor = self.guest_hit().is_some()
|
||||
|| self
|
||||
@@ -956,6 +985,7 @@ impl TesseraApp {
|
||||
}
|
||||
|
||||
impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
/// 处理 OS 直接结束事件循环的收尾,尝试在明确时限内等待运行时清理。
|
||||
fn exiting(&mut self, _: &ActiveEventLoop) {
|
||||
// OS termination can bypass CloseRequested. The loop is already ending;
|
||||
// use a bounded fallback rather than silently dropping every callback.
|
||||
@@ -973,8 +1003,10 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前不在进入等待前轮询工作;文档通知和原生事件负责唤醒循环,避免空闲忙转。
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {}
|
||||
|
||||
/// 应用获得原生活跃上下文时建立窗口,并仅在需要时请求初始页面。
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.resource == crate::window::MAIN_WINDOW
|
||||
&& !self.startup_page_requested
|
||||
@@ -998,6 +1030,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理 worker 发回的文档更新、故障和退出完成通知,不在 GUI 线程执行源回调。
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: AppEvent) {
|
||||
if self.control.is_exiting() && !matches!(&event, AppEvent::ShutdownComplete { .. }) {
|
||||
return;
|
||||
@@ -1029,6 +1062,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理原生尺寸、输入、焦点和关闭事件;先判断宿主控件,再转发页面事件。
|
||||
fn window_event(
|
||||
&mut self,
|
||||
_event_loop: &ActiveEventLoop,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! 把 UTF-8 文本读写适配到当前平台的系统剪贴板命令。
|
||||
//! 外部命令失败作为普通错误交给调用方处理;这里不解析或执行剪贴板中的程序文本。
|
||||
//!
|
||||
//! Clipboard text is always UTF-8, including Finder/LaunchServices launches without LANG.
|
||||
use anyhow::{Result, bail};
|
||||
#[cfg(target_os = "macos")]
|
||||
/// 运行选定平台的剪贴板命令并检查其退出状态。
|
||||
fn command(program: &str) -> std::process::Command {
|
||||
let mut cmd = std::process::Command::new(program);
|
||||
cmd.env("LC_ALL", "en_US.UTF-8")
|
||||
@@ -8,6 +12,7 @@ fn command(program: &str) -> std::process::Command {
|
||||
.env("LANG", "en_US.UTF-8");
|
||||
cmd
|
||||
}
|
||||
/// 取得系统剪贴板的 UTF-8 文本,失败作为可显示错误返回。
|
||||
pub fn read() -> Result<String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -26,6 +31,7 @@ pub fn read() -> Result<String> {
|
||||
bail!("system clipboard adapter is not available on this platform")
|
||||
}
|
||||
}
|
||||
/// 将提供的文本交给系统剪贴板命令,不执行文本内容。
|
||||
pub fn write(value: &str) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//! Console/Sources/Network/Settings 的宿主交互协调与绘制状态。
|
||||
//! 工具标签有独立焦点、滚动和文本选择状态;源程序与结果来自当前发布文档,提交动作通过本地 API 进入 worker。
|
||||
//! 输入法预编辑、光标、选择和控件捕获分别处理,避免把界面操作直接当成业务模型写入。
|
||||
//!
|
||||
//! Native developer workspace. Coordinates and scroll offsets are logical pixels.
|
||||
use crate::{
|
||||
activity::{Activity, Exchange},
|
||||
@@ -32,6 +36,7 @@ use selection::SourceSelection;
|
||||
const ROW: f32 = 19.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
/// 当前管理工具种类,决定使用 Console/Sources/Network/Settings 哪份状态。
|
||||
pub enum Tab {
|
||||
#[default]
|
||||
Console,
|
||||
@@ -40,6 +45,7 @@ pub enum Tab {
|
||||
Settings,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 管理面板中可区分的输入/列表/详情区域。
|
||||
pub enum Region {
|
||||
Transcript = 0,
|
||||
Tree,
|
||||
@@ -48,11 +54,13 @@ pub enum Region {
|
||||
Details,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
/// 工具区域保留的滚动偏移与内容范围。
|
||||
struct Scroll {
|
||||
x: f32,
|
||||
y: f32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 当前工具内接受键盘输入的目标类别。
|
||||
pub enum Focus {
|
||||
Source,
|
||||
Input,
|
||||
@@ -60,6 +68,7 @@ pub enum Focus {
|
||||
Custom,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
/// 工具控件激活后执行的管理动作,避免在绘制时产生业务副作用。
|
||||
pub enum Action {
|
||||
ClearConsole,
|
||||
ClearNetwork,
|
||||
@@ -75,11 +84,13 @@ pub enum Action {
|
||||
Theme(usize),
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 工具画面分隔布局的可调整方向/区域。
|
||||
pub enum Split {
|
||||
Tree,
|
||||
Network,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 正在捕获的工具拖动操作,移动时保持原目标。
|
||||
enum Drag {
|
||||
Split(Split),
|
||||
Scroll(Region, bool, f32),
|
||||
@@ -87,11 +98,13 @@ enum Drag {
|
||||
|
||||
/// Each workspace keeps its own input, focus, selection, scroll and request state.
|
||||
/// Only appearance and the window-level launcher are shared.
|
||||
/// 多工具页的状态拥有者,切换时保存/恢复当前页数据。
|
||||
pub struct Workspace {
|
||||
states: [DeveloperToolsState; 4],
|
||||
active: usize,
|
||||
}
|
||||
impl Workspace {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(proxy: winit::event_loop::EventLoopProxy<AppEvent>) -> Self {
|
||||
let states = [Tab::Console, Tab::Sources, Tab::Network, Tab::Settings].map(|tab| {
|
||||
DeveloperToolsState {
|
||||
@@ -102,6 +115,7 @@ impl Workspace {
|
||||
});
|
||||
Self { states, active: 0 }
|
||||
}
|
||||
/// 选择一个工具页并恢复它各自保留的输入、焦点和滚动状态。
|
||||
pub fn select(&mut self, name: Option<&str>) {
|
||||
let next = match name {
|
||||
Some("Sources") => 1,
|
||||
@@ -120,17 +134,20 @@ impl Workspace {
|
||||
}
|
||||
impl std::ops::Deref for Workspace {
|
||||
type Target = DeveloperToolsState;
|
||||
/// 将包装类型的只读访问转交给内部当前上下文。
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.states[self.active]
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for Workspace {
|
||||
/// 将包装类型的可变访问转交给内部当前上下文。
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.states[self.active]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// 一份工具页面的输入、选择、滚动、焦点和显示配置。
|
||||
pub struct DeveloperToolsState {
|
||||
pub open: bool,
|
||||
pointer: Point,
|
||||
@@ -167,6 +184,7 @@ pub struct DeveloperToolsState {
|
||||
draft: String,
|
||||
}
|
||||
impl Default for DeveloperToolsState {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
@@ -206,6 +224,7 @@ impl Default for DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 本帧工具面板与子区域的几何,用于绘制和原生事件命中。
|
||||
pub struct DeveloperToolsLayout {
|
||||
pub panel: Rect,
|
||||
pub widgets: Vec<Target<Action>>,
|
||||
@@ -216,6 +235,7 @@ pub struct DeveloperToolsLayout {
|
||||
source: Option<(Rect, Point, Vec<String>)>,
|
||||
}
|
||||
impl DeveloperToolsLayout {
|
||||
/// 将工具局部绘制/交互布局平移到实际窗口位置。
|
||||
pub fn translate(&mut self, dx: f32, dy: f32) {
|
||||
self.panel.x += dx;
|
||||
self.panel.y += dy;
|
||||
@@ -250,11 +270,13 @@ impl DeveloperToolsLayout {
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// Console 文字输入、光标与可见范围的本帧排版。
|
||||
struct ConsoleInputLayout {
|
||||
viewport: Rect,
|
||||
origin: Point,
|
||||
}
|
||||
impl DeveloperToolsState {
|
||||
/// 用最新完整源文档更新工具投影,维护 Sources 与 Console 历史的一致性。
|
||||
pub fn document_updated(&mut self, doc: &RenderDocument) {
|
||||
if self
|
||||
.selected_node_id
|
||||
@@ -264,6 +286,7 @@ impl DeveloperToolsState {
|
||||
self.selected_node_id = doc.root.as_ref().map(|n| n.id.clone());
|
||||
}
|
||||
}
|
||||
/// 根据当前工具命中区域选择文本、拖动或默认系统光标。
|
||||
pub fn cursor_kind(&self, layout: &DeveloperToolsLayout) -> winit::window::CursorIcon {
|
||||
use winit::window::CursorIcon;
|
||||
if !self.open {
|
||||
@@ -286,15 +309,18 @@ impl DeveloperToolsState {
|
||||
Cursor::Disabled => CursorIcon::NotAllowed,
|
||||
}
|
||||
}
|
||||
/// 清理指针或焦点离开后的悬停/捕获状态,避免按下状态残留。
|
||||
pub fn pointer_left(&mut self) {
|
||||
self.interaction.pointer_left();
|
||||
}
|
||||
/// 清理指针或焦点离开后的悬停/捕获状态,避免按下状态残留。
|
||||
pub fn blur(&mut self) {
|
||||
self.interaction.blur();
|
||||
self.drag = None;
|
||||
self.focus = None;
|
||||
self.preedit.clear();
|
||||
}
|
||||
/// 记录按下时的控件或文本选择目标,为释放激活和拖动保留捕获。
|
||||
pub fn press(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
|
||||
if !self.open || !layout.panel.contains(p) {
|
||||
return;
|
||||
@@ -333,6 +359,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 执行已确认激活的管理控件动作,区分工具切换、源码操作和设置。
|
||||
fn activate(
|
||||
&mut self,
|
||||
action: Option<Action>,
|
||||
@@ -423,6 +450,7 @@ impl DeveloperToolsState {
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
/// 结束捕获并按同一目标/启用条件决定是否执行操作。
|
||||
pub fn release(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
|
||||
self.pointer = p;
|
||||
self.interaction.pointer_moved(p, &layout.widgets);
|
||||
@@ -433,6 +461,7 @@ impl DeveloperToolsState {
|
||||
self.activate(action, layout, p, activity);
|
||||
}
|
||||
}
|
||||
/// 按工具控件焦点处理 Tab、确认等管理键盘交互。
|
||||
pub fn keyboard_control(
|
||||
&mut self,
|
||||
event: &winit::event::KeyEvent,
|
||||
@@ -491,6 +520,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
false
|
||||
}
|
||||
/// 更新工具内悬停、拖动、选择或分隔条位置。
|
||||
pub fn move_pointer(
|
||||
&mut self,
|
||||
p: Point,
|
||||
@@ -535,6 +565,7 @@ impl DeveloperToolsState {
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
/// 将滚轮作用到当前工具的可滚动区域,钳制本工具偏移。
|
||||
pub fn wheel(&mut self, layout: &DeveloperToolsLayout, p: Point, dx: f32, dy: f32) {
|
||||
if let Some((_, r, _, _)) = layout
|
||||
.regions
|
||||
@@ -550,6 +581,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 在当前选择/光标处插入输入文本,并更新光标与可见范围。
|
||||
pub fn insert(&mut self, value: &str) {
|
||||
match self.focus {
|
||||
Some(Focus::Input) => {
|
||||
@@ -591,9 +623,11 @@ impl DeveloperToolsState {
|
||||
Some(Focus::Source) | None => {}
|
||||
}
|
||||
}
|
||||
/// 调整输入区滚动位置,让当前光标保持在可见范围内。
|
||||
pub(crate) fn reveal_cursor(&mut self) {
|
||||
self.reveal_input = true;
|
||||
}
|
||||
/// 返回输入法候选窗应跟随的逻辑光标位置。
|
||||
pub fn ime_position(&self, layout: &DeveloperToolsLayout) -> Point {
|
||||
if let Some(input) = layout.input {
|
||||
let before = &self.input[..self.cursor];
|
||||
@@ -608,6 +642,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
Point::new(layout.panel.x + 20.0, 60.0)
|
||||
}
|
||||
/// 取得当前工具文本选择内容,供复制命令使用。
|
||||
pub fn selected_text(&self) -> String {
|
||||
if self.focus == Some(Focus::Source) {
|
||||
return self.source_selection.text(self.selection);
|
||||
@@ -622,6 +657,7 @@ impl DeveloperToolsState {
|
||||
Some(Focus::Source) | None => String::new(),
|
||||
}
|
||||
}
|
||||
/// 处理编辑键和导航键,维护 UTF-8 光标/选择边界。
|
||||
pub fn key(&mut self, key: &winit::keyboard::NamedKey, shift: bool, command: bool) {
|
||||
use winit::keyboard::NamedKey::*;
|
||||
if *key == Escape {
|
||||
@@ -792,6 +828,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
self.reveal_cursor();
|
||||
}
|
||||
/// 初始化内置工具对应的页面和展示状态,避免重复建立同一组工具。
|
||||
pub fn initialize_tools(&mut self, name: &str) {
|
||||
let name = name.to_owned();
|
||||
let proxy = self.proxy.clone();
|
||||
@@ -823,9 +860,11 @@ impl DeveloperToolsState {
|
||||
}
|
||||
});
|
||||
}
|
||||
/// 把当前工具输入按所选模式提交给本地 API,并异步反馈结果。
|
||||
fn submit(&mut self) {
|
||||
self.submit_console(false);
|
||||
}
|
||||
/// 提交 Console 输入到持久运行时,而不是把全部历史重新执行一遍。
|
||||
fn submit_console(&mut self, reset: bool) {
|
||||
if !reset && self.input.trim().is_empty() {
|
||||
return;
|
||||
@@ -867,6 +906,7 @@ impl DeveloperToolsState {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub fn build(
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制 Console 输入、结果历史和对应命中区域。
|
||||
//! 这里消费工具状态并生成显示列表;用户程序的编译执行由提交路径和 worker 完成。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(
|
||||
p: &mut Painter,
|
||||
state: &mut DeveloperToolsState,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制已观察到的 HTTP 活动、请求列表与详情。
|
||||
//! 筛选、选择和滚动是工具视图状态;展示一条请求不会重发该请求。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(
|
||||
p: &mut Painter,
|
||||
state: &mut DeveloperToolsState,
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
//! 开发工具画面的绘制辅助:文字行、按钮、分隔线和滚动条。
|
||||
//! 绘制命令与可交互区域同步登记,保证屏幕上的管理控件能用同一几何命中。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 开发工具的绘制助手,把布局结果写入同帧显示列表。
|
||||
pub(super) struct Painter {
|
||||
pub(super) ui: Ui<Action>,
|
||||
pub(super) layout: DeveloperToolsLayout,
|
||||
}
|
||||
impl Painter {
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn fill(&mut self, r: Rect, c: Color) {
|
||||
self.ui.fill(r, c);
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn label(&mut self, x: f32, y: f32, value: &str, c: Color, code: bool) {
|
||||
self.ui.label(x, y, value, c, code);
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn button(&mut self, r: Rect, label: &str, action: Action, active: bool) {
|
||||
let role = match action {
|
||||
Action::Follow | Action::Snapshot | Action::Cloud => Role::Toggle,
|
||||
@@ -19,14 +26,17 @@ impl Painter {
|
||||
self.ui
|
||||
.button(r, action, Button::new(label).selected(active).role(role));
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn enabled_button(&mut self, r: Rect, label: &str, action: Action, enabled: bool) {
|
||||
self.ui
|
||||
.button(r, action, Button::new(label).enabled(enabled));
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn split(&mut self, r: Rect, kind: Split) {
|
||||
// Paint and register splitters after all pane content, in one overlay pass.
|
||||
self.layout.splits.push((r, kind));
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn lines(
|
||||
&mut self,
|
||||
area: Rect,
|
||||
@@ -132,6 +142,7 @@ impl Painter {
|
||||
self.scrollbar(area, region, false, s.y, content_h);
|
||||
self.scrollbar(area, region, true, s.x, content_w);
|
||||
}
|
||||
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
||||
pub(super) fn scrollbar(
|
||||
&mut self,
|
||||
r: Rect,
|
||||
@@ -170,6 +181,7 @@ impl Painter {
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
/// 一行可绘制、可关联源码位置的工具文本。
|
||||
pub(super) struct Line {
|
||||
pub(super) value: String,
|
||||
pub(super) color: Color,
|
||||
@@ -178,11 +190,13 @@ pub(super) struct Line {
|
||||
pub(super) action: Option<Action>,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
/// 管理画面内分隔条方向/位置描述。
|
||||
pub(super) enum Divider {
|
||||
Accent,
|
||||
Neutral,
|
||||
}
|
||||
impl Line {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub(super) fn new(value: impl Into<String>, color: Color) -> Self {
|
||||
Self {
|
||||
value: value.into(),
|
||||
@@ -193,6 +207,7 @@ impl Line {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// 按行绘制工具文本,返回后续布局需要的占用范围。
|
||||
pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
|
||||
out.extend(
|
||||
value
|
||||
@@ -201,6 +216,7 @@ pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
|
||||
.map(|s| Line::new(s, color)),
|
||||
);
|
||||
}
|
||||
/// 把一条 Console 结果投影为可滚动、可选择的文字画面。
|
||||
pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
|
||||
if let Ok(value) = serde_json::from_str::<serde_json::Value>(body) {
|
||||
if value["execution"] == "repl" {
|
||||
@@ -263,6 +279,7 @@ pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
|
||||
}
|
||||
multiline(out, body, color);
|
||||
}
|
||||
/// 将代码层次展开为工具可绘制的行数据,保留节点与源范围关联。
|
||||
pub(super) fn flatten<'a>(node: &'a CodeNode, depth: usize, out: &mut Vec<(usize, &'a CodeNode)>) {
|
||||
out.push((depth, node));
|
||||
for child in &node.children {
|
||||
@@ -270,6 +287,7 @@ pub(super) fn flatten<'a>(node: &'a CodeNode, depth: usize, out: &mut Vec<(usize
|
||||
}
|
||||
}
|
||||
|
||||
/// 绘制选中请求的头部、正文预览与响应信息,不重新执行该请求。
|
||||
pub(super) fn request_details(out: &mut Vec<Line>, r: &Exchange) {
|
||||
multiline(
|
||||
out,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 多行文本选择和光标导航。选择端点映射到合法字符边界,避免把 UTF-8 字节中间当成一个字符。
|
||||
//! 工具只保留当前文本行和选择范围;复制操作取得的是所选内容,不是整份运行时状态。
|
||||
//!
|
||||
//! Read-only, grapheme-aware source selection, independent of console editing.
|
||||
use crate::{
|
||||
geometry::Point,
|
||||
@@ -6,6 +9,7 @@ use crate::{
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// 行文本、锚点与选择端点,既供鼠标拖选也供键盘导航使用。
|
||||
pub(super) struct SourceSelection {
|
||||
lines: Vec<String>,
|
||||
anchor: (usize, usize),
|
||||
@@ -13,6 +17,7 @@ pub(super) struct SourceSelection {
|
||||
pub dragging: bool,
|
||||
}
|
||||
impl SourceSelection {
|
||||
/// 替换可选择文本行,并把旧选择限制在新文本仍有效的位置。
|
||||
pub fn set_lines(&mut self, lines: &[String]) {
|
||||
if self.lines != lines {
|
||||
self.lines = lines.to_vec();
|
||||
@@ -21,6 +26,7 @@ impl SourceSelection {
|
||||
self.dragging = false;
|
||||
}
|
||||
}
|
||||
/// 把工具指针位置换算成合法行号与文本字符边界。
|
||||
fn locate(p: Point, origin: Point, lines: &[String]) -> (usize, usize) {
|
||||
let row = (((p.y - origin.y) / super::ROW).floor().max(0.0) as usize)
|
||||
.min(lines.len().saturating_sub(1));
|
||||
@@ -39,14 +45,17 @@ impl SourceSelection {
|
||||
}
|
||||
(row, byte)
|
||||
}
|
||||
/// 在指针位置建立文本选择锚点,开始一次拖动选择。
|
||||
pub fn begin(&mut self, p: Point, origin: Point, lines: &[String]) {
|
||||
self.anchor = Self::locate(p, origin, lines);
|
||||
self.end = self.anchor;
|
||||
self.dragging = true;
|
||||
}
|
||||
/// 保持原锚点,随指针更新选择另一端。
|
||||
pub fn extend(&mut self, p: Point, origin: Point, lines: &[String]) {
|
||||
self.end = Self::locate(p, origin, lines);
|
||||
}
|
||||
/// 用键盘移动文本选择端点,按扩展模式决定是否保留锚点。
|
||||
pub fn navigate(
|
||||
&mut self,
|
||||
key: &winit::keyboard::NamedKey,
|
||||
@@ -109,6 +118,7 @@ impl SourceSelection {
|
||||
}
|
||||
Some(row)
|
||||
}
|
||||
/// 返回排序后的有效选择范围,隐藏拖动方向差异。
|
||||
fn range(&self, all: bool) -> ((usize, usize), (usize, usize)) {
|
||||
if all {
|
||||
return (
|
||||
@@ -121,6 +131,7 @@ impl SourceSelection {
|
||||
}
|
||||
(self.anchor.min(self.end), self.anchor.max(self.end))
|
||||
}
|
||||
/// 拼接当前选择范围内的实际文本,保持多行换行与 UTF-8 边界。
|
||||
pub fn text(&self, all: bool) -> String {
|
||||
let (a, b) = self.range(all);
|
||||
self.lines
|
||||
@@ -135,6 +146,7 @@ impl SourceSelection {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
/// 取得某行的可见选择片段,供高亮绘制复用。
|
||||
pub fn span(&self, row: usize, all: bool, lines: &[String]) -> Option<(f32, f32)> {
|
||||
let (a, b) = self.range(all);
|
||||
if row < a.0 || row > b.0 || a == b {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制宿主设置页面和主题选择区域。
|
||||
//! 这里只投影当前设置和交互目标,平台原生窗口行为由对应系统适配层负责。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(p: &mut Painter, state: &mut DeveloperToolsState, viewport: Rect) {
|
||||
let x = viewport.x;
|
||||
let w = viewport.width;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! 绘制 AST、当前源码快照和云文件视图。
|
||||
//! 当前快照是发布源的只读投影;云文件 provider 的可用性与打开状态分别反馈,不把查看代码当成执行代码。
|
||||
//!
|
||||
use super::*;
|
||||
|
||||
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
|
||||
pub(super) fn build(
|
||||
p: &mut Painter,
|
||||
state: &mut DeveloperToolsState,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 原生宿主模块入口。窗口、管理控件、系统适配和开发工具状态与用户业务界面分开维护。
|
||||
//!
|
||||
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
|
||||
pub(crate) mod activity;
|
||||
pub(crate) mod app;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! macOS 原生标题栏适配。定位系统窗口按钮并沿用用户的标题栏双击设置。
|
||||
//! 仅在 macOS 编译;Objective-C 交互局限在原生窗口层,不参与 Endor 布局与事件分发。
|
||||
//!
|
||||
//! Keep AppKit's real window buttons aligned with the custom tab strip.
|
||||
use objc2_app_kit::{NSApplication, NSView, NSWindowButton, NSWindowStyleMask};
|
||||
use objc2_foundation::{MainThreadMarker, NSPoint, NSString, NSUserDefaults};
|
||||
@@ -8,6 +11,7 @@ use winit::{
|
||||
|
||||
/// The custom draggable row does not receive AppKit's titlebar double-click
|
||||
/// action automatically. Use the native click count and native window actions.
|
||||
/// 读取系统偏好的标题栏双击行为,委托原生窗口执行相应动作。
|
||||
pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
let Some(main_thread) = MainThreadMarker::new() else {
|
||||
return false;
|
||||
@@ -49,6 +53,7 @@ pub fn titlebar_double_click(window: &Window) -> bool {
|
||||
|
||||
/// Called only on winit's main thread, after native creation and before drawing.
|
||||
/// AppKit can restore its default button positions when laying out the title bar.
|
||||
/// 调整真实 macOS 窗口三键的位置,与全尺寸内容和侧栏控制区对齐。
|
||||
pub fn align_controls(window: &Window) {
|
||||
let Ok(handle) = window.window_handle() else {
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 宿主管理工具和页面列表的唯一侧栏。展开时占据页面左侧空间,折叠后只保留展开按钮区域。
|
||||
//! 页面标题和身份来自发布帧;实际切换/关闭通过带版本的事件进入运行时,不在绘制时修改业务对象。
|
||||
//!
|
||||
//! The only persistent host toolbar. A collapsed sidebar owns just its toggle
|
||||
//! and the native traffic-light safe area; the page owns the rest of the window.
|
||||
use crate::{
|
||||
@@ -15,6 +18,7 @@ pub const HEADER: f32 = 40.0;
|
||||
pub const CONTROLS: f32 = if cfg!(target_os = "macos") { 86.0 } else { 0.0 };
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 侧栏释放激活后要执行的管理动作,绘制本身只登记它。
|
||||
pub enum Action {
|
||||
Toggle,
|
||||
NewPage,
|
||||
@@ -23,6 +27,7 @@ pub enum Action {
|
||||
Close(u64),
|
||||
}
|
||||
|
||||
/// 侧栏开合、控件交互和本帧目标集合,决定实际占用页面宽度。
|
||||
pub struct State {
|
||||
pub open: bool,
|
||||
pub interaction: Interaction,
|
||||
@@ -31,6 +36,7 @@ pub struct State {
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: true,
|
||||
@@ -42,10 +48,12 @@ impl Default for State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// 返回展开侧栏占据的逻辑宽度,折叠时页面收回这部分空间。
|
||||
pub fn left(&self) -> f32 {
|
||||
if self.open { WIDTH } else { 0.0 }
|
||||
}
|
||||
|
||||
/// 判断指针是否落入宿主侧栏/展开按钮区域,保护页面输入边界。
|
||||
pub fn covers(&self, point: Point) -> bool {
|
||||
if self.open {
|
||||
point.x >= 0.0 && point.x < WIDTH
|
||||
@@ -54,6 +62,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// 绘制工具入口、页面列表和展开控制,同时登记稳定的交互目标。
|
||||
pub fn draw(
|
||||
&mut self,
|
||||
tabs: &[Tab],
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
//! 云文件浏览的 provider 抽象与工作区状态。默认 provider 未连接,不假装已有外部文件。
|
||||
//! 列出和打开文件的结果作为工具数据展示,错误保持在当前工具状态中供界面说明。
|
||||
//!
|
||||
//! Cloud source integration seam. The host installs a provider when one exists;
|
||||
//! the default workspace stays disconnected and never fabricates or stores files.
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// provider 提供的一条文件元数据,供云文件列表显示。
|
||||
pub struct SourceFile {
|
||||
pub id: String,
|
||||
pub path: String,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
/// 已经取得的源文件内容与标识,不代表它已经被执行。
|
||||
pub struct OpenedSource {
|
||||
pub path: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
/// 外部文件能力接口;只负责列出和读取,不与 VM 全局环境耦合。
|
||||
pub trait CloudSourceProvider: std::fmt::Debug + Send {
|
||||
/// 通过当前 provider 请求文件列表;默认未连接实现返回明确不可用状态。
|
||||
fn list_files(&mut self) -> Result<Vec<SourceFile>>;
|
||||
/// 通过 provider 取得指定文件内容,不在工具查看动作中执行该内容。
|
||||
fn open_file(&mut self, id: &str) -> Result<OpenedSource>;
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
/// Sources 云文件页面保留的 provider、列表、选择、内容与错误状态。
|
||||
pub struct SourceWorkspace {
|
||||
pub provider: Option<Box<dyn CloudSourceProvider>>,
|
||||
pub files: Vec<SourceFile>,
|
||||
@@ -25,6 +34,7 @@ pub struct SourceWorkspace {
|
||||
pub error: String,
|
||||
}
|
||||
impl SourceWorkspace {
|
||||
/// 更新云文件列表并保存结果或错误,供 Sources 页面显示。
|
||||
pub fn refresh(&mut self) {
|
||||
let Some(provider) = &mut self.provider else {
|
||||
self.error = "No cloud file provider configured".into();
|
||||
@@ -38,6 +48,7 @@ impl SourceWorkspace {
|
||||
Err(error) => self.error = error.to_string(),
|
||||
}
|
||||
}
|
||||
/// 更新选中文件的内容与打开状态,失败保持可读错误信息。
|
||||
pub fn open(&mut self, id: &str) {
|
||||
let Some(provider) = &mut self.provider else {
|
||||
return;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
//! 即时绘制控件的跨帧交互状态:稳定 WidgetId、悬停、按下捕获、键盘焦点和释放激活。
|
||||
//! 控件用语义 key 标识,而不是屏幕位置或行号。释放时必须仍命中同一个启用目标,拖出控件不能意外触发点击。
|
||||
//!
|
||||
use crate::geometry::{Point, Rect};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Stable across frames, resizing and text changes. Use semantic keys, never row indices.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
/// 由语义 key 生成的稳定控件身份,跨帧移动和文字变化不应改变它。
|
||||
pub struct WidgetId(u64);
|
||||
impl WidgetId {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(key: impl Hash) -> Self {
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
key.hash(&mut hash);
|
||||
@@ -12,11 +17,13 @@ impl WidgetId {
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 分隔条和滚动条的操作方向。
|
||||
pub enum Axis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 管理控件角色,决定焦点资格和系统光标。
|
||||
pub enum Role {
|
||||
Button,
|
||||
Tab,
|
||||
@@ -28,11 +35,13 @@ pub enum Role {
|
||||
Scrollbar(Axis),
|
||||
}
|
||||
impl Role {
|
||||
/// 判断控件角色是否可以接受键盘焦点。
|
||||
fn focusable(self) -> bool {
|
||||
!matches!(self, Self::Splitter(_) | Self::Scrollbar(_) | Self::Row)
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
/// 宿主控件请求的系统光标种类。
|
||||
pub enum Cursor {
|
||||
Default,
|
||||
Pointer,
|
||||
@@ -43,6 +52,7 @@ pub enum Cursor {
|
||||
Disabled,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
/// 本帧可命中的管理控件,绑定几何、角色、启用条件与可选 action。
|
||||
pub struct Target<A> {
|
||||
pub id: WidgetId,
|
||||
pub rect: Rect,
|
||||
@@ -51,6 +61,7 @@ pub struct Target<A> {
|
||||
pub action: Option<A>,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 绘制使用的组合交互状态,避免按钮自己重新推断焦点/捕获。
|
||||
pub struct VisualState {
|
||||
pub hovered: bool,
|
||||
pub pressed: bool,
|
||||
@@ -62,6 +73,7 @@ pub struct VisualState {
|
||||
/// Retained input state for an immediate-mode frame. Actions fire on release inside
|
||||
/// the same enabled control, so dragging away cancels a click.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
/// 即时绘制之外保留的输入状态,确保按下与释放跨帧仍匹配同一控件。
|
||||
pub struct Interaction {
|
||||
pointer: Option<Point>,
|
||||
hovered: Option<WidgetId>,
|
||||
@@ -71,18 +83,22 @@ pub struct Interaction {
|
||||
keyboard_focus: bool,
|
||||
}
|
||||
impl Interaction {
|
||||
/// 按逆绘制顺序选择指针处的管理控件,避免重叠控件同时激活。
|
||||
pub fn hit<'a, A>(&self, targets: &'a [Target<A>]) -> Option<&'a Target<A>> {
|
||||
let p = self.pointer?;
|
||||
targets.iter().rev().find(|t| t.rect.contains(p))
|
||||
}
|
||||
/// 更新指针与悬停目标,但不在移动时直接触发点击。
|
||||
pub fn pointer_moved<A>(&mut self, p: Point, targets: &[Target<A>]) {
|
||||
self.pointer = Some(p);
|
||||
self.hovered = self.hit(targets).map(|t| t.id);
|
||||
}
|
||||
/// 清除离开窗口后的指针/悬停状态。
|
||||
pub fn pointer_left(&mut self) {
|
||||
self.pointer = None;
|
||||
self.hovered = None;
|
||||
}
|
||||
/// 捕获启用控件的稳定身份,并按角色更新键盘焦点。
|
||||
pub fn press<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<Target<A>> {
|
||||
self.key_active = None;
|
||||
self.keyboard_focus = false;
|
||||
@@ -91,22 +107,26 @@ impl Interaction {
|
||||
self.focused = target.as_ref().filter(|t| t.role.focusable()).map(|t| t.id);
|
||||
target
|
||||
}
|
||||
/// 只有释放时仍处于原启用目标内才返回 action,拖出后释放不触发。
|
||||
pub fn release<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<A> {
|
||||
let active = self.pointer_active.take()?;
|
||||
self.hit(targets)
|
||||
.filter(|t| t.enabled && t.id == active && t.role != Role::TextField)
|
||||
.and_then(|t| t.action.clone())
|
||||
}
|
||||
/// 撤销当前操作的后续交付资格,并执行对应取消清理。
|
||||
pub fn cancel(&mut self) {
|
||||
self.pointer_active = None;
|
||||
self.key_active = None;
|
||||
}
|
||||
/// 撤销失焦后的键盘/指针激活状态,防止回到窗口时补发旧操作。
|
||||
pub fn blur(&mut self) {
|
||||
self.cancel();
|
||||
self.focused = None;
|
||||
self.keyboard_focus = false;
|
||||
self.pointer_left();
|
||||
}
|
||||
/// 用本帧目标集合校正跨帧焦点和捕获,已消失控件不保留可激活身份。
|
||||
pub fn reconcile<A>(&mut self, targets: &[Target<A>]) -> bool {
|
||||
let before = (
|
||||
self.hovered,
|
||||
@@ -127,11 +147,13 @@ impl Interaction {
|
||||
self.key_active,
|
||||
)
|
||||
}
|
||||
/// 返回当前仍持有焦点的稳定控件身份。
|
||||
pub fn focused<'a, A>(&self, targets: &'a [Target<A>]) -> Option<&'a Target<A>> {
|
||||
targets
|
||||
.iter()
|
||||
.find(|t| Some(t.id) == self.focused && t.enabled)
|
||||
}
|
||||
/// 沿启用且可聚焦的目标循环导航,支持正向与反向遍历。
|
||||
pub fn focus_next<A: Clone>(&mut self, targets: &[Target<A>], back: bool) -> Option<Target<A>> {
|
||||
self.cancel();
|
||||
self.keyboard_focus = true;
|
||||
@@ -154,18 +176,21 @@ impl Interaction {
|
||||
self.focused = Some(target.id);
|
||||
Some(target.clone())
|
||||
}
|
||||
/// 为当前焦点控件记录键盘激活,后续释放仍须匹配目标。
|
||||
pub fn key_press<A>(&mut self, targets: &[Target<A>]) {
|
||||
self.key_active = self
|
||||
.focused(targets)
|
||||
.filter(|t| t.role != Role::TextField)
|
||||
.map(|t| t.id);
|
||||
}
|
||||
/// 结束键盘激活并按有效控件身份返回操作。
|
||||
pub fn key_release<A: Clone>(&mut self, targets: &[Target<A>]) -> Option<A> {
|
||||
let active = self.key_active.take()?;
|
||||
self.focused(targets)
|
||||
.filter(|t| t.id == active)
|
||||
.and_then(|t| t.action.clone())
|
||||
}
|
||||
/// 组合悬停、按下、焦点、选中和启用状态,供控件选择正确样式。
|
||||
pub fn visual(&self, id: WidgetId, selected: bool, enabled: bool) -> VisualState {
|
||||
let hovered = enabled && self.hovered == Some(id);
|
||||
VisualState {
|
||||
@@ -177,9 +202,11 @@ impl Interaction {
|
||||
enabled,
|
||||
}
|
||||
}
|
||||
/// 返回本次拖动/按压捕获的控件,移动过程中不随悬停目标跳变。
|
||||
pub fn captured(&self, id: WidgetId) -> bool {
|
||||
self.pointer_active == Some(id)
|
||||
}
|
||||
/// 按命中角色选择系统光标,例如文本、分隔条或可点击按钮。
|
||||
pub fn cursor<A>(&self, targets: &[Target<A>]) -> Cursor {
|
||||
let target = self
|
||||
.pointer_active
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 宿主管理控件的共享组件入口。主题、绘制原语和跨帧交互状态分离,用户 Endor 页面不依赖这里的控件树。
|
||||
//!
|
||||
//! Tessera's host management widgets. No Endor AST, VM, UiNode or ui_bridge dependency.
|
||||
//! Shares only low-level drawing, font shaping and geometry with guest content.
|
||||
mod interaction;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! 宿主控件的颜色与状态调色。根据背景、强调色和交互状态生成可读颜色,不改变业务状态。
|
||||
//!
|
||||
use crate::display_list::Color;
|
||||
pub const BG: Color = Color::rgb(255, 255, 255);
|
||||
// A visibly separate chrome layer against the warm, near-white content canvas.
|
||||
@@ -16,25 +18,31 @@ pub const PALETTE: [Color; 6] = [
|
||||
Color::rgb(52, 123, 78),
|
||||
];
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// 当前管理界面的配色基础,派生状态颜色由方法统一计算。
|
||||
pub struct Theme {
|
||||
pub accent: Color,
|
||||
}
|
||||
impl Theme {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(accent: Color) -> Self {
|
||||
Self {
|
||||
accent: readable_accent(accent),
|
||||
}
|
||||
}
|
||||
/// 从当前主题生成对应交互状态颜色,保持控件在状态变化时的视觉一致性。
|
||||
pub fn soft(self) -> Color {
|
||||
mix(BG, self.accent, 0.10)
|
||||
}
|
||||
/// 从当前主题生成对应交互状态颜色,保持控件在状态变化时的视觉一致性。
|
||||
pub fn hover(self) -> Color {
|
||||
mix(BG, self.accent, 0.17)
|
||||
}
|
||||
/// 从当前主题生成对应交互状态颜色,保持控件在状态变化时的视觉一致性。
|
||||
pub fn pressed(self) -> Color {
|
||||
mix(BG, self.accent, 0.29)
|
||||
}
|
||||
}
|
||||
/// 按比例混合颜色通道,作为状态颜色的基础运算。
|
||||
pub fn mix(a: Color, b: Color, amount: f32) -> Color {
|
||||
let channel =
|
||||
|a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * amount.clamp(0.0, 1.0)).round() as u8;
|
||||
@@ -44,6 +52,7 @@ pub fn mix(a: Color, b: Color, amount: f32) -> Color {
|
||||
channel(a.blue, b.blue),
|
||||
)
|
||||
}
|
||||
/// 根据背景选择可读的强调色,供选中与焦点状态使用。
|
||||
pub fn readable_accent(mut c: Color) -> Color {
|
||||
while (c.red as u32 * 299 + c.green as u32 * 587 + c.blue as u32 * 114) / 1000 > 155 {
|
||||
c = Color::rgb(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! 用统一主题绘制宿主管理控件,并同步注册交互目标。
|
||||
//! 每个控件的图形、角色、启用状态和 action 一起登记,绘制几何与命中几何保持一致。
|
||||
//!
|
||||
use super::{
|
||||
Axis, Interaction, Role, Target, VisualState, WidgetId,
|
||||
theme::{self, Theme},
|
||||
@@ -9,6 +12,7 @@ use crate::{
|
||||
};
|
||||
use std::hash::Hash;
|
||||
|
||||
/// 一次按钮绘制的借用描述,包括显示、角色和启用/选中状态。
|
||||
pub struct Button<'a> {
|
||||
pub label: &'a str,
|
||||
pub selected: bool,
|
||||
@@ -16,6 +20,7 @@ pub struct Button<'a> {
|
||||
pub role: Role,
|
||||
}
|
||||
impl<'a> Button<'a> {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(label: &'a str) -> Self {
|
||||
Self {
|
||||
label,
|
||||
@@ -24,20 +29,24 @@ impl<'a> Button<'a> {
|
||||
role: Role::Button,
|
||||
}
|
||||
}
|
||||
/// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。
|
||||
pub fn selected(mut self, value: bool) -> Self {
|
||||
self.selected = value;
|
||||
self
|
||||
}
|
||||
/// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。
|
||||
pub fn enabled(mut self, value: bool) -> Self {
|
||||
self.enabled = value;
|
||||
self
|
||||
}
|
||||
/// 设置当前控件描述的选择、启用或角色信息,供后续绘制和目标登记使用。
|
||||
pub fn role(mut self, value: Role) -> Self {
|
||||
self.role = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// 本帧宿主控件绘制上下文,共同拥有显示列表与交互目标登记入口。
|
||||
pub struct Ui<A> {
|
||||
pub list: DisplayList,
|
||||
pub targets: Vec<Target<A>>,
|
||||
@@ -45,6 +54,7 @@ pub struct Ui<A> {
|
||||
interaction: Interaction,
|
||||
}
|
||||
impl<A: Hash> Ui<A> {
|
||||
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
||||
pub fn new(theme: Theme, interaction: Interaction) -> Self {
|
||||
Self {
|
||||
list: DisplayList::default(),
|
||||
@@ -53,6 +63,7 @@ impl<A: Hash> Ui<A> {
|
||||
interaction,
|
||||
}
|
||||
}
|
||||
/// 使用当前主题绘制控件填充、轮廓或文字原语。
|
||||
pub fn fill(&mut self, r: Rect, color: Color) {
|
||||
self.list.push(DrawCommand::FillRoundedRect {
|
||||
rect: r,
|
||||
@@ -60,6 +71,7 @@ impl<A: Hash> Ui<A> {
|
||||
color,
|
||||
});
|
||||
}
|
||||
/// 使用当前主题绘制控件填充、轮廓或文字原语。
|
||||
fn outline(&mut self, r: Rect, color: Color, width: f32) {
|
||||
self.list.push(DrawCommand::StrokeRoundedRect {
|
||||
rect: Rect::new(
|
||||
@@ -73,6 +85,7 @@ impl<A: Hash> Ui<A> {
|
||||
color,
|
||||
});
|
||||
}
|
||||
/// 使用当前主题绘制控件填充、轮廓或文字原语。
|
||||
pub fn label(&mut self, x: f32, y: f32, value: &str, color: Color, code: bool) {
|
||||
self.list.push(DrawCommand::Text {
|
||||
origin: Point::new(x, y),
|
||||
@@ -89,6 +102,7 @@ impl<A: Hash> Ui<A> {
|
||||
color,
|
||||
});
|
||||
}
|
||||
/// 把控件身份、几何、角色、启用状态和 action 一起加入本帧交互目标。
|
||||
fn register(
|
||||
&mut self,
|
||||
id: WidgetId,
|
||||
@@ -108,6 +122,7 @@ impl<A: Hash> Ui<A> {
|
||||
});
|
||||
response
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
fn surface(&mut self, rect: Rect, s: VisualState, base: Option<Color>) {
|
||||
let color = if !s.enabled {
|
||||
base
|
||||
@@ -127,6 +142,7 @@ impl<A: Hash> Ui<A> {
|
||||
self.outline(rect, self.theme.accent, 1.5);
|
||||
}
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn button(&mut self, rect: Rect, action: A, button: Button<'_>) -> VisualState {
|
||||
let id = WidgetId::new(&action);
|
||||
let s = self.register(
|
||||
@@ -168,6 +184,7 @@ impl<A: Hash> Ui<A> {
|
||||
self.list.push(DrawCommand::PopClip);
|
||||
s
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn text_field(&mut self, rect: Rect, action: A, focused: bool) {
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -191,6 +208,7 @@ impl<A: Hash> Ui<A> {
|
||||
);
|
||||
}
|
||||
/// A terminal prompt shares its surface with output; its caret is the focus cue.
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn inline_input(&mut self, rect: Rect, action: A) {
|
||||
self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -201,6 +219,7 @@ impl<A: Hash> Ui<A> {
|
||||
true,
|
||||
);
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn row(&mut self, paint: Rect, hit: Rect, action: A, selected: bool) {
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -212,6 +231,7 @@ impl<A: Hash> Ui<A> {
|
||||
);
|
||||
self.surface(paint, s, None);
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn swatch(&mut self, rect: Rect, action: A, color: Color, selected: bool) {
|
||||
let s = self.register(
|
||||
WidgetId::new(&action),
|
||||
@@ -249,6 +269,7 @@ impl<A: Hash> Ui<A> {
|
||||
self.label(rect.x + 10.0, rect.y + 6.0, "✓", theme::BG, false);
|
||||
}
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn splitter(&mut self, rect: Rect, id: WidgetId, axis: Axis) {
|
||||
let hit = Rect::new(
|
||||
rect.x - 3.0,
|
||||
@@ -273,6 +294,7 @@ impl<A: Hash> Ui<A> {
|
||||
};
|
||||
self.fill(line, color);
|
||||
}
|
||||
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
|
||||
pub fn scrollbar(&mut self, rect: Rect, id: WidgetId, axis: Axis) {
|
||||
let mut s = self.register(id, rect, Role::Scrollbar(axis), None, false, true);
|
||||
s.pressed |= self.interaction.captured(id);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! 发布页面标签的数据结构。保留稳定页面身份、标题、选中状态和开发工具类别;页面的托管所有权由模型层管理。
|
||||
//!
|
||||
//! Immutable page registry projections. Host navigation is rendered in sidebar.
|
||||
//! IDs are session-local data and never own managed VM objects.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// 托管页面注册表的不可变投影,原生工具只使用稳定 ID 与显示元数据。
|
||||
pub struct Tab {
|
||||
pub id: u64,
|
||||
pub title: String,
|
||||
|
||||
Reference in New Issue
Block a user