1 Commits

79 changed files with 159 additions and 1419 deletions
-116
View File
@@ -1,116 +0,0 @@
# 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 中记录。
+57
View File
@@ -0,0 +1,57 @@
# 场景交互性能研究
## 结论
性能问题有可验证的解决路径,但单改 Aster 字典不能让当前世界 UI 达到稳定 60 FPS。本次保留去除字符串键比较临时分配的小改动,交付可重复基准;面板纹理缓存实验因当前示例收益不足而撤回。没有实现 GPU 后端或宣称完成性能目标。
## 方法
在 Apple M4 Pro、macOS 15.6 本机使用 release 构建,固定 1024×800 逻辑视口、1× / 2× 像素比例,分别加载 world-ui 和 scene3d 示例。每组执行 33 次真实 scene-orbit 运行时分发,前 3 次作为预热,统计后 30 次的均值、P50、P95。每次先测分发到文档更新,再测变化后的软件绘制,最后测相同显示列表的缓存绘制。没有运行 Console 编译来代替原生事件。
```sh
cargo test --release --locked scene_interaction_performance -- --ignored --nocapture --test-threads=1
```
这是无窗口的运行时与软件渲染基准,包含回调、快照、布局和运行时收尾,但不包含本地 HTTP、OS 事件排队、窗口呈现或输入到显示延迟。阶段耗时不能简单当作实测 FPS。时间是本机样本,不是跨设备保证;基准默认 ignored,普通回归不以绝对耗时断言通过。
## 对照结果
世界 UI、2× 像素比例,单位 ms:
| 条件 | 更新均值 | 更新 P95 | 变化绘制均值 | 变化绘制 P95 |
| --- | ---: | ---: | ---: | ---: |
| 原始 Aster | 8.456 | 8.843 | 18.704 | 20.742 |
| 借用字符串键比较 | 6.842 | 7.226 | 18.598 | 20.665 |
| 再加面板纹理缓存实验 | 6.823 | 7.053 | 18.410 | 20.482 |
去掉字符串键比较的分配后,本组更新均值减少约 19%,绘制基本不变。面板缓存的绘制收益约 1%,不足以证明值得在本次增加缓存与失效复杂度,因此撤回。
作为对照,普通 scene3d 示例在 2× 下更新约 0.94 ms、变化绘制约 6.15 ms;它的视口尺寸、覆盖像素和 UI 复杂度不同,不能据此把两个示例的全部差值都归给某一层。
## Aster 发现与保留的修改
`EndorDictObject` 用插入有序的数组存储,`dict_position` 逐项调用 `key_equal`。原实现每次比较都会把内联文本或托管 String 转成新的 String;查找名称时会重复分配和复制候选键。
本次只改为借用字符串切片比较。保留线性查找、原 key_equal 分支、对象检查、只读依赖观察、键的插入身份与顺序、写入验证和引用计数流程。新增测试覆盖空文本、普通文本、Unicode、托管 / 内联字符串等价替换、删除和引用释放。
没有把通用字典直接改成 HashMap,也没有绕过 heap.replace_payload 的事务性边验证。前者还需处理托管键相等 / 哈希与顺序语义;后者涉及 GC、失败原子性和只读保护。它们不属于本次已经证明安全的改动。
## 尚未解决的主要成本
1. 世界 UI 的摄像机变化后,当前统计仍为 measured=8、arranged=88;普通 3D 为 measured=0、arranged=14。数据证明相机更新没有彻底绕开排版。
2. 3D 变化帧仍在窗口线程逐像素进行 CPU 绘制。当前世界 UI 的绘制本身已超过 60 Hz 的 16.7 ms 预算,其他 CPU 优化不能消除这个事实。
3. 原生连续输入仍逐项经本地 HTTP 发送,有界队列不等于按显示帧合并;真实排队和呈现延迟需要另外测量。
## 后续实施顺序与条件
1. **相机更新专用路径。** 复用已有 Mount 几何、UI 排版和绘制结果,仅更新视图 / 投影、可见性和命中映射。必须检查源 measure / arrange / paint 的实际读取依赖;源函数若读取相机,应继续正确失效。不能无条件跳过所有布局或绘制。验收:不依赖相机的 UI 在旋转时零测量、零重新排版,依赖相机的源协议回归仍通过。
2. **按帧收集连续输入。** 原生操作直接进入进程内通道,连续手势按捕获会话 / 目标与顺序合并;点击、页面切换和运行时替换保持边界。外部 HTTP API 保留严格版本校验。验收包括缩放锚点、非交换手势顺序、嵌套捕获与过期会话,不能只检查最终数值。
3. **GPU 合成与独立渲染。** 现有不可变网格、变换、相机、深度和 UV 数据可以成为渲染输入;VM 对象留在运行时线程。GPU 应直接向窗口表面呈现,同时合成 2D / 3D 内容,避免每帧 GPU→CPU 回读后再交给 softbuffer。跨线程队列只保留最新可呈现快照;纹理、缓冲与缓存均需资源预算和正确失效。
建议验收目标:固定示例 2× 下相机更新 P95 ≤2 ms、GPU 绘制 P95 ≤8 ms,另外记录真实窗口的帧间隔和输入到呈现延迟。目标尚未达到,也没有用无窗口基准宣称原生 60 FPS。
## 检查说明
Aster 全工作区测试发现 compiler 的 classes 测试中两项解析失败(第三行 expected EOF),以及 core 的 managed_coroutine_hard_budget_accumulates_and_error_releases_frames 指令预算失败。将对象实现恢复为未优化的基线后,这三项失败均能复现,说明不是本次借用字符串比较引入;不把全工作区标为全绿。对象层测试通过,Aster 全工作区 Clippy / 格式通过;GUI 为 186 项通过、1 项默认忽略的基准,GUI Clippy / 格式通过。该基准已经另行在 release 模式实际执行。
本次没有修改用户指向的 builtins.rs 第 176 行(该处是函数对象定义)。实际热路径修改位于 Aster 的 store_builtins.rs。
-4
View File
@@ -1,22 +1,18 @@
#!/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">
-5
View File
@@ -7,7 +7,6 @@ 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');
@@ -24,7 +23,6 @@ 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);
@@ -43,7 +41,6 @@ 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 = () => {
@@ -68,7 +65,6 @@ 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');
@@ -81,7 +77,6 @@ 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.
-2
View File
@@ -1,7 +1,5 @@
# Tessera 源码导航
本体代码已补充中文模块、类型、函数与关键流程注释;建议按[注释阅读索引和逐文件说明](../docs/code-comments.md)从启动、运行时发布和视觉协议依次阅读。
整体设计和当前进展从 [架构导览](../docs/architecture/README.md) 开始;本轮每个文件的迁移对应见 [逐文件清单](../docs/architecture/file-map.md)。
`main.rs` 只负责组合启动,并保留 crate 私有模块路径别名。五组目录仍在同一个 binary crate 中,不是严格单向分层。
-2
View File
@@ -1,4 +1,2 @@
//! 语言桥接模块入口。集中导出 Endor 类型和宿主实现之间的适配层。
//!
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
pub(crate) mod ui_bridge;
-85
View File
@@ -1,7 +1,3 @@
//! 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;
@@ -94,7 +90,6 @@ 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>,
@@ -114,7 +109,6 @@ struct Session {
}
impl Session {
/// 收集所有窗口的私有 desktop 身份,用于排除宿主记账读取造成的无关布局失效。
fn host_desktops(&self) -> std::collections::HashSet<ObjectRef> {
self.presentations
.values()
@@ -125,7 +119,6 @@ impl Session {
impl std::ops::Deref for Session {
type Target = crate::presentation::Presentation;
/// 将包装类型的只读访问转交给内部当前上下文。
fn deref(&self) -> &Self::Target {
self.presentations
.get(&self.current)
@@ -133,7 +126,6 @@ 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)
@@ -143,7 +135,6 @@ impl std::ops::DerefMut for Session {
type Shared = Arc<Mutex<Session>>;
#[derive(Debug, Clone, Copy)]
/// ui/组件宿主入口的操作枚举;统一由调用适配器分派,不直接代表已经发生的业务变更。
enum Operation {
IsInstance,
Implement,
@@ -211,7 +202,6 @@ enum Operation {
}
impl Operation {
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
fn arity(self) -> endor_core::CallArity {
use Operation::*;
let (minimum, maximum) = match self {
@@ -248,7 +238,6 @@ impl Operation {
}
#[derive(Debug)]
/// 将一个宿主 Operation 及会话环境封装成 VM 可调用对象。
struct Function {
signature: Option<CallSignature>,
operation: Operation,
@@ -260,7 +249,6 @@ 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,
@@ -268,7 +256,6 @@ struct TypeMethod {
session: Shared,
}
/// 判断方法是否仍为宿主提供的原生事件实现,区分它与用户源覆盖。
pub(crate) fn is_intrinsic_event(
store: &endor_core::ObjectStore,
function: ObjectRef,
@@ -287,11 +274,9 @@ 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(
@@ -300,7 +285,6 @@ 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)? {
@@ -320,7 +304,6 @@ 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>,
@@ -329,11 +312,9 @@ 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,
@@ -341,7 +322,6 @@ impl NativeCallable for NativeFactory {
))
}
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
let requested = object(args.first())?;
let fields = cx
@@ -387,25 +367,21 @@ 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
@@ -415,7 +391,6 @@ 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)))
@@ -424,7 +399,6 @@ 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"));
@@ -440,7 +414,6 @@ fn dimension(value: Option<&Value>) -> Result<f32, VmError> {
Ok(value as f32)
}
/// 为一个托管对象安装绑定到当前会话的原生方法,声明调用名与具体 Operation。
fn bind(
cx: &mut NativeContext<'_>,
owner: ObjectRef,
@@ -467,7 +440,6 @@ fn bind(
cx.set_attr(owner, name, Value::Object(function))
}
/// 使用默认原生类型创建 Block;需要源子类身份的构造走 new_typed_block。
fn new_block(
cx: &mut NativeContext<'_>,
data: Data,
@@ -476,7 +448,6 @@ fn new_block(
new_typed_block(cx, data, session, None)
}
/// 分配有名义类型和默认位置/缩放的独立相机,供新视口拥有自己的导航状态。
fn new_camera2d(
cx: &mut NativeContext<'_>,
session: &Shared,
@@ -502,7 +473,6 @@ fn new_camera2d(
Ok(camera)
}
/// 创建原生 Block 并附加请求的 Endor 类型;先验证继承关系,再登记到会话节点索引。
fn new_typed_block(
cx: &mut NativeContext<'_>,
data: Data,
@@ -558,7 +528,6 @@ fn new_typed_block(
Ok(block)
}
/// 在当前 VM 中建立窗口、desktop 和独立 Presentation,实际 OS 窗口等命令发布后再创建。
fn create_window(
cx: &mut NativeContext<'_>,
state: &mut Session,
@@ -588,7 +557,6 @@ fn create_window(
Ok(window)
}
/// 从页面登记信息取得所属窗口,保持 Page 的归属与原生资源身份一致。
fn page_window(
store: &endor_core::ObjectStore,
state: &Session,
@@ -602,7 +570,6 @@ fn page_window(
}
Ok(None)
}
/// 撤销页面在展示会话中的登记,配合模型层处理活动页面与生命周期。
fn remove_page(
cx: &mut NativeContext<'_>,
view: &mut crate::presentation::Presentation,
@@ -621,7 +588,6 @@ fn remove_page(
}
Ok(())
}
/// 在指定窗口安排页面展示,并维护目标 Presentation 的活动/待发布状态。
fn show_in(
cx: &mut NativeContext<'_>,
page: ObjectRef,
@@ -672,24 +638,20 @@ 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"));
@@ -714,7 +676,6 @@ impl NativeCallable for PresentResult {
}
}
/// 根据 Presentable 能力把任意源值转换成可展示内容,避免只允许某个原生 Block 类型。
fn present_value(
cx: &mut NativeContext<'_>,
value: &Value,
@@ -757,13 +718,11 @@ 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>,
@@ -775,20 +734,17 @@ 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,
@@ -814,7 +770,6 @@ fn invoke_member(
result
}
/// 将托管页面注册表投影成稳定标签 ID、标题与活动状态,不把托管引用交给原生侧栏。
fn project_tabs(
vm: &Vm,
state: &mut crate::presentation::Presentation,
@@ -861,7 +816,6 @@ 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();
@@ -931,20 +885,16 @@ 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
@@ -967,7 +917,6 @@ impl UiBridge {
});
Ok(ids)
}
/// 判断窗口是否仍登记且没有进入关闭状态。
pub fn accepts_window(&self, id: u64) -> bool {
self.session.lock().is_ok_and(|state| {
state
@@ -977,7 +926,6 @@ impl UiBridge {
})
}
/// 切换当前 Presentation 上下文;不会创建新窗口或复制 VM。
pub fn select_window(&self, id: u64) -> Result<()> {
let mut state = self
.session
@@ -989,7 +937,6 @@ 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");
@@ -1017,7 +964,6 @@ impl UiBridge {
Ok(())
}
/// 收尾已经关闭的展示资源,避免它们参与下一次窗口遍历。
pub fn finish_closed(&self, vm: &mut Vm) -> Result<()> {
let mut state = self
.session
@@ -1042,7 +988,6 @@ 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
@@ -1057,7 +1002,6 @@ 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()
@@ -1093,7 +1037,6 @@ 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.
@@ -1108,7 +1051,6 @@ impl UiBridge {
}
Ok(())
}
/// 检查当前会话是否已经安排或拥有可展示页面。
pub fn has_page(&self) -> Result<bool> {
Ok(self
.session
@@ -1118,7 +1060,6 @@ 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;
@@ -1156,14 +1097,12 @@ 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
@@ -1275,7 +1214,6 @@ impl UiBridge {
Ok(())
}
/// 在所属窗口的展示上下文中处理输入,区分页面管理、滚动、焦点、相机与普通事件。
pub fn dispatch(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
input
.validate()
@@ -1481,7 +1419,6 @@ 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
@@ -1501,7 +1438,6 @@ impl UiBridge {
}
Ok(())
}
/// 丢弃未成功发布的展示请求,恢复后续输入可以使用的会话状态。
pub fn discard_pending(&self, vm: &mut Vm) -> Result<()> {
let mut state = self
.session
@@ -1552,7 +1488,6 @@ impl UiBridge {
}
Ok(())
}
/// 重新评估已失效的属性绑定,并把结果计入会话统计。
pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> {
let (nodes, desktops) = {
let state = self
@@ -1569,7 +1504,6 @@ 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
@@ -1614,7 +1548,6 @@ impl UiBridge {
}
Ok(changed.then_some(root))
}
/// 提交已经完成的出现几何与快照,并处理展示生命周期;不把挂起或失败的布局当成已发布帧。
pub fn commit_frame(
&self,
vm: &mut Vm,
@@ -1670,7 +1603,6 @@ impl UiBridge {
result
}
/// 在已完成的几何上启动绘制协议,保留绘制任务所需的宿主上下文。
pub fn begin_paint_frame(
&self,
vm: &mut Vm,
@@ -1701,7 +1633,6 @@ impl UiBridge {
self.accept_paint_progress(progress)
}
/// 恢复同一次源绘制调用,把完成的 Draft 交回发布流程。
pub fn resume_paint_frame(
&self,
vm: &mut Vm,
@@ -1711,7 +1642,6 @@ impl UiBridge {
self.accept_paint_progress(progress)
}
/// 区分挂起与完成的绘制进度;完成时保存真实读取依赖并接回发布流程。
fn accept_paint_progress(
&self,
progress: crate::source_paint::Progress,
@@ -1736,7 +1666,6 @@ impl UiBridge {
Ok(progress)
}
/// 取消尚未完成的源绘制,释放其续体和未发布上下文。
pub fn cancel_paint_frame(
&self,
vm: &mut Vm,
@@ -1755,7 +1684,6 @@ impl UiBridge {
invalidated
}
/// 为目标快照建立 Mount、检查点和布局上下文,再启动可挂起的只读布局求解。
pub fn begin_layout_frame(
&self,
vm: &mut Vm,
@@ -1878,7 +1806,6 @@ impl UiBridge {
self.accept_layout_progress(vm, context, progress)
}
/// 记录本次刷新主动让出 worker 的次数,用于诊断分片执行。
pub fn note_refresh_yield(&self) -> Result<()> {
let mut state = self
.session
@@ -1888,7 +1815,6 @@ impl UiBridge {
Ok(())
}
/// 记录视口目标替换导致的刷新重启,便于区分正常挂起与取消重试。
pub fn note_refresh_restart(
&self,
cancelled_layout: bool,
@@ -1908,7 +1834,6 @@ impl UiBridge {
Ok(())
}
/// 撤销未完成布局所保留的窗口、挂载与生命周期上下文。
fn discard_layout_context(&self, vm: &mut Vm, context: LayoutContext) -> Result<()> {
let LayoutContext {
surface: _,
@@ -1941,7 +1866,6 @@ 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);
@@ -1950,12 +1874,10 @@ 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,
@@ -1973,7 +1895,6 @@ impl UiBridge {
self.accept_layout_progress(vm, context, progress)
}
/// 接收布局的 Pending/Complete 结果;保留上下文或执行最终投影和提交。
fn accept_layout_progress(
&self,
vm: &mut Vm,
@@ -1997,7 +1918,6 @@ impl UiBridge {
}
}
/// 依次投影滚动/视口、补页面元数据、保存读取版本、提交 Mount,并处理延后的生命周期调用。
fn finish_layout_frame(
&self,
vm: &mut Vm,
@@ -2098,7 +2018,6 @@ impl UiBridge {
Ok(frame)
}
/// 读取当前源程序已经选择的展示内容,为编译入口生成最终文档提供根。
pub fn rendered(&self, vm: &Vm) -> Result<Option<Arc<UiNode>>> {
let state = self
.session
@@ -2113,7 +2032,6 @@ impl UiBridge {
}
}
/// 从会话的类型注册表取得指定类型,再给原生分配的对象附加托管类型身份。
fn attach_named_type(
cx: &mut NativeContext<'_>,
object: ObjectRef,
@@ -2127,7 +2045,6 @@ fn attach_named_type(
attach_desktop_type(cx, object, name, desktop)
}
/// 按给定 desktop 查找类型并附加,支持不同窗口使用自己的展示上下文。
pub fn attach_desktop_type(
cx: &mut NativeContext<'_>,
instance: ObjectRef,
@@ -2138,7 +2055,6 @@ pub fn attach_desktop_type(
cx.attach_type(instance, definition)
}
/// 读取 desktop 的托管类型注册表;缺失或类型错误时返回 VM 错误。
pub(crate) fn desktop_type(
store: &endor_core::ObjectStore,
name: &str,
@@ -2148,7 +2064,6 @@ 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");
-4
View File
@@ -1,10 +1,6 @@
//! 安装一次 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);
-6
View File
@@ -1,20 +1,14 @@
//! 把 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();
-7
View File
@@ -1,20 +1,15 @@
//! 把内置开发工具和起始页接入托管 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"));
@@ -68,7 +63,6 @@ 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",
@@ -85,7 +79,6 @@ 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",
-6
View File
@@ -1,11 +1,7 @@
//! 把页面、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,
@@ -112,7 +108,6 @@ impl UiBridge {
result
}
/// 把具体 CanvasViewport 的位移或缩放锚点映射到其显式 Camera2D。
pub(super) fn dispatch_viewport_view(
&self,
vm: &mut Vm,
@@ -202,7 +197,6 @@ impl UiBridge {
result
}
/// 处理旧 Page 模式的兼容相机事件;独立视口使用各自的输入路径。
pub(super) fn dispatch_page_view(
&self,
vm: &mut Vm,
-5
View File
@@ -1,10 +1,6 @@
//! 声明宿主方法在 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,
@@ -90,7 +86,6 @@ pub(super) fn method_signature(
Ok(signature)
}
/// 将具体宿主 Operation 映射到参数/返回类型契约。
pub(super) fn operation_signature(
store: &endor_core::ObjectStore,
desktop: ObjectRef,
-12
View File
@@ -1,6 +1,4 @@
// 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 {
@@ -20,15 +18,12 @@ 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 = "窗口页面"
@@ -36,7 +31,6 @@ 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 = "纵向页面"
@@ -51,7 +45,6 @@ class StartActions {
page.crossAlign = "stretch"
page.render(view)
}
// Row 内可继续放 Column 卡片;横向滚动不要求改变 Page 的身份或生命周期。
function horizontal(self: StartActions, event: Event): Unit {
page = Page()
page.title = "横向页面"
@@ -66,11 +59,9 @@ 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("增加计数")
@@ -80,7 +71,6 @@ 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 = "自由画布"
@@ -96,7 +86,6 @@ 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)
@@ -120,7 +109,6 @@ function newTab(): Page {
end.x = 716
end.y = 614
world.append(end)
// 视口参加有限布局并占用剩余空间;移动它的 Camera2D 不改写卡片世界位置。
view = CanvasViewport(1024, 800, world)
view.flex = 1
page.crossAlign = "stretch"
-12
View File
@@ -1,13 +1,8 @@
//! 定义从托管对象提取的不可变视觉树。共享业务对象可以有多次展示,树节点与 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,
@@ -20,17 +15,14 @@ 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, .. }
@@ -46,7 +38,6 @@ impl UiNode {
}
}
/// 判断是否需要对子项执行弹性主轴分配。
pub fn has_flex(&self) -> bool {
self.flex > 0.0
|| self.flex_policy != crate::layout::FlexPolicy::default()
@@ -54,7 +45,6 @@ impl UiNode {
|| self.sizing.height == crate::box_model::Dimension::Fill
}
/// 判断当前节点是否会为子项指定实际分配范围,供源布局选择安排路径。
pub fn allocates_children(&self) -> bool {
self.children().iter().any(|child| {
child.has_flex()
@@ -63,7 +53,6 @@ impl UiNode {
})
}
/// 比较节点自身配置和直接子项身份,决定能否复用旧 Arc;不是深拷贝业务图。
pub fn shallow_eq(&self, other: &Self) -> bool {
if self.surface != other.surface
|| self.flex_policy != other.flex_policy
@@ -209,7 +198,6 @@ impl UiNode {
}
#[derive(Debug, Clone, PartialEq)]
/// 视觉内容的原生分类。叶节点保存绘制数据,容器保存子树,场景视口另外保存相机与世界投影。
pub enum UiKind {
Viewport3D {
width: f32,
-7
View File
@@ -1,6 +1,3 @@
//! 应用组合入口。先创建原生事件循环和共享发布状态,再启动 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;
@@ -34,16 +31,12 @@ 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()
}
-11
View File
@@ -1,6 +1,3 @@
//! 属性绑定:在只读计算中收集依赖,将结果写回绑定目标。
//! 缓存记录读取版本而不是复制业务模型;依赖变化才重新计算。托管连接对象追踪其拥有的引用。
//!
//! Target-owned managed bindings. Dependency versions are borrowed identities;
//! the host's weak Block registry never roots an otherwise unreachable graph.
use std::{
@@ -16,33 +13,27 @@ 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);
@@ -50,7 +41,6 @@ impl ObjectPayload for Binding {
}
}
/// 检查绑定目标与可调用计算,建立托管连接及其引用关系。
pub fn connect(
cx: &mut NativeContext<'_>,
target: ObjectRef,
@@ -124,7 +114,6 @@ pub fn connect(
Ok(token)
}
/// 在只读调用中重算失效绑定,采集新的读取版本,再将成功结果应用到目标属性。
pub fn refresh(
vm: &mut Vm,
nodes: &[WeakObjectRef],
-33
View File
@@ -1,7 +1,3 @@
//! 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;
@@ -11,14 +7,12 @@ 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,
@@ -55,7 +49,6 @@ 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,
@@ -63,11 +56,9 @@ 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"));
@@ -85,7 +76,6 @@ 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"));
@@ -98,26 +88,21 @@ 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,
@@ -137,7 +122,6 @@ impl ObjectPayload for Block {
}
#[derive(Debug, Clone)]
/// 组件当前业务描述与组成。与一次求出的屏幕几何分开保存,替换或属性变化会触发派生快照失效。
pub struct Data {
pub kind: Kind,
pub children: Vec<Child>,
@@ -171,7 +155,6 @@ 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 {
@@ -213,7 +196,6 @@ impl Data {
properties: None,
}
}
/// 检查原生组件种类是否承载子组成,供树修改和布局选择使用。
pub fn is_container(&self) -> bool {
matches!(
self.kind,
@@ -230,19 +212,15 @@ 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);
@@ -277,7 +255,6 @@ 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 {
@@ -299,13 +276,11 @@ 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,
@@ -336,13 +311,11 @@ 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),
@@ -388,16 +361,13 @@ 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())
}
@@ -516,7 +486,6 @@ 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
@@ -575,7 +544,6 @@ 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> {
@@ -831,7 +799,6 @@ 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")
}
-41
View File
@@ -1,7 +1,3 @@
//! 运行时与窗口线程之间的发布数据契约。NamespaceState 管理提交序号、版本和当前完整文档。
//! RenderDocument/Frame 包含绘制命令、Mount 几何、页面标签和相机投影。发布前统一检查数量、坐标、颜色及索引一致性。
//! 屏幕命中、逻辑坐标和世界 UI 的射线映射必须与绘制采用同一变换;这里的 ID 是数据身份,不是窗口线程可操作的托管引用。
//!
use std::collections::HashSet;
use std::sync::Arc;
@@ -17,23 +13,18 @@ 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(),
@@ -49,7 +40,6 @@ 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();
@@ -73,16 +63,13 @@ 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);
}
@@ -95,7 +82,6 @@ impl NamespaceState {
Ok(Some(revision))
}
/// 清空本对象维护的当前内容或缓存,后续状态由调用方重新建立。
pub fn clear(&mut self) -> Result<u64> {
let submission = self.next_submission()?;
let revision = self
@@ -112,7 +98,6 @@ 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>,
@@ -127,7 +112,6 @@ pub struct RenderDocument {
}
impl RenderDocument {
/// 检查当前输入或发布值的不变量,调用方据校验结果决定是否继续。
pub fn validate(&self) -> Result<()> {
let surface = &self.frame.surface;
if !surface.zoom.is_finite()
@@ -250,7 +234,6 @@ impl RenderDocument {
Ok(())
}
/// 校验文档后把逻辑绘制命令转换成给定设备比例的显示列表,应用变换与裁剪。
pub fn display_list(&self, scale: f32) -> Result<DisplayList> {
let frame = &self.frame;
let mut list = DisplayList::default();
@@ -338,14 +321,12 @@ 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,
@@ -359,7 +340,6 @@ 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");
@@ -379,7 +359,6 @@ impl CodeNode {
Ok(())
}
/// 按请求的代码节点身份或路径查找对应的源码投影。
fn find(&self, id: &str) -> Option<&CodeNode> {
if self.id == id {
return Some(self);
@@ -389,9 +368,7 @@ impl CodeNode {
}
#[derive(Clone, Debug, Serialize, Deserialize)]
/// 一个窗口的一次完整视觉发布:有序命令及其裁剪/变换、出现几何、焦点和版本。平行索引表必须和命令数量一致。
pub struct Frame {
// 命中索引是可重建的本地优化,不作为序列化协议的一部分保存。
#[serde(skip)]
pub hit_index: Arc<crate::viewport::spatial::Index>,
#[serde(default)]
@@ -423,7 +400,6 @@ pub struct Frame {
}
impl Default for Frame {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
hit_index: Arc::default(),
@@ -446,7 +422,6 @@ 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>>,
@@ -477,18 +452,15 @@ 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() {
@@ -503,7 +475,6 @@ 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;
@@ -517,7 +488,6 @@ impl MountGeometry {
}
impl Frame {
/// 从顶层绘制顺序向后查找可控制的视口,命中与裁剪均成立才接收相机手势。
pub fn camera_at(&self, point: Point) -> Option<&MountGeometry> {
self.hit_index
.candidates(point, self.mounts.len())
@@ -533,7 +503,6 @@ impl Frame {
})
})
}
/// 在无故障帧中按逆绘制顺序选中可交互的出现,空间索引只缩小候选。
pub fn hit_test(&self, point: Point) -> Option<&MountGeometry> {
if self.fault.is_some() {
return None;
@@ -546,19 +515,16 @@ 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,
@@ -600,7 +566,6 @@ pub enum RenderCommand {
}
impl RenderCommand {
/// 检查当前输入或发布值的不变量,调用方据校验结果决定是否继续。
pub(crate) fn validate(&self) -> Result<()> {
match self {
Self::Scene3D {
@@ -673,7 +638,6 @@ impl RenderCommand {
Ok(())
}
/// 把一条已验证的发布命令变成原生绘制原语,缩放几何和文字样式。
fn to_draw_command(&self, scale: f32) -> Result<DrawCommand> {
Ok(match self {
Self::Scene3D {
@@ -737,17 +701,14 @@ 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");
@@ -758,12 +719,10 @@ 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('#')
-2
View File
@@ -1,5 +1,3 @@
//! 模型层模块入口。包含托管业务对象、每次展示上下文、依赖缓存,以及交给窗口线程的发布值。
//!
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
pub(crate) mod binding;
pub(crate) mod block;
-34
View File
@@ -1,7 +1,3 @@
//! 每次展示的托管上下文:同一个 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};
@@ -15,20 +11,16 @@ 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.
@@ -36,23 +28,18 @@ 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 {
@@ -62,7 +49,6 @@ impl ObjectPayload for Mount {
}
#[derive(Debug, Clone, Copy)]
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
enum Query {
Bounds,
ScrollTo,
@@ -82,7 +68,6 @@ enum Query {
}
#[derive(Debug)]
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
struct Method {
signature: endor_core::CallSignature,
owner: ObjectRef,
@@ -91,11 +76,9 @@ 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!(
@@ -114,7 +97,6 @@ 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
@@ -342,7 +324,6 @@ impl NativeCallable for Method {
}
}
/// 声明 Mount 查询方法的源类型契约,区分几何、层次与坐标操作。
fn mount_signature(
store: &endor_core::ObjectStore,
desktop: ObjectRef,
@@ -389,14 +370,12 @@ 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>,
@@ -409,7 +388,6 @@ struct Sync {
}
#[derive(Debug)]
/// 将一个生命周期回调及参数接入同一 VM 调用序列。
struct LifecycleCall {
function: ObjectRef,
block: ObjectRef,
@@ -417,12 +395,10 @@ 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)?;
@@ -434,7 +410,6 @@ impl NativeCallable for LifecycleCall {
}
}
/// 构造生命周期调用所需的托管函数与参数,使回调遵循同一 VM 的引用和执行约束。
fn lifecycle_call(
cx: &mut NativeContext<'_>,
mount: ObjectRef,
@@ -462,12 +437,10 @@ 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 {
@@ -657,7 +630,6 @@ 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>,
@@ -666,7 +638,6 @@ 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() {
@@ -677,7 +648,6 @@ impl Checkpoint {
result
}
/// 恢复检查点中尚未发布的出现关系与布局上下文,并释放临时保留。
pub fn restore(
self,
vm: &mut Vm,
@@ -737,7 +707,6 @@ 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),
@@ -793,7 +762,6 @@ 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)? {
@@ -820,7 +788,6 @@ impl MountTable {
Ok(())
}
/// 按原始展示顺序返回弱 Mount 身份,供事件和协议驱动器保持稳定顺序。
pub fn ordered(&self) -> Vec<WeakObjectRef> {
self.order
.iter()
@@ -828,7 +795,6 @@ impl MountTable {
.collect()
}
/// 根据新结构复用或建立每次出现的 Mount,连接父子关系并准备生命周期调用。
pub fn synchronize(
&mut self,
vm: &mut Vm,
-20
View File
@@ -1,32 +1,23 @@
//! 给源协议返回的对象列表提供受控封装。列表内容通过托管引用被追踪,读方法只暴露检查后的条目。
//! 用于儿童组成、布局结果和几何输入等需要保留对象身份的场景;不是窗口线程共享的可变容器。
//!
//! 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);
@@ -35,10 +26,8 @@ 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],
@@ -46,12 +35,10 @@ 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"));
@@ -70,7 +57,6 @@ impl NativeCallable for At {
}
}
/// 将已有对象引用包装为新的 ObjectList,并安装默认名义类型及下标查询方法。
pub fn collection(
cx: &mut NativeContext<'_>,
mounts: Vec<ObjectRef>,
@@ -79,7 +65,6 @@ pub fn collection(
build(cx, mounts, desktop, None)
}
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
fn build(
cx: &mut NativeContext<'_>,
mounts: Vec<ObjectRef>,
@@ -111,28 +96,23 @@ 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"));
-8
View File
@@ -1,6 +1,3 @@
//! 一个原生窗口对应一份 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.
@@ -9,7 +6,6 @@ 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,
@@ -38,7 +34,6 @@ 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
@@ -87,7 +82,6 @@ impl Presentation {
});
}
/// 按尺寸、内容或绘制失效原因清理对应缓存,避免把所有变化都当成相同重建。
pub fn invalidate(
&mut self,
cx: &mut NativeContext<'_>,
@@ -118,7 +112,6 @@ impl Presentation {
Ok(())
}
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
pub fn new(desktop: ObjectRef, tabs: ObjectRef) -> Self {
Self {
desktop,
@@ -149,7 +142,6 @@ impl Presentation {
}
}
/// 把读取记录拆成对象版本和精确名称依赖,便于按实际观察内容判断失效。
pub(crate) fn split_reads(
reads: Vec<endor_core::ReadDependency>,
) -> (
-4
View File
@@ -1,13 +1,9 @@
//! 安装 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>,
-43
View File
@@ -1,7 +1,3 @@
//! 将 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::{
@@ -15,7 +11,6 @@ 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,
@@ -31,7 +26,6 @@ pub enum Constructor {
Panel,
}
impl Constructor {
/// 返回内部枚举或对象对应的稳定源名称。
pub fn name(self) -> &'static str {
match self {
Self::Vec3 => "Vec3",
@@ -48,7 +42,6 @@ impl Constructor {
Self::Panel => "Panel3D",
}
}
/// 取得本构造器对应的源工厂函数名称。
pub fn function(self) -> &'static str {
match self {
Self::Vec3 => "vec3",
@@ -65,7 +58,6 @@ impl Constructor {
Self::Panel => "panel3D",
}
}
/// 反查源构造器名称对应的原生 3D 种类,不认识的名称返回空值。
pub fn from_function(name: &str) -> Option<Self> {
[
Self::Vec3,
@@ -84,7 +76,6 @@ impl Constructor {
.into_iter()
.find(|kind| kind.function() == name)
}
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
pub fn arity(self) -> (usize, usize) {
match self {
Self::Vec3 | Self::Triangle => (3, 3),
@@ -96,7 +87,6 @@ impl Constructor {
_ => (0, 0),
}
}
/// 列出本构造器的参数类型名称,供签名安装与源调用检查使用。
pub fn parameters(self) -> &'static [&'static str] {
match self {
Self::Vec3 | Self::Triangle => &["Number", "Number", "Number"],
@@ -108,14 +98,12 @@ 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,
@@ -129,52 +117,40 @@ 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)
@@ -182,11 +158,9 @@ 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())
@@ -194,7 +168,6 @@ fn text(value: &Value) -> Result<String, VmError> {
Err(invalid("3D field requires text"))
}
}
/// 把经过转换的数值写回指定托管字段,仍通过 VM/NativeContext 的检查入口。
fn set_number(
cx: &mut NativeContext<'_>,
object: ObjectRef,
@@ -203,7 +176,6 @@ 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 {
@@ -216,7 +188,6 @@ fn color(value: &str) -> Result<[f32; 3], VmError> {
])
}
/// 按构造器建立对应托管 3D 类型,检查几何输入并安装默认属性。
pub fn allocate(
cx: &mut NativeContext<'_>,
desktop: ObjectRef,
@@ -397,22 +368,18 @@ 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])?,
@@ -420,7 +387,6 @@ 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 {
@@ -450,7 +416,6 @@ impl Reader<'_> {
}
}
/// 从场景中提取的有限 UI 根,携带稳定面板 key 和逻辑排版尺寸。
pub struct PanelMount {
pub key: u64,
pub content: ObjectRef,
@@ -461,7 +426,6 @@ pub type Snapshot = (
Vec<(ObjectRef, String)>,
Vec<PanelMount>,
);
/// 将当前托管或可变状态提取为后续阶段使用的派生表示。
pub fn snapshot(
store: &ObjectStore,
scene: ObjectRef,
@@ -469,7 +433,6 @@ pub fn snapshot(
) -> Result<Snapshot, VmError> {
snapshot_reusing(store, scene, camera, None)
}
/// 遍历场景并记录字段依赖;如果世界数据未变则复用不可变场景和 BVH,只替换摄像机视图。
pub fn snapshot_reusing(
store: &ObjectStore,
scene: ObjectRef,
@@ -652,7 +615,6 @@ pub fn snapshot_reusing(
Ok((view, reader.reads.into_iter().collect(), panels))
}
/// 把视口中的指针射线结果转换成 RayHit3D,关联实际托管网格和交点数据。
pub fn pick(
cx: &mut NativeContext<'_>,
desktop: ObjectRef,
@@ -744,7 +706,6 @@ pub fn pick(
}
#[derive(Debug)]
/// 指针拾取的中间结果,保留交点与所属实例信息供 Event/RayHit3D 使用。
pub struct PointerPick {
pub mount: ObjectRef,
pub desktop: ObjectRef,
@@ -752,7 +713,6 @@ pub struct PointerPick {
}
/// The default controller is replaceable; camera data has no dependency on it.
/// 为指定出现解码相机手势并调用控制器;嵌套面板输入先转换到所属视口坐标。
pub fn control(
cx: &mut NativeContext<'_>,
controller: ObjectRef,
@@ -849,7 +809,6 @@ pub fn control(
Ok(())
}
/// 执行托管 3D 节点的原生平移/旋转操作,供尚不支持普通数值算术表达式的源程序调用。
pub fn transform(
cx: &mut NativeContext<'_>,
owner: ObjectRef,
@@ -904,11 +863,9 @@ 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<'_>,
-14
View File
@@ -1,7 +1,3 @@
//! 从可变托管对象图提取不可变 UiNode,并维护反向依赖与父子失效传播。
//! 缓存只借用可检查的对象身份,不增加托管根。对象释放后必须清理缓存索引;快照仍可持有独立的原生数值数据。
//! 干净子树直接复用 Arc;脏节点重新检查属性、方法、绑定和子项,循环与展开数量在发布前拒绝。
//!
//! Object-to-visual dependencies. Cached results never own VM references.
use crate::{
block::{self, Kind},
@@ -14,7 +10,6 @@ use std::{
};
#[derive(Debug)]
/// 某个业务组件的视觉快照,以及使它失效的读取依赖和子项身份。
struct Cached {
node: Arc<UiNode>,
dependencies: Vec<Dependency>,
@@ -22,14 +17,12 @@ 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>,
@@ -42,7 +35,6 @@ pub struct RenderCache {
}
impl RenderCache {
/// 把 VM 变化映射到读取过这些属性/对象的视觉节点,再沿父链向上传播脏标记。
pub fn invalidate(&mut self, batch: ChangeBatch) {
let mut queue = VecDeque::new();
if batch.rescan_required {
@@ -66,7 +58,6 @@ 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() {
@@ -78,7 +69,6 @@ impl RenderCache {
}
}
/// 撤销旧缓存的反向读取边和父子边,为重新登记该节点做准备。
fn remove(&mut self, block: ObjectRef) -> Option<Cached> {
let old = self.entries.remove(&block)?;
for source in &old.dependencies {
@@ -100,7 +90,6 @@ impl RenderCache {
Some(old)
}
/// 删除托管堆中已不存在的身份,缓存本身不负责保持业务对象存活。
pub fn prune(&mut self, store: &ObjectStore) {
self.scenes3d
.retain(|object, _| store.object(*object).is_ok());
@@ -116,7 +105,6 @@ impl RenderCache {
}
}
/// 按显式栈遍历托管组成;复用干净子树,重建脏节点,并在完成前检查循环和资源上限。
pub fn snapshot(
&mut self,
store: &ObjectStore,
@@ -128,7 +116,6 @@ 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)
@@ -211,7 +198,6 @@ impl RenderCache {
} else {
None
};
// path 只追踪当前祖先链,因此允许同一业务组件在不同分支多次出现。
if !finish {
if !path.insert(data_ref) {
return Err(VmError::InvalidArgument("cyclic Block composition"));
-33
View File
@@ -1,7 +1,3 @@
//! 托管 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};
@@ -14,7 +10,6 @@ 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),
@@ -23,27 +18,23 @@ 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,
@@ -57,23 +48,18 @@ 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 {
@@ -85,11 +71,9 @@ 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"));
@@ -100,7 +84,6 @@ 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"));
@@ -115,7 +98,6 @@ fn scalar(value: &Value) -> Result<f64, VmError> {
Ok(number)
}
#[derive(Debug, Clone, Copy)]
/// 当前宿主 API 的操作分类,分派代码负责参数校验与状态更新。
enum Operation {
Id,
Viewport,
@@ -126,18 +108,15 @@ 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,
@@ -146,7 +125,6 @@ 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"));
@@ -218,7 +196,6 @@ impl NativeCallable for Method {
Ok(NativeResult::Return(result))
}
}
/// 把合法窗口操作追加到所属窗口的待发布命令,不能直接在 VM 线程操作 OS。
pub fn enqueue(
cx: &mut NativeContext<'_>,
owner: ObjectRef,
@@ -248,7 +225,6 @@ pub fn enqueue(
Ok(())
}
/// 将页面登记到其窗口,维护托管所有权与页面顺序。
pub fn register_page(
cx: &mut NativeContext<'_>,
window: ObjectRef,
@@ -277,7 +253,6 @@ pub fn register_page(
}
Ok(())
}
/// 从窗口的页面集合撤销指定页面,保持后续展示索引一致。
pub fn unregister_page(
cx: &mut NativeContext<'_>,
window: ObjectRef,
@@ -301,7 +276,6 @@ pub fn unregister_page(
Ok(())
}
/// Discard an unpublished Page registration without invoking guest callbacks.
/// 宿主收尾路径撤销页面登记,用于已经由外层决定的关闭/退役流程。
pub fn unregister_page_host(
vm: &mut Vm,
window: ObjectRef,
@@ -316,7 +290,6 @@ pub fn unregister_page_host(
Ok(())
}
/// 在窗口之间移动页面的所有权和登记顺序,保留同一业务页面身份。
pub fn move_page(
cx: &mut NativeContext<'_>,
window: ObjectRef,
@@ -350,11 +323,9 @@ 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
@@ -365,7 +336,6 @@ 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>,
@@ -443,7 +413,6 @@ 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,
@@ -480,7 +449,6 @@ 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(
@@ -509,7 +477,6 @@ 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)?;
-11
View File
@@ -1,13 +1,9 @@
//! 有限布局盒子的尺寸策略。区分 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,
@@ -16,7 +12,6 @@ pub enum Dimension {
}
impl Dimension {
/// 把内部尺寸策略投影为相应的源属性表示。
pub fn value(self) -> Value {
match self {
Self::Auto => Value::Text("auto".into()),
@@ -25,7 +20,6 @@ impl Dimension {
}
}
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
pub fn parse(value: &Value) -> Result<Self, VmError> {
match value {
Value::Text(text) if text == "auto" => Ok(Self::Auto),
@@ -49,7 +43,6 @@ impl Dimension {
}
#[derive(Debug, Clone, Copy, PartialEq)]
/// border-box 尺寸策略与双轴上下限,实际求解仍要结合父级约束。
pub struct Sizing {
pub width: Dimension,
pub height: Dimension,
@@ -60,7 +53,6 @@ pub struct Sizing {
}
impl Default for Sizing {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
width: Dimension::Auto,
@@ -75,7 +67,6 @@ 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);
@@ -90,9 +81,7 @@ impl Sizing {
constraints
}
/// 结合内容期望尺寸、轴策略和父级约束求最终盒子尺寸;无界轴不能解析 fill。
pub fn resolve(self, intrinsic: Size, constraints: [f32; 4]) -> Result<Size> {
/// 针对单个尺寸轴应用约束与边界规则,供双轴尺寸求解复用。
fn axis(
policy: Dimension,
intrinsic: f32,
-14
View File
@@ -1,11 +1,7 @@
//! 面向设备像素的绘制指令集合。它承载矩形、文字、裁剪与 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,
@@ -14,7 +10,6 @@ pub struct Color {
}
impl Color {
/// 从 RGB 通道构造不透明颜色值。
pub const fn rgb(red: u8, green: u8, blue: u8) -> Self {
Self {
red,
@@ -24,7 +19,6 @@ impl Color {
}
}
/// 从 RGBA 通道构造带透明度的颜色值。
pub const fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
Self {
red,
@@ -36,7 +30,6 @@ impl Color {
}
#[derive(Clone, Debug)]
/// 原生画布最终顺序执行的绘制原语,已经使用对应设备比例。
pub enum DrawCommand {
Scene3D {
rect: Rect,
@@ -71,7 +64,6 @@ pub enum DrawCommand {
}
impl DrawCommand {
/// 返回平移后的绘制命令,并同步移动命令中的相关几何。
pub fn translated(mut self, delta: Point) -> Self {
match &mut self {
Self::WrappedText { rect, .. }
@@ -93,13 +85,11 @@ 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 {
@@ -139,12 +129,10 @@ 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 {
@@ -165,12 +153,10 @@ impl DisplayList {
}
}
/// 借用按绘制顺序排列的原生原语,不复制命令集合。
pub fn commands(&self) -> &[DrawCommand] {
&self.commands
}
/// 按原顺序合并另一组绘制命令,不在这里重排层次。
pub fn extend(&mut self, other: DisplayList) {
self.commands.extend(other.commands);
}
-7
View File
@@ -1,21 +1,16 @@
//! 共享的二维点和矩形值。命中判断在调用方完成坐标系转换后使用,矩形本身不隐式处理相机。
//!
#[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,
@@ -24,7 +19,6 @@ pub struct Rect {
}
impl Rect {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
@@ -34,7 +28,6 @@ impl Rect {
}
}
/// 判断给定点是否位于目标范围;需要相机转换的类型会先转换再判断。
pub fn contains(self, point: Point) -> bool {
point.x >= self.x
&& point.x <= self.x + self.width
-56
View File
@@ -1,7 +1,3 @@
//! 原生布局求解器:自底向上测量期望尺寸,再自顶向下分配位置和实际尺寸。
//! 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::{
@@ -15,7 +11,6 @@ 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,
@@ -24,15 +19,12 @@ 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(),
@@ -44,7 +36,6 @@ 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,
@@ -54,7 +45,6 @@ pub enum CrossAlign {
}
impl CrossAlign {
/// 返回内部枚举或对象对应的稳定源名称。
pub fn name(self) -> &'static str {
match self {
Self::Start => "start",
@@ -64,7 +54,6 @@ impl CrossAlign {
}
}
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
pub fn parse(value: &str) -> Result<Self, endor_core::VmError> {
match value {
"start" => Ok(Self::Start),
@@ -77,7 +66,6 @@ impl CrossAlign {
}
}
/// 根据交叉轴对齐方式计算有限剩余空间中的偏移。
fn offset(self, available: f32, occupied: f32) -> f32 {
let spare = (available - occupied).max(0.0);
match self {
@@ -89,7 +77,6 @@ impl CrossAlign {
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
/// 容器主轴的内容对齐或剩余间隔分配方式。
pub enum MainAlign {
#[default]
Start,
@@ -98,7 +85,6 @@ pub enum MainAlign {
SpaceBetween,
}
impl MainAlign {
/// 返回内部枚举或对象对应的稳定源名称。
pub fn name(self) -> &'static str {
match self {
Self::Start => "start",
@@ -107,7 +93,6 @@ impl MainAlign {
Self::SpaceBetween => "space-between",
}
}
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
pub fn parse(value: &str) -> Result<Self, endor_core::VmError> {
match value {
"start" => Ok(Self::Start),
@@ -123,7 +108,6 @@ 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,
@@ -131,7 +115,6 @@ pub struct FlexPolicy {
pub shrink: f32,
}
impl Default for FlexPolicy {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
basis: None,
@@ -143,14 +126,12 @@ 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),
@@ -160,7 +141,6 @@ enum Shape {
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
/// 结构出现身份键,区分同一业务对象在不同父位置的展示。
struct Key {
parent: Option<u64>,
slot: u64,
@@ -168,7 +148,6 @@ struct Key {
}
#[derive(Debug, Clone, Default)]
/// 布局缓存、稳定出现身份与统计。构建在临时副本上完成,成功后才写回。
pub struct LayoutRuntime {
sizes: HashMap<String, (Shape, Size)>,
ids: HashMap<Key, u64>,
@@ -182,7 +161,6 @@ pub struct LayoutRuntime {
pub page_mode: Option<crate::page::Mode>,
}
/// 迭代遍历中的一个出现,保存父子索引、约束与稳定 ID。
struct Item<'a> {
node: &'a UiNode,
parent: Option<usize>,
@@ -191,7 +169,6 @@ 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");
@@ -199,7 +176,6 @@ 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");
@@ -207,7 +183,6 @@ 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,
@@ -216,7 +191,6 @@ 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
@@ -224,7 +198,6 @@ 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 {
@@ -266,7 +239,6 @@ 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()
@@ -282,7 +254,6 @@ 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,
@@ -310,7 +281,6 @@ pub fn occurrence_constraints(
margin_constraints(child, base)
}
/// 生成求解开始时的子项约束,供后续测量和分配迭代使用。
pub fn initial_child_constraints(
parent: &UiNode,
child: &UiNode,
@@ -331,7 +301,6 @@ 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);
@@ -353,7 +322,6 @@ 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]),
@@ -362,7 +330,6 @@ 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, .. }
@@ -380,7 +347,6 @@ 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)),
@@ -392,12 +358,10 @@ 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) => {
@@ -455,7 +419,6 @@ 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 {
@@ -518,7 +481,6 @@ 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)?)
}
@@ -526,7 +488,6 @@ 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>,
@@ -534,7 +495,6 @@ 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);
@@ -577,7 +537,6 @@ 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;
@@ -614,7 +573,6 @@ 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
@@ -623,7 +581,6 @@ 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 {
@@ -637,7 +594,6 @@ 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);
@@ -796,7 +752,6 @@ pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size], preferred: &[S
output
}
/// 根据最终分配尺寸计算子项原点,处理容器方向、对齐、间距与外边距。
pub fn arrange_native(
node: &UiNode,
x: f32,
@@ -932,31 +887,25 @@ 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.
@@ -967,7 +916,6 @@ 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)?;
@@ -975,7 +923,6 @@ impl LayoutRuntime {
Ok(frame)
}
/// 以形状、子项尺寸和约束查缓存;未命中才执行测量并更新本轮统计。
pub fn measure_node(
&mut self,
node: &UiNode,
@@ -996,7 +943,6 @@ impl LayoutRuntime {
Ok(size)
}
/// 迭代展开出现树、计算约束和几何、产生绘制命令;结构准备与完整布局共用此驱动。
fn build(&mut self, root: &UiNode, structural: bool) -> Result<Frame> {
self.measured = 0;
self.arranged = 0;
@@ -1145,12 +1091,10 @@ 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),
-2
View File
@@ -1,5 +1,3 @@
//! 渲染模块入口。排版、源视觉协议、相机投影和最终光栅化各自独立,通过不可变帧衔接。
//!
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
pub(crate) mod box_model;
pub(crate) mod display_list;
-20
View File
@@ -1,6 +1,3 @@
//! 让原生组件也通过 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::{
@@ -15,11 +12,9 @@ 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)
@@ -27,7 +22,6 @@ fn object(value: &Value) -> Result<ObjectRef, VmError> {
Err(invalid("visual protocol requires objects"))
}
}
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
pub(crate) fn number(
cx: &NativeContext<'_>,
object: ObjectRef,
@@ -46,11 +40,9 @@ 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,
@@ -64,7 +56,6 @@ 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,
@@ -84,7 +75,6 @@ fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size,
})
}
#[derive(Debug, Clone, Copy)]
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
enum Query {
Measure,
Arrange,
@@ -92,14 +82,12 @@ 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,
@@ -134,16 +122,13 @@ 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"));
@@ -367,18 +352,15 @@ 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
@@ -387,7 +369,6 @@ 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"));
@@ -438,7 +419,6 @@ impl NativeCallable for PointMethod {
}
}
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
pub fn install(
vm: &mut Vm,
roots: &mut Vec<ObjectRef>,
-15
View File
@@ -1,6 +1,3 @@
//! 旧 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};
@@ -9,7 +6,6 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// 旧 Page 的有限或自由空间模式,新场景视口不依赖它表达布局方向。
pub enum Mode {
#[default]
Window,
@@ -19,7 +15,6 @@ pub enum Mode {
}
impl Mode {
/// 返回内部枚举或对象对应的稳定源名称。
pub fn name(self) -> &'static str {
match self {
Self::Window => "window",
@@ -29,7 +24,6 @@ impl Mode {
}
}
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
pub fn parse(name: &str) -> Result<Self, VmError> {
match name {
"window" => Ok(Self::Window),
@@ -42,7 +36,6 @@ impl Mode {
}
}
/// 为旧页面模式生成有限轴和可延伸轴的根约束。
pub fn constraints(self, viewport: Option<Size>) -> [f32; 4] {
let Some(size) = viewport else {
return crate::layout::root_constraints(None);
@@ -58,7 +51,6 @@ impl Mode {
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
/// 页面级兼容相机状态和有限内容范围,支持绘制与命中正逆变换。
pub struct Surface {
pub mode: Mode,
pub page: String,
@@ -68,13 +60,11 @@ pub struct Surface {
pub extent: Size,
}
/// 生成页面相机计算所需的单位比例或边界默认值。
fn unit() -> f32 {
1.0
}
impl Default for Surface {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
mode: Mode::Window,
@@ -87,7 +77,6 @@ impl Default for Surface {
}
impl Surface {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
pub fn new(mode: Mode) -> Self {
Self {
mode,
@@ -96,7 +85,6 @@ impl Surface {
}
}
/// 把页面视图坐标反算为旧 Page 相机下的世界坐标。
pub fn to_world(&self, point: Point) -> Point {
Point::new(
point.x / self.zoom + self.pan.x,
@@ -104,7 +92,6 @@ impl Surface {
)
}
/// 把旧页面世界坐标变换到可见视图坐标。
pub fn to_view(&self, point: Point) -> Point {
Point::new(
(point.x - self.pan.x) * self.zoom,
@@ -112,7 +99,6 @@ 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
@@ -120,7 +106,6 @@ 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 => {
-6
View File
@@ -1,6 +1,3 @@
//! 合成整个原生窗口的显示列表:页面画面、宿主侧栏和开发工具绘制。
//! 窗口逻辑尺寸与设备像素比例在这里衔接。页面内容先应用其视角并裁剪,宿主控件的区域和命中布局另外返回。
//!
use crate::{
activity::Activity,
developer_tools::{self, DeveloperToolsLayout, Workspace as DeveloperToolsState},
@@ -10,12 +7,10 @@ use crate::{
};
#[derive(Clone, Debug, Default)]
/// 宿主管理区域的几何布局,供窗口事件分辨页面与工具命中。
pub struct SceneLayout {
pub developer_tools: DeveloperToolsLayout,
}
/// 组合页面、开发工具和侧栏的设备像素显示列表,同时返回供命中使用的宿主布局。
pub fn build_scene(
width: u32,
height: u32,
@@ -89,7 +84,6 @@ pub fn build_scene(
}
}
/// 整窗合成结果:显示列表与同帧管理交互几何一起返回。
pub struct DisplayScene {
pub display_list: DisplayList,
pub layout: SceneLayout,
-39
View File
@@ -1,7 +1,3 @@
//! 有界 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;
@@ -15,7 +11,6 @@ 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]>,
@@ -23,7 +18,6 @@ pub struct Geometry {
pub uv: Vec<[f32; 2]>,
}
impl Geometry {
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
pub fn valid(&self) -> bool {
self.vertices.len() <= MAX_VERTICES
&& (self.uv.is_empty()
@@ -37,7 +31,6 @@ 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 {
@@ -70,7 +63,6 @@ impl Geometry {
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
/// 一次场景实例:几何、世界矩阵、颜色与可选 UI 面板,身份用于拾取回关联。
pub struct Mesh {
pub identity: String,
pub geometry: Arc<Geometry>,
@@ -80,14 +72,12 @@ 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,
@@ -96,14 +86,12 @@ 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>,
@@ -113,7 +101,6 @@ 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
@@ -122,26 +109,21 @@ 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,
@@ -153,13 +135,10 @@ 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));
}
@@ -240,7 +219,6 @@ 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)
@@ -300,16 +278,13 @@ 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,
@@ -318,13 +293,11 @@ 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,
@@ -368,13 +341,11 @@ 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,
@@ -385,7 +356,6 @@ impl Vertex {
}
}
}
/// 生成相机空间六个视锥平面,供三角形裁剪与 BVH 可见性查询共用。
fn frustum_planes(camera: Camera, aspect: f32) -> [[f32; 4]; 6] {
let (x, y, slope) = match camera.projection {
Projection::Perspective { fov } => {
@@ -403,7 +373,6 @@ 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() {
@@ -433,11 +402,9 @@ 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,
@@ -481,7 +448,6 @@ 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()?;
@@ -498,7 +464,6 @@ fn render_aspect(
}
}
let color = rgb(std::array::from_fn(|i| mesh.color[i] * light[i]));
// 在透视除法之前裁剪,并插入具有正确 UV 的交点;不能先投影再裁剪近面。
let polygon = clip(
indices
.map(|index| Vertex {
@@ -520,7 +485,6 @@ 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));
@@ -551,19 +515,16 @@ 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
-29
View File
@@ -1,26 +1,19 @@
//! 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,
@@ -28,16 +21,13 @@ 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()
@@ -46,31 +36,26 @@ 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.,
@@ -78,7 +63,6 @@ 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])
@@ -111,7 +95,6 @@ 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 {
@@ -123,7 +106,6 @@ 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],
@@ -131,7 +113,6 @@ 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();
@@ -159,14 +140,12 @@ 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,
@@ -176,7 +155,6 @@ pub struct Camera {
pub projection: Projection,
}
impl Default for Camera {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
position: Vec3::new(0., 0., 5.),
@@ -189,21 +167,18 @@ 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()
@@ -221,12 +196,10 @@ 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.);
@@ -251,13 +224,11 @@ 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;
-15
View File
@@ -1,7 +1,3 @@
//! 连接 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::{
@@ -15,7 +11,6 @@ use crate::{
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
/// 一次面板出现的正逆投影信息,可以链接父面板形成嵌套映射。
pub struct Mapping {
pub owner: u64,
pub key: u64,
@@ -26,7 +21,6 @@ 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)?
@@ -53,7 +47,6 @@ 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
@@ -63,7 +56,6 @@ 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)
@@ -89,7 +81,6 @@ 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
@@ -127,7 +118,6 @@ impl Mapping {
}
#[derive(Clone)]
/// 归属明确的一条内部 UI 绘制命令,装配时还保留裁剪、变换和 owner。
struct Entry {
command: crate::document::RenderCommand,
clip: Option<[f32; 4]>,
@@ -137,14 +127,12 @@ 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];
@@ -184,7 +172,6 @@ pub fn compose(frame: &mut crate::document::Frame) -> anyhow::Result<()> {
owner: mount.id,
});
}
/// 递归装配一个绘制条目包含的世界 UI,检查嵌套环与深度,再生成独立不可变视图。
fn assemble(
entry: Entry,
groups: &HashMap<(u64, u64), Vec<Entry>>,
@@ -199,7 +186,6 @@ 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 {
@@ -240,7 +226,6 @@ 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 =
-17
View File
@@ -1,6 +1,3 @@
//! 不可变世界空间 BVH。用网格变换后的包围盒快速筛选视锥与射线候选。
//! 空间索引只做宽阶段筛选;最终拾取仍检查真实三角形。相机变化可复用索引,几何或变换变化需要新索引。
//!
//! Immutable world-space BVH. Camera changes reuse it; it contains no VM data.
use super::{
Mesh,
@@ -8,13 +5,11 @@ 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
@@ -41,7 +36,6 @@ impl Bounds {
},
))
}
/// 求两个包围盒的最小共同外包围,供 BVH 父节点使用。
fn union(self, other: Self) -> Self {
Self {
min: Vec3::new(
@@ -56,13 +50,11 @@ 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;
@@ -87,7 +79,6 @@ 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(
@@ -119,7 +110,6 @@ impl Bounds {
}
}
#[derive(Debug)]
/// BVH 叶或分支;叶保存实例索引,分支保存两个子节点索引。
enum Node {
Leaf {
bounds: Bounds,
@@ -132,7 +122,6 @@ enum Node {
},
}
impl Node {
/// 读取叶/分支统一的包围盒,隐藏节点种类差异。
fn bounds(&self) -> Bounds {
match self {
Self::Leaf { bounds, .. } | Self::Branch { bounds, .. } => *bounds,
@@ -141,14 +130,12 @@ 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
@@ -166,7 +153,6 @@ impl Index {
}
index
}
/// 按包围盒最长轴的中心排序并递归二分,叶节点保留少量网格候选。
fn split(&mut self, items: &mut [usize]) -> usize {
let bounds = items
.iter()
@@ -210,11 +196,9 @@ 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();
@@ -239,7 +223,6 @@ 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();
-14
View File
@@ -1,6 +1,3 @@
//! 逐 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::{
@@ -15,7 +12,6 @@ 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,
@@ -23,7 +19,6 @@ pub enum Axis {
Horizontal,
}
impl Axis {
/// 返回内部枚举或对象对应的稳定源名称。
pub fn name(self) -> &'static str {
match self {
Self::Both => "both",
@@ -31,7 +26,6 @@ impl Axis {
Self::Horizontal => "horizontal",
}
}
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
pub fn parse(name: &str) -> Result<Self, VmError> {
match name {
"both" => Ok(Self::Both),
@@ -42,17 +36,14 @@ 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]);
@@ -63,7 +54,6 @@ 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];
@@ -211,7 +201,6 @@ 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(
@@ -227,7 +216,6 @@ pub fn coordinate(value: &Value) -> Result<f32, VmError> {
}
Ok(n)
}
/// 更新指定出现的滚动偏移,并钳制到本轴实际可滚动的内容范围。
pub fn set(
cx: &mut NativeContext<'_>,
owner: ObjectRef,
@@ -261,7 +249,6 @@ 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,
@@ -315,7 +302,6 @@ 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
-19
View File
@@ -1,23 +1,17 @@
//! 把 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();
@@ -33,12 +27,10 @@ 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);
@@ -51,7 +43,6 @@ pub(crate) fn reserve_pixels(units: usize) -> Result<(), &'static str> {
}
impl SoftwareRenderer {
/// 顺序执行显示列表并维护裁剪栈;3D 和面板可递归进入同一渲染预算域。
pub fn render(
list: &DisplayList,
target: &mut [u32],
@@ -154,7 +145,6 @@ impl SoftwareRenderer {
}
}
/// 借用目标像素切片并维护当前裁剪,所有像素写入都受该上下文限制。
struct Canvas<'a> {
pixels: &'a mut [u32],
width: u32,
@@ -163,12 +153,10 @@ 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);
@@ -181,7 +169,6 @@ 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);
@@ -207,7 +194,6 @@ 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;
@@ -222,7 +208,6 @@ 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;
@@ -255,7 +240,6 @@ 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,
@@ -271,7 +255,6 @@ impl Canvas<'_> {
)
}
/// 在画布与当前裁剪范围内进行透明度混合,防止文字/图形写出目标缓冲。
fn blend_pixel(&mut self, x: i32, y: i32, source: Color) {
if x < 0
|| y < 0
@@ -305,7 +288,6 @@ 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;
@@ -324,7 +306,6 @@ 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
}
-15
View File
@@ -1,6 +1,3 @@
//! 通过真正的源/原生 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};
@@ -10,15 +7,12 @@ 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"));
@@ -27,15 +21,12 @@ 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 => {
@@ -46,7 +37,6 @@ impl NativeCallable for SameTarget {
Ok(NativeResult::Return(value))
}
}
/// 构造只读命中选择驱动所需的托管代码与函数对象。
fn code(
cx: &mut NativeContext<'_>,
instructions: Vec<Instruction>,
@@ -62,7 +52,6 @@ fn code(
let globals = cx.new_dict()?;
cx.new_function("pointer hitTest", code, globals)
}
/// 按逆绘制顺序构造候选 hitTest 调用,先进行几何与局部坐标筛选。
fn selector(
cx: &mut NativeContext<'_>,
candidates: &[WeakObjectRef],
@@ -148,7 +137,6 @@ fn selector(
code(cx, instructions, constants, CodeLayout::default())
}
#[derive(Debug)]
/// 在一个只读调用中准备两次指针选择所需的候选与局部坐标。
struct Prepare {
candidates: Vec<WeakObjectRef>,
press: [f32; 2],
@@ -156,12 +144,10 @@ 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)?;
@@ -206,7 +192,6 @@ impl NativeCallable for Prepare {
}
}
/// Returned Mount owns one root. No-hit and mismatched targets return None.
/// 解析输入所指向的实际目标,避免把包装身份与底层数据混淆。
pub fn resolve(
vm: &mut Vm,
desktop: ObjectRef,
-41
View File
@@ -1,7 +1,3 @@
//! 源 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::{
@@ -26,11 +22,9 @@ 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),
@@ -38,7 +32,6 @@ fn desktop_type_registry(cx: &NativeContext<'_>, desktop: ObjectRef) -> Result<O
}
}
#[derive(Debug, Clone)]
/// 一次源测量的缓存输入、输出和真实读取依赖;不同约束或依赖版本不能复用。
struct Measurement {
owner: ObjectRef,
method: ObjectRef,
@@ -49,14 +42,12 @@ 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
@@ -64,12 +55,10 @@ 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;
@@ -77,7 +66,6 @@ impl Cache {
}
}
#[derive(Debug)]
/// 一个等待源测量返回的收集项,记录本次方法、约束和子项输入。
struct Pending {
token: usize,
owner: ObjectRef,
@@ -87,13 +75,10 @@ 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,
@@ -107,19 +92,16 @@ 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",
@@ -128,7 +110,6 @@ impl Phase {
}
}
#[derive(Debug)]
/// 为某次出现准备一个 measure/arrange 调用,选择原生快速结果或真正的源覆盖。
struct Step {
plan: Shared,
index: usize,
@@ -137,12 +118,10 @@ 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
@@ -314,7 +293,6 @@ impl NativeCallable for Step {
}
}
#[derive(Debug)]
/// 接收并检查源协议返回结果,将其合入 Plan,而不是直接写成发布帧。
struct Collect {
plan: Shared,
index: usize,
@@ -322,12 +300,10 @@ 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
@@ -520,17 +496,14 @@ 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
@@ -653,18 +626,15 @@ 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",
@@ -685,7 +655,6 @@ impl NativeCallable for Prepare {
}
#[derive(Debug)]
/// 在同一驱动帧中安排下一项布局工作,使大树能够分片让出。
struct Advance {
plan: Shared,
mounts: Arc<[WeakObjectRef]>,
@@ -693,11 +662,9 @@ 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 = {
@@ -774,7 +741,6 @@ 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,
@@ -783,7 +749,6 @@ pub struct Draft {
}
#[must_use = "pending layout work must be resumed or cancelled with its owning VM"]
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
pub enum Progress {
Pending(Work),
Complete(Draft),
@@ -791,7 +756,6 @@ 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,
@@ -801,14 +765,12 @@ 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);
@@ -817,7 +779,6 @@ impl Work {
Ok(())
}
/// 接收一次续体进度,挂起时保留工作,完成时将派生结果交给收尾阶段。
fn accept(
self,
vm: &mut Vm,
@@ -844,7 +805,6 @@ impl Work {
}
}
/// 建立只读布局 Plan 与托管驱动函数;小型原生树可走直接路径,大树保留分片推进能力。
pub fn start(
vm: &mut Vm,
desktop: ObjectRef,
@@ -985,7 +945,6 @@ pub fn start(
work.accept(vm, progress)
}
/// 验证求解已收敛,收集最终几何、读取依赖和缓存,返回尚待桥接层提交的 Draft。
fn finish(
vm: &mut Vm,
mut work: Work,
-28
View File
@@ -1,7 +1,3 @@
//! 执行源 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::{
@@ -21,11 +17,9 @@ 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)
@@ -33,7 +27,6 @@ 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)
@@ -41,7 +34,6 @@ 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"));
@@ -58,7 +50,6 @@ 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(
@@ -67,7 +58,6 @@ fn named_type(store: &ObjectStore, desktop: ObjectRef, name: &str) -> Result<Obj
.ok_or(invalid("missing paint type"))?,
)
}
/// 把源方法返回的托管绘制值解码成受限 Rust 命令,检查名义类型、字段和绘制预算。
fn decode(
store: &ObjectStore,
command: ObjectRef,
@@ -160,26 +150,22 @@ 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"));
@@ -209,19 +195,16 @@ 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",
@@ -242,7 +225,6 @@ impl NativeCallable for Prepare {
}
#[derive(Debug)]
/// 安排下一个出现的绘制调用,保持原始绘制顺序和共享 VM 预算。
struct Advance {
desktop: ObjectRef,
mounts: Arc<[(u64, WeakObjectRef)]>,
@@ -250,11 +232,9 @@ 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
@@ -324,7 +304,6 @@ impl NativeCallable for Advance {
}
}
/// 已经完成本阶段、但尚未由桥接层发布的派生结果,包含帧/缓存或真实读取依赖。
pub struct Draft {
pub frame: Frame,
pub reads: Vec<endor_core::ReadDependency>,
@@ -332,14 +311,12 @@ 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,
@@ -350,14 +327,12 @@ 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);
@@ -366,7 +341,6 @@ impl Work {
Ok(())
}
/// 区分源绘制续体是挂起还是完成,保留工作或结束只读域并生成 Draft。
fn accept(
self,
vm: &mut Vm,
@@ -394,7 +368,6 @@ impl Work {
}
}
/// 建立本轮只读绘制驱动和结果收集器;源覆盖与原生默认绘制最终保持同一出现顺序。
pub fn start(
vm: &mut Vm,
desktop: ObjectRef,
@@ -438,7 +411,6 @@ 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
-21
View File
@@ -1,7 +1,3 @@
//! 统一字体发现、文字整形、测量与抗锯齿字形绘制。
//! 测量与绘制使用同一字体系统和样式,避免布局宽度与屏幕文字不一致;中文与其他字符通过字体回退处理。
//! 整形和字形缓存有数量/字节预算;极端放大采用有界字形 LOD,最终只遍历裁剪区域内的像素。
//!
//! Shared font discovery, shaping, measurement and antialiased rasterization.
use std::{
collections::HashMap,
@@ -13,14 +9,12 @@ 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,
@@ -28,7 +22,6 @@ pub struct TextStyle {
}
impl TextStyle {
/// 生成界面文字的基准字号与行高,size 是协议使用的字号倍率。
pub fn ui(size: u32) -> Self {
Self {
font_size: 14.0 * size as f32,
@@ -37,7 +30,6 @@ impl TextStyle {
}
}
/// 生成源码/Console 使用的等宽风格基础尺寸。
pub fn code() -> Self {
Self {
font_size: 13.0,
@@ -46,7 +38,6 @@ impl TextStyle {
}
}
/// 同比缩放字号与行高,保持设备比例变化后的排版比例一致。
pub fn scaled(self, scale: f32) -> Self {
Self {
font_size: self.font_size * scale,
@@ -57,14 +48,12 @@ 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,
@@ -73,13 +62,11 @@ struct TextKey {
wrap_width: Option<u32>,
}
/// 已整形 Buffer 与测量结果,后续测量/绘制复用同一布局。
struct PreparedText {
buffer: Buffer,
size: TextSize,
}
/// 共享字体系统、字形缓存和整形缓存;调用通过全局锁串行访问。
struct TextEngine {
fonts: FontSystem,
code_family: Option<String>,
@@ -89,7 +76,6 @@ struct TextEngine {
}
impl TextEngine {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
fn new() -> Self {
let mut fonts = FontSystem::new();
// Select installed, readable families rather than assuming an OS font
@@ -128,12 +114,10 @@ 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(),
@@ -191,20 +175,17 @@ 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 {
@@ -220,7 +201,6 @@ 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,
@@ -320,7 +300,6 @@ 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;
-20
View File
@@ -1,6 +1,3 @@
//! 独立 2D 相机与可逆投影。视口尺寸属于布局;平移、缩放属于视图,不改变世界对象的逻辑位置。
//! project 为每次出现附加组合变换、裁剪和嵌套面板映射;绘制与命中共用这些结果。fit 根据有限内容边界调整指定相机。
//!
//! Reversible projections keep camera movement independent of layout.
pub mod spatial;
use crate::geometry::{Point, Rect};
@@ -9,27 +6,21 @@ 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,
@@ -37,7 +28,6 @@ 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")?)?;
@@ -58,7 +48,6 @@ 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,
@@ -138,14 +127,12 @@ 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,
@@ -155,18 +142,15 @@ 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(
@@ -176,7 +160,6 @@ impl Transform {
rect.height * self.zoom,
)
}
/// 将当前变换再放入外层变换,保持绘制与逆向命中的组合顺序一致。
pub fn then(self, outer: Self) -> Self {
Self {
zoom: self.zoom * outer.zoom,
@@ -184,7 +167,6 @@ impl Transform {
y: self.y * outer.zoom + outer.y,
}
}
/// 检查数值、几何和资源条件是否满足本类型约束,供上层拒绝非法输入。
pub fn valid(self) -> bool {
self.zoom.is_finite()
&& self.zoom > 0.0
@@ -196,7 +178,6 @@ 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]);
@@ -208,7 +189,6 @@ fn intersect(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
]
}
/// 遍历出现树,组合相机与父视图变换,建立裁剪和世界 UI 映射,最后重建屏幕命中索引。
pub fn project(
root: &crate::ui_bridge::UiNode,
frame: &mut crate::document::Frame,
-7
View File
@@ -1,20 +1,15 @@
//! 有界屏幕网格,用来减少普通二维命中和相机目标查询的候选。
//! 巨大对象和 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(),
@@ -71,7 +66,6 @@ impl Index {
}
result
}
/// 返回当前网格格子与后备集合中的候选,去重并恢复逆绘制顺序。
pub fn candidates(&self, point: Point, count: usize) -> Vec<usize> {
if self.count != count {
return (0..count).rev().collect();
@@ -85,7 +79,6 @@ impl Index {
output
}
}
/// 把屏幕坐标映射到有界整数网格坐标,处理负方向与极端数值。
fn cell(value: f32) -> i32 {
(value / 128.)
.floor()
-34
View File
@@ -1,7 +1,3 @@
//! 本机 HTTP API 入口:路由请求、限制输入大小、观察活动,并将执行工作送入串行 worker。
//! HTTP 线程不操作 VM;共享 NamespaceState 只保存最近完整发布文档和资源命令。提交序号用来拒绝已被较新 PUT/DELETE 取代的结果。
//! 关闭流程先请求运行时有界清理,再通过完成通知让原生事件循环退出。
//!
mod guide;
mod worker;
@@ -39,21 +35,18 @@ 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;
@@ -65,11 +58,9 @@ 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()?;
@@ -79,7 +70,6 @@ impl RuntimeControl {
result.clone()
}
}
/// worker 持有的关闭接收端、控制状态和 GUI 完成通知代理。
struct Shutdown {
receiver: Receiver<()>,
control: RuntimeControl,
@@ -87,7 +77,6 @@ struct Shutdown {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// 送入执行队列的请求类别,区分完整源码、持久输入、事件与重置。
enum RequestKind {
Event,
Code,
@@ -96,7 +85,6 @@ enum RequestKind {
ResetRepl,
}
/// 保留原 HTTP 请求与提交身份的执行工作,完成或失败后由 worker 回应。
struct BuildRequest {
request: ObservedRequest,
kind: RequestKind,
@@ -105,7 +93,6 @@ struct BuildRequest {
submission: u64,
}
/// 建立本模块的执行/服务上下文,并把后续推进交给返回的句柄或驱动。
pub fn start(
namespace: Arc<RwLock<NamespaceState>>,
proxy: EventLoopProxy<AppEvent>,
@@ -140,7 +127,6 @@ pub fn start(
}
#[cfg(test)]
/// 为本地监听服务建立请求处理线程,并把执行请求转交有界队列。
fn start_server(
server: Arc<Server>,
namespace: Arc<RwLock<NamespaceState>>,
@@ -156,7 +142,6 @@ fn start_server(
)
}
/// 启动带活动观察的 HTTP 服务,将请求状态变化通知管理界面。
fn start_server_observed(
server: Arc<Server>,
namespace: Arc<RwLock<NamespaceState>>,
@@ -298,7 +283,6 @@ fn start_server_observed(
}
// Capture only allowlisted headers: credentials and cookies never enter the inspector.
/// HTTP 请求的观察包装,在读取正文和响应时维护 Network 工具记录。
struct ObservedRequest {
inner: Request,
activity: Activity,
@@ -306,7 +290,6 @@ struct ObservedRequest {
notify: Notify,
}
impl ObservedRequest {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
fn new(inner: Request, activity: Activity, notify: Notify) -> Self {
let headers = inner
.headers()
@@ -339,12 +322,10 @@ 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();
@@ -367,19 +348,16 @@ 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>>,
@@ -432,7 +410,6 @@ fn handle(
let _ = request.respond(response);
}
/// 处理源码资源的 GET/DELETE 等请求,返回对应版本头或撤销运行时所有权。
fn code_request(
method: Method,
addressed_namespace: &str,
@@ -533,7 +510,6 @@ fn code_request(
}
}
/// 读取并检查请求体,分配提交身份,再把工作送进有界执行队列。
fn submit(
mut request: ObservedRequest,
addressed: &str,
@@ -600,7 +576,6 @@ fn submit(
}
}
/// 生成较旧请求已被新提交取代的响应,拒绝晚到结果覆盖当前文档。
fn superseded(submission: u64) -> Response<std::io::Cursor<Vec<u8>>> {
json(
StatusCode(409),
@@ -609,18 +584,15 @@ 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()
@@ -640,7 +612,6 @@ 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:#}") });
@@ -659,7 +630,6 @@ fn compile_error(error: anyhow::Error) -> Response<std::io::Cursor<Vec<u8>>> {
)
}
/// 生成统一 JSON 错误响应,使调用方能区分协议错误与成功结果。
fn error_response(
status: StatusCode,
message: impl Into<String>,
@@ -667,7 +637,6 @@ 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,
@@ -676,12 +645,10 @@ 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);
@@ -703,7 +670,6 @@ 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);
-3
View File
@@ -1,9 +1,6 @@
//! 从当前命名空间生成 /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
-3
View File
@@ -1,6 +1,3 @@
//! API 集成回归。通过实际本地 HTTP 请求检查队列、候选替换、状态码、版本和完整文档发布。
//! 测试服务使用独立监听地址和串行 worker;应结合断言理解成功输入、失败候选、过期事件和关闭的不同保留语义。
//!
use super::*;
use std::{
io::Write,
-24
View File
@@ -1,7 +1,3 @@
//! 唯一串行执行 VM 的 API worker。保留活跃运行时,另建 PUT 候选,并在候选完整成功后切换发布。
//! 输入、视口刷新与候选布局可以跨 worker 轮次挂起;中途只公布完整文档,不能让新输入进入未结束的只读调用。
//! 事件身份同时校验窗口、session、layout_version 和 Mount 能力。失败、替换与关闭分别处理,避免旧回调或旧命令泄漏到新 VM。
//!
//! Serial VM ownership, incremental requests, publication and shutdown.
use super::*;
@@ -17,7 +13,6 @@ type CandidateOutcome = std::thread::Result<
)>,
>;
/// 已执行业务部分但仍在等待展示刷新的输入元数据,避免恢复时重放回调。
pub(super) struct PendingInput {
epoch: u64,
id: u64,
@@ -25,20 +20,17 @@ 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> {
@@ -63,13 +55,11 @@ 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 {
@@ -100,7 +90,6 @@ impl Worker {
}
}
/// 处理当前无法继续正常交互的窗口关闭,撤销原生资源与相应运行时记录。
pub(super) fn close_inactive_window(
&mut self,
input: &crate::events::Input,
@@ -173,7 +162,6 @@ impl Worker {
)
}
/// 在没有前台输入的 worker 轮次推进尺寸和任务引起的展示刷新。
pub(super) fn refresh_viewport(
&mut self,
namespace: &Arc<RwLock<NamespaceState>>,
@@ -182,14 +170,12 @@ 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(&current);
@@ -230,7 +216,6 @@ impl Worker {
}
Ok((completed.or(resized), runtime.take_window_commands()?))
}));
// 计算结束后重新核对归属:期间若发生 PUT/DELETE,不能把旧域草稿发布出去。
let mut current = match namespace.write() {
Ok(current) => current,
Err(_) => return,
@@ -282,7 +267,6 @@ impl Worker {
}
}
/// 检查请求种类和归属,必要时先等已有刷新结束,再建立输入或候选任务。
pub(super) fn begin_request(
&mut self,
job: &BuildRequest,
@@ -607,7 +591,6 @@ impl Worker {
))
}
/// 恢复已挂起的请求,保持原 HTTP 响应和运行时身份直到最终完成。
pub(super) fn resume_request(
&mut self,
job: &BuildRequest,
@@ -624,7 +607,6 @@ impl Worker {
}
}
/// 继续同一 Console/事件输入的展示刷新,不再次执行其业务回调。
fn resume_input(
&mut self,
job: &BuildRequest,
@@ -689,7 +671,6 @@ impl Worker {
}
#[allow(clippy::too_many_arguments)]
/// 校验输入仍属于当前域,再一次性发布结果、元数据和原生窗口命令。
fn finish_input(
&mut self,
job: &BuildRequest,
@@ -787,7 +768,6 @@ 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 {
@@ -798,7 +778,6 @@ impl Worker {
}
}
/// 为完整 PUT 建立隔离候选,保留旧运行时直到新候选完整成功。
fn begin_program_request(
&mut self,
job: &BuildRequest,
@@ -851,7 +830,6 @@ impl Worker {
RequestProgress::Complete(self.finish_candidate(job, namespace, notify, id, result))
}
/// 推进候选的布局与视口收敛,检查其是否已被新提交撤销。
fn resume_candidate(
&mut self,
job: &BuildRequest,
@@ -899,7 +877,6 @@ impl Worker {
RequestProgress::Complete(self.finish_candidate(job, namespace, notify, id, result))
}
/// 候选成功才切换活跃 VM 和文档;失败保留先前已发布程序。
fn finish_candidate(
&mut self,
job: &BuildRequest,
@@ -948,7 +925,6 @@ impl Worker {
}
}
/// 处理隔离表达式或一次性文档编译;编译前后核对 submission,只发布仍有效的完整快照。
fn build(
job: &BuildRequest,
namespace: &Arc<RwLock<NamespaceState>>,
-78
View File
@@ -1,7 +1,3 @@
//! 把 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};
@@ -26,7 +22,6 @@ 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");
@@ -111,7 +106,6 @@ 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");
@@ -149,7 +143,6 @@ 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,
@@ -162,7 +155,6 @@ fn compile_plan(source: &SourceUnit, plan: &SourcePlan) -> Result<Vec<compiler::
)
}
/// 结合已有全局信息编译本轮输入,保留持久 Console 的名称解析上下文。
fn compile_plan_with_globals(
source: &SourceUnit,
plan: &SourcePlan,
@@ -208,39 +200,31 @@ 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,
@@ -254,7 +238,6 @@ pub struct ReplSession {
documents: std::collections::BTreeMap<u64, RenderDocument>,
refresh: Option<RefreshWork>,
}
/// 一次 Console/事件操作的标量显示结果与可选完整文档,避免直接跨线程暴露托管返回值。
pub struct ReplResult {
pub value: String,
pub document: Option<RenderDocument>,
@@ -265,7 +248,6 @@ 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,
@@ -276,7 +258,6 @@ impl ReplSession {
}
#[cfg(test)]
/// 测试用同步装载入口:建立完整候选并排空初始刷新,返回真实运行时与完成文档。
pub fn load_with_viewport(
source: &str,
namespace: &str,
@@ -294,7 +275,6 @@ impl ReplSession {
Ok((runtime, document))
}
/// 建立可由 worker 分轮推进的完整源码候选,初始布局不必在一次调用里排空。
pub fn load_incremental(
source: &str,
namespace: &str,
@@ -317,7 +297,6 @@ impl ReplSession {
Ok((runtime, document))
}
/// 建立隔离候选、安装宿主根并执行完整源码;候选尚未完成布局时不替换活跃程序。
fn prepare_program(
source: &str,
namespace: &str,
@@ -389,7 +368,6 @@ 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
@@ -398,7 +376,6 @@ impl ReplSession {
Ok(runtime)
}
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
pub fn new(id: u64) -> Result<Self> {
let mut candidate = Candidate::new()?;
let globals = candidate.vm.new_dict()?;
@@ -421,12 +398,10 @@ 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,
@@ -436,7 +411,6 @@ impl ReplSession {
self.execute_mode(source, namespace, Some(viewports))
}
/// 在当前真实 VM 中编译并执行新输入,再按同步或分片模式刷新展示;已发生的业务写入不会被源重放回滚。
fn execute_mode(
&mut self,
source: &str,
@@ -581,17 +555,14 @@ 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(),
@@ -600,7 +571,6 @@ impl ReplSession {
Ok(())
}
/// 在业务变化后开始或继续展示刷新,保证后续输入不会进入未结束的只读视觉调用。
fn refresh_after_mutation(
&mut self,
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
@@ -618,7 +588,6 @@ impl ReplSession {
})
}
/// 同步排空当前刷新状态机,直到全窗口收敛或出现错误。
fn refresh_document(&mut self) -> Result<Option<RenderDocument>> {
loop {
match self.poll_refresh(false)? {
@@ -630,7 +599,6 @@ 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(),
@@ -686,9 +654,7 @@ 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();
@@ -697,7 +663,6 @@ 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);
@@ -710,7 +675,6 @@ impl ReplSession {
return Ok(true);
};
self.bridge.select_window(id)?;
// 已进入 paint 的窗口直接恢复原绘制续体,不重放业务回调或布局。
if let Some(paint) = work.paint.take() {
let PendingPaint {
window,
@@ -722,7 +686,6 @@ 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)?
@@ -741,7 +704,6 @@ impl ReplSession {
}
crate::ui_bridge::LayoutProgress::Complete(frame) => *frame,
};
// 阶段四:几何完成后建立候选文档,再运行 paint;这里尚未向 GUI 发布。
let mut document = RenderDocument {
windows: Default::default(),
source: self.source.clone(),
@@ -770,7 +732,6 @@ impl ReplSession {
Self::accept_window_paint(work, id, document, progress)
}
/// 接收指定窗口的绘制进度,将挂起 Work 保留或把完整文档存入本轮结果。
fn accept_window_paint(
work: &mut RefreshWork,
id: u64,
@@ -796,7 +757,6 @@ 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()?;
@@ -836,7 +796,6 @@ impl ReplSession {
}
#[cfg(test)]
/// 同步更新多个窗口尺寸,复用同一运行时并完成必要刷新。
pub fn resize_windows(
&mut self,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
@@ -844,7 +803,6 @@ impl ReplSession {
self.resize_windows_incremental(viewports, true)
}
/// 用最新视口替换旧目标,显式取消未完成布局,同时保留已完成窗口结果和已发生的业务效果。
fn retarget_refresh(
&mut self,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
@@ -888,7 +846,6 @@ impl ReplSession {
Ok(())
}
/// 把各窗口最新尺寸送入同一运行时;视口更新可跨轮次挂起,也可由调用方同步排空。
pub fn resize_windows_incremental(
&mut self,
viewports: &std::collections::BTreeMap<u64, crate::layout::Size>,
@@ -943,7 +900,6 @@ impl ReplSession {
}
}
/// 在空闲轮次推进一个刷新步骤,完成时执行统一结果收尾。
fn advance_idle_refresh(&mut self) -> Result<Option<RenderDocument>> {
let result = self.poll_refresh(true).map(|progress| match progress {
RefreshProgress::Pending => None,
@@ -952,7 +908,6 @@ impl ReplSession {
self.finish_refresh_update(result)
}
/// 统一处理刷新失败、未发布状态清理和运行时引用回收。
fn finish_refresh_update(
&mut self,
result: Result<Option<RenderDocument>>,
@@ -970,13 +925,11 @@ 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>,
@@ -984,7 +937,6 @@ impl ReplSession {
self.poll_tasks_mode(Some(viewports))
}
/// 按本轮预算交付已完成任务或推进协程,再对其业务变化执行相同的展示刷新流程。
fn poll_tasks_mode(
&mut self,
viewports: Option<&std::collections::BTreeMap<u64, crate::layout::Size>>,
@@ -1021,14 +973,12 @@ 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>,
@@ -1050,7 +1000,6 @@ 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() {
@@ -1068,7 +1017,6 @@ impl ReplSession {
Ok(())
}
/// 有界关闭任务、挂载和窗口,释放运行时根,并报告清理结果。
pub fn shutdown(&mut self) -> Result<(usize, usize, usize)> {
self.cancel_pending_refresh()?;
let callbacks = !self.faulted;
@@ -1076,7 +1024,6 @@ 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.
@@ -1089,28 +1036,23 @@ 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,
@@ -1119,7 +1061,6 @@ impl ReplSession {
self.dispatch_mode(input, Some(viewports))
}
/// 在正确窗口上下文执行输入回调,再刷新展示;故障时停止受影响域并清理未完成工作。
fn dispatch_mode(
&mut self,
input: &crate::events::Input,
@@ -1166,13 +1107,11 @@ 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;
@@ -1180,7 +1119,6 @@ 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(),
@@ -1191,7 +1129,6 @@ fn value_repr(vm: &Vm, value: &Value) -> Result<String> {
})
}
/// 拥有 VM、全局字典与宿主保留根;关闭时按顺序释放,失败也必须收尾。
struct Candidate {
vm: Vm,
roots: Vec<ObjectRef>,
@@ -1199,7 +1136,6 @@ struct Candidate {
}
impl Candidate {
/// 创建独立 VM,先不保留任何宿主根或全局字典;后续安装失败也由该候选负责清理。
fn new() -> Result<Self> {
Ok(Self {
vm: Vm::new()?,
@@ -1208,7 +1144,6 @@ impl Candidate {
})
}
/// 释放候选拥有的宿主根并清理 VM;通过显式关闭保证错误也能被报告。
fn close(&mut self) -> Result<()> {
if let Some(globals) = self.globals.take() {
self.vm.dict_clear(globals)?;
@@ -1225,7 +1160,6 @@ 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.
@@ -1233,7 +1167,6 @@ impl Drop for Candidate {
}
}
/// 把 VM 错误与当前源位置/调用栈组合成用户可读的诊断。
pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error {
let locations = vm
.last_traceback()
@@ -1254,7 +1187,6 @@ pub(crate) fn runtime_error(vm: &Vm, error: VmError) -> anyhow::Error {
}
#[derive(Default)]
/// 编译前的语法节点数量/嵌套预算,限制输入结构复杂度。
struct AstBudget {
depth: usize,
nodes: usize,
@@ -1262,7 +1194,6 @@ struct AstBudget {
impl Visitor for AstBudget {
type Error = anyhow::Error;
/// 更新源结构遍历的深度/数量预算,拒绝超出允许范围的语法树。
fn enter_node(&mut self, _span: Span) -> Result<()> {
self.depth += 1;
self.nodes += 1;
@@ -1271,13 +1202,11 @@ impl Visitor for AstBudget {
}
Ok(())
}
/// 离开当前语法节点时归还嵌套深度;累计节点数保留,不能通过退出子树绕过总量预算。
fn leave_node(&mut self) {
self.depth -= 1;
}
}
/// 把输入语法结构转换为 Sources 工具使用的 CodeNode 层次,保存原始源码位置。
fn architecture(
source: &SourceUnit,
plan: &SourcePlan,
@@ -1317,7 +1246,6 @@ 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];
@@ -1329,7 +1257,6 @@ fn line_starts(source: &str) -> Vec<usize> {
starts
}
/// 构造 Sources 语法投影所需的源码与定位辅助状态。
struct Architecture {
line_starts: Vec<usize>,
next_id: usize,
@@ -1337,7 +1264,6 @@ 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;
@@ -1352,7 +1278,6 @@ impl Architecture {
}
}
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
fn declaration(&mut self, declaration: &Decl) -> CodeNode {
match &declaration.kind {
DeclKind::Function(function) => self.function(function),
@@ -1368,7 +1293,6 @@ impl Architecture {
}
}
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
fn function(&mut self, function: &frontend::ast::Function) -> CodeNode {
let children = function
.body
@@ -1384,7 +1308,6 @@ impl Architecture {
)
}
/// 将相应语法结构投影为带源码范围的 CodeNode,供 Sources 展示和定位。
fn statement(&mut self, statement: &Stmt) -> CodeNode {
match &statement.kind {
StmtKind::MemberAssignment(assignment) => {
@@ -1428,7 +1351,6 @@ 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![]),
+101 -4
View File
@@ -1,13 +1,110 @@
//! 运行时集成回归与示例验收。测试经真实 Aster 前端、编译器、VM 和展示协议执行,不是手工伪造布局结果。
//! 覆盖共享对象/独立 Mount、源覆盖、绑定、窗口、任务、取消、二维/三维投影及像素。局部辅助函数负责取统计值、收敛刷新和构造可重复输入。
//! 阅读时先看每个测试提交的 Endor 程序,再看后续变更与最终断言;编译成功、回调成功、文档发布成功是不同检查点。
//!
use super::*;
use crate::document::{NamespaceState, RenderCommand};
const EXAMPLE: &str = include_str!("../../../examples/hello.endor");
const PAGE: &str = include_str!("../../../examples/page.endor");
/// Opt-in release benchmark; run alone to avoid test-thread interference.
/// cargo test --release --locked scene_interaction_performance -- --ignored --nocapture --test-threads=1
#[test]
#[ignore]
fn scene_interaction_performance() {
use std::time::Instant;
fn summary(mut values: Vec<f64>) -> String {
values.sort_by(f64::total_cmp);
format!(
"mean={:.3} p50={:.3} p95={:.3}ms",
values.iter().sum::<f64>() / values.len() as f64,
values[values.len() / 2],
values[(values.len() * 95).div_ceil(100) - 1]
)
}
for (name, source) in [
("world-ui", include_str!("../../../examples/world-ui.endor")),
("scene3d", include_str!("../../../examples/scene3d.endor")),
] {
for dpi in [1., 2.] {
let (mut session, mut document) = ReplSession::load_with_viewport(
source,
"app.main",
1,
988,
Some(crate::layout::Size {
width: 1024.,
height: 800.,
}),
)
.unwrap();
let (width, height) = ((1024. * dpi) as u32, (800. * dpi) as u32);
let mut pixels = vec![0; (width * height) as usize];
let mut updates = Vec::new();
let mut draws = Vec::new();
let mut cached = Vec::new();
for sample in 0..33 {
let target = document
.frame
.mounts
.iter()
.find(|mount| mount.scene3d_viewport)
.unwrap();
let input = crate::events::Input {
window: 1,
session: document.frame.session,
layout_version: document.frame.layout_version,
mount: target.id,
kind: "scene-orbit".into(),
point: Some([target.x + target.width / 2., target.y + target.height / 2.]),
delta: Some([1., 0.]),
press_point: None,
tab_drop: None,
screen_position: None,
};
let start = Instant::now();
document = session.dispatch(&input).unwrap().document.unwrap();
let update = start.elapsed().as_secs_f64() * 1000.;
let list = document.display_list(dpi).unwrap();
let start = Instant::now();
crate::software_renderer::SoftwareRenderer::render(
&list,
&mut pixels,
width,
height,
)
.unwrap();
let draw = start.elapsed().as_secs_f64() * 1000.;
let start = Instant::now();
crate::software_renderer::SoftwareRenderer::render(
&list,
&mut pixels,
width,
height,
)
.unwrap();
if sample >= 3 {
updates.push(update);
draws.push(draw);
cached.push(start.elapsed().as_secs_f64() * 1000.);
}
}
println!(
"PERF {name} dpi={dpi} update {} changed_draw {} cached_draw {}",
summary(updates),
summary(draws),
summary(cached)
);
for stat in ["measured", "arranged", "snapshotVisited"] {
println!(
"PERFSTAT {name} {stat}={}",
session
.execute(&format!("ui.stats().{stat}"), "app.main")
.unwrap()
.value
);
}
}
}
}
#[test]
fn actual_mount_version_reads_still_bypass_measurement_cache() {
let (mut session, initial) = ReplSession::load(
-12
View File
@@ -1,6 +1,3 @@
//! 区分完整 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.
@@ -10,14 +7,12 @@ 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),
@@ -26,7 +21,6 @@ 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));
@@ -84,13 +78,11 @@ 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 {
@@ -98,7 +90,6 @@ 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 {
@@ -129,7 +120,6 @@ 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 {
@@ -152,7 +142,6 @@ 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);
@@ -168,7 +157,6 @@ 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 {
-39
View File
@@ -1,6 +1,3 @@
//! 托管事件与订阅的传播实现。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};
@@ -10,12 +7,10 @@ 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,
@@ -35,7 +30,6 @@ pub fn validate_handler(
}
#[derive(Debug, Clone, Default)]
/// 本次传播的托管事件状态,包含目标、当前阶段和阻止默认/停止传播控制。
pub struct Event {
dispatch: Option<WeakObjectRef>,
group: usize,
@@ -44,45 +38,36 @@ 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);
@@ -92,27 +77,22 @@ 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,
@@ -122,12 +102,10 @@ 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"));
@@ -201,21 +179,17 @@ 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"));
@@ -238,7 +212,6 @@ impl NativeCallable for Constructor {
}
}
/// 建立本次分发拥有的托管 Event,保存目标、输入与传播控制状态。
pub fn new_event(
cx: &mut NativeContext<'_>,
definition: ObjectRef,
@@ -258,23 +231,19 @@ 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>,
@@ -323,14 +292,12 @@ pub fn install(
}
#[derive(Debug)]
/// 本次递送选择的对象方法或显式订阅处理器。
enum Handler {
Token(ObjectRef),
Legacy(ObjectRef),
ObjectMethod,
}
#[derive(Debug)]
/// 事件传播过程中将要调用的一项处理器及其阶段/目标上下文。
struct Delivery {
event: ObjectRef,
owner: ObjectRef,
@@ -340,12 +307,10 @@ 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) {
@@ -398,15 +363,12 @@ 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;
@@ -421,7 +383,6 @@ 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,
-13
View File
@@ -1,7 +1,3 @@
//! 原生窗口输入到执行 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::{
@@ -18,7 +14,6 @@ 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,
@@ -27,7 +22,6 @@ pub struct TabDrop {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
/// 原生或 HTTP 输入的纯数据报文;身份与坐标必须同时通过 worker 和能力校验。
pub struct Input {
#[serde(default = "default_window")]
pub window: u64,
@@ -47,13 +41,11 @@ 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");
@@ -162,10 +154,8 @@ 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>>,
@@ -191,7 +181,6 @@ impl Client {
})?;
Ok(Self(sender))
}
/// 通过有界原生输入客户端或本地 HTTP 发送纯数据事件;过期版本按既定协议处理。
pub fn send(&self, input: Input) -> bool {
self.0.try_send(input).is_ok()
}
@@ -200,7 +189,6 @@ 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(),
@@ -240,7 +228,6 @@ 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 =
-21
View File
@@ -1,7 +1,3 @@
//! 后台文件/网络 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::{
@@ -25,24 +21,20 @@ 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()
@@ -54,23 +46,19 @@ 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);
@@ -78,7 +66,6 @@ impl Drop for Lease {
self.shared.active.fetch_sub(1, Ordering::AcqRel);
}
}
/// 线程池队列中的纯数据工作,关联弱任务身份、控制和共享完成表。
struct Job {
request: Request,
task: WeakObjectRef,
@@ -86,7 +73,6 @@ struct Job {
shared: Arc<Shared>,
}
impl Provider {
/// 检查并发额度并取得任务 Lease,再把纯数据工作放入有界线程池队列。
pub fn submit(
&self,
task: WeakObjectRef,
@@ -115,7 +101,6 @@ 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
@@ -126,7 +111,6 @@ impl Provider {
.collect())
}
}
/// 懒初始化共享工作线程和有界任务队列;初始化失败反馈给提交方。
fn pool() -> Result<&'static SyncSender<Job>, &'static str> {
static POOL: OnceLock<Result<SyncSender<Job>, String>> = OnceLock::new();
POOL.get_or_init(|| {
@@ -166,7 +150,6 @@ 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() {
@@ -195,7 +178,6 @@ 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;
@@ -211,10 +193,8 @@ 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 {
@@ -223,7 +203,6 @@ 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());
-2
View File
@@ -1,5 +1,3 @@
//! 运行时模块入口。VM 生命周期、API、源程序组织、事件与异步任务在这里组合。
//!
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
pub(crate) mod api;
pub(crate) mod endor_host;
-40
View File
@@ -1,7 +1,3 @@
//! 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::{
@@ -28,7 +24,6 @@ const MAX_READY: usize = 64;
type Index = Arc<Mutex<Vec<WeakObjectRef>>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// 任务对源代码可见的生命周期状态,完成/取消/失败各有不同 await 语义。
enum State {
Pending,
Running,
@@ -37,7 +32,6 @@ enum State {
Failed,
}
impl State {
/// 返回内部枚举或对象对应的稳定源名称。
fn name(self) -> &'static str {
match self {
Self::Pending => "pending",
@@ -50,7 +44,6 @@ impl State {
}
#[derive(Debug, Clone)]
/// 托管异步对象,保存 owner/key、结果与真正拥有的回调/协程/等待引用。
struct Task {
owner: WeakObjectRef,
key: String,
@@ -67,19 +60,15 @@ 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),
@@ -97,7 +86,6 @@ impl ObjectPayload for Task {
)),
}
}
/// 枚举本对象真正拥有的托管强引用,供 Aster 引用追踪和循环回收使用。
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
if let Some(callback) = self.callback {
visit(callback);
@@ -113,18 +101,15 @@ 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>() {
@@ -147,7 +132,6 @@ fn scope(store: &ObjectStore, owner: ObjectRef) -> Result<Option<ObjectRef>, VmE
Err(error) => Err(error),
}
}
/// 为可拥有任务的对象安装作用域,真实引用仍由托管图追踪。
fn install_scope(
cx: &mut NativeContext<'_>,
owner: ObjectRef,
@@ -171,7 +155,6 @@ 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>() {
@@ -211,7 +194,6 @@ fn eligible(store: &ObjectStore, owner: ObjectRef, desktop: ObjectRef) -> Result
}
Ok(true)
}
/// 检查弱索引对应的任务是否仍是 owner/key 当前登记的那一项。
fn current(
store: &ObjectStore,
owner: ObjectRef,
@@ -228,7 +210,6 @@ 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(());
@@ -252,7 +233,6 @@ pub fn cancel_owner(cx: &mut NativeContext<'_>, owner: ObjectRef) -> Result<(),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// 当前宿主 API 的操作分类,分派代码负责参数校验与状态更新。
enum Operation {
Timer,
Sleep,
@@ -261,7 +241,6 @@ enum Operation {
Spawn,
}
#[derive(Debug)]
/// 创建定时/I/O/协作任务时的原生调用适配器。
struct After {
operation: Operation,
provider: crate::io_tasks::Provider,
@@ -272,16 +251,13 @@ 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"));
@@ -483,7 +459,6 @@ impl NativeCallable for After {
}
#[derive(Debug, Clone, Copy)]
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
enum Query {
Cancel,
Status,
@@ -492,23 +467,19 @@ 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"));
@@ -590,7 +561,6 @@ impl NativeCallable for Method {
}
}
/// 弱任务索引、公平调度位置和 provider,索引不会单独把任务保活。
pub struct Scheduler {
index: Index,
runnable: Arc<AtomicBool>,
@@ -599,18 +569,15 @@ 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
@@ -764,7 +731,6 @@ impl Scheduler {
self.prune(vm)?;
Ok(work)
}
/// 取消当前域全部任务,切断回调、协程和等待交付资格。
pub fn cancel_all(&self, vm: &mut Vm) -> Result<usize> {
let snapshot = self
.index
@@ -787,7 +753,6 @@ impl Scheduler {
self.runnable.store(false, Ordering::Release);
Ok(count)
}
/// 移除已释放的弱任务索引,索引本身不让任务或其 owner 存活。
fn prune(&self, vm: &mut Vm) -> Result<()> {
let mut index = self
.index
@@ -824,7 +789,6 @@ impl Scheduler {
Ok(())
}
}
/// 沿等待链检查任务依赖环,防止 await 永远无法被满足。
fn cyclic_wait(vm: &Vm, source: ObjectRef, mut target: ObjectRef) -> Result<bool> {
let mut seen = std::collections::HashSet::new();
loop {
@@ -841,7 +805,6 @@ 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 {
@@ -934,7 +897,6 @@ 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());
@@ -942,7 +904,6 @@ 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) {
@@ -973,7 +934,6 @@ fn finish(vm: &mut Vm, reference: ObjectRef, state: State) -> Result<()> {
Ok(())
}
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
pub fn install(
vm: &mut Vm,
roots: &mut Vec<ObjectRef>,
-8
View File
@@ -1,13 +1,9 @@
//! 构造共享 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>,
@@ -40,7 +36,6 @@ 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,
@@ -68,7 +63,6 @@ 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,
@@ -100,7 +94,6 @@ pub fn collect_and_continue(
driver(cx, instructions, constants, name, desktop)
}
/// 建立承载整个步骤序列的托管函数,使续体可以在同一个 VM 帧中恢复。
fn driver(
cx: &mut NativeContext<'_>,
instructions: Vec<Instruction>,
@@ -119,7 +112,6 @@ 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);
-9
View File
@@ -1,6 +1,3 @@
//! 有界 HTTP 活动记录。保存请求/响应的可显示预览供 Network 工具读取,不负责执行请求。
//! 正文预览和允许显示的头部受限制,避免活动面板成为无限日志或暴露不必要数据的通道。
//!
//! Bounded observation of this process's local HTTP API, never external traffic.
use std::{
collections::VecDeque,
@@ -13,7 +10,6 @@ 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,
@@ -30,13 +26,11 @@ 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;
@@ -69,13 +63,11 @@ 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);
@@ -86,7 +78,6 @@ impl ActivityLog {
}
}
/// 截取可显示的受限文本预览,避免日志无限保留完整大正文。
pub fn preview(value: &str) -> String {
if value.len() <= BODY_PREVIEW {
return value.to_owned();
-34
View File
@@ -1,7 +1,3 @@
//! winit 原生应用生命周期和窗口事件入口。这里创建窗口、发布逻辑视口、转发输入,并把不可变文档画到原生表面。
//! 窗口线程不执行 Endor 业务回调;共享状态通过锁读取,实际 VM 更新由 runtime worker 串行完成。
//! 侧栏、页面与开发工具有各自命中区域。拖动捕获必须保留最初的视口目标,不能随指针经过其他组件而跳转。
//!
use std::{
num::NonZeroU32,
sync::{Arc, RwLock},
@@ -28,7 +24,6 @@ use crate::{
};
#[derive(Clone, Debug)]
/// worker 到原生事件循环的通知,携带更新/错误/退出状态而不是可执行 VM 引用。
pub enum AppEvent {
RuntimeFailed { message: String },
DocumentUpdated,
@@ -36,7 +31,6 @@ pub enum AppEvent {
ConsoleFailed { message: String, source: String },
}
/// 一个原生窗口的宿主状态;主窗口还协调子窗口资源,共用同一运行时与输入通道。
pub struct TesseraApp {
resource: u64,
control: crate::api::RuntimeControl,
@@ -69,7 +63,6 @@ pub struct TesseraApp {
}
impl TesseraApp {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
pub fn new(
namespace: Arc<RwLock<NamespaceState>>,
activity: Activity,
@@ -86,7 +79,6 @@ impl TesseraApp {
))
}
/// 建立窗口宿主状态并复用权威输入客户端,避免子窗口使用滞后的独立事件队列。
fn with_event_client(
namespace: Arc<RwLock<NamespaceState>>,
activity: Activity,
@@ -126,7 +118,6 @@ impl TesseraApp {
}
}
/// 将已发布窗口文档与原生资源对齐;session 更换时丢弃旧域的子窗口。
fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
if self.resource != crate::window::MAIN_WINDOW {
return;
@@ -192,7 +183,6 @@ impl TesseraApp {
}
}
/// 只应用当前运行时身份的窗口命令;执行前从共享队列取走,避免重复执行。
fn apply_window_commands(&mut self) {
let Ok(mut namespace) = self.namespace.write() else {
return;
@@ -246,7 +236,6 @@ impl TesseraApp {
self.publish_viewport();
}
/// 将真实原生窗口尺寸换算成扣除侧栏的逻辑页面尺寸,并发布给 worker。
fn publish_viewport(&self) {
let Some(window) = &self.window else {
return;
@@ -291,14 +280,12 @@ 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;
@@ -326,7 +313,6 @@ impl TesseraApp {
self.request_redraw();
}
/// 接收完整文档更新,协调子窗口和原生命令,再更新输入焦点并请求重绘。
fn document_updated(&mut self, event_loop: &ActiveEventLoop) {
self.reconcile_windows(event_loop);
self.apply_window_commands();
@@ -357,7 +343,6 @@ 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
@@ -501,7 +486,6 @@ impl TesseraApp {
self.request_redraw();
}
/// 判断指针是否被侧栏或管理面板覆盖,阻止一次点击同时落入页面。
fn guest_covered(&self, point: Point) -> bool {
if self.sidebar.covers(point) {
return true;
@@ -517,7 +501,6 @@ impl TesseraApp {
.contains(point)
}
/// 排除宿主管理区域后,把原生指针转换到页面使用的坐标系。
fn guest_point(&self) -> Option<Point> {
if self.guest_covered(self.pointer) {
return None;
@@ -535,7 +518,6 @@ 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()?;
@@ -558,7 +540,6 @@ 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;
@@ -596,7 +577,6 @@ impl TesseraApp {
}
}
/// 处理侧栏展开、工具入口和页面管理,尺寸变化还需重新发布可用视口。
fn sidebar_action(&mut self, action: crate::sidebar::Action) {
use crate::sidebar::Action;
match action {
@@ -639,7 +619,6 @@ impl TesseraApp {
self.request_redraw();
}
/// 在手势开始时捕获具体相机视口 ID,后续拖动保持同一目标。
fn begin_pan(&mut self) -> bool {
self.pan_viewport = None;
let Some(point) = self.guest_point() else {
@@ -653,7 +632,6 @@ 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;
@@ -746,7 +724,6 @@ 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();
@@ -754,7 +731,6 @@ impl TesseraApp {
}
}
/// 把页面拥有焦点时的键盘动作转换为托管事件,保留按下/释放配对。
fn handle_guest_key(&mut self, event: &winit::event::KeyEvent) -> bool {
let Key::Named(key) = &event.logical_key else {
return false;
@@ -824,7 +800,6 @@ impl TesseraApp {
false
}
/// 按平台窗口属性创建原生窗口、软件表面和输入法支持,再公布实际逻辑视口。
fn create_native(&mut self, event_loop: &ActiveEventLoop) -> anyhow::Result<()> {
if self.window.is_some() {
return Ok(());
@@ -872,7 +847,6 @@ impl TesseraApp {
Ok(())
}
/// 停止新的交互并请求运行时清理;原生事件循环等待完成通知后退出。
fn request_exit(&mut self) {
if self.exit_requested {
return;
@@ -889,7 +863,6 @@ impl TesseraApp {
}
}
/// 把原生应用记录的最终错误反馈给 main 的退出结果。
pub fn exit_result(&self) -> anyhow::Result<()> {
if let Some(error) = &self.native_error {
anyhow::bail!("{error}");
@@ -897,7 +870,6 @@ impl TesseraApp {
Ok(())
}
/// 包装一次实际绘制,统一记录故障并避免对同一错误反复发送通知。
fn redraw(&mut self) {
#[cfg(target_os = "macos")]
if let Some(window) = self.window.as_ref() {
@@ -919,7 +891,6 @@ impl TesseraApp {
}
}
/// 读取完整发布文档,生成整窗显示列表,写入原生像素表面并呈现;错误交由外层记录。
fn try_redraw(&mut self) -> anyhow::Result<()> {
let guest_cursor = self.guest_hit().is_some()
|| self
@@ -985,7 +956,6 @@ 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.
@@ -1003,10 +973,8 @@ 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
@@ -1030,7 +998,6 @@ 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;
@@ -1062,7 +1029,6 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
}
}
/// 处理原生尺寸、输入、焦点和关闭事件;先判断宿主控件,再转发页面事件。
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
-6
View File
@@ -1,10 +1,6 @@
//! 把 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")
@@ -12,7 +8,6 @@ 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")]
{
@@ -31,7 +26,6 @@ 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")]
{
-40
View File
@@ -1,7 +1,3 @@
//! Console/Sources/Network/Settings 的宿主交互协调与绘制状态。
//! 工具标签有独立焦点、滚动和文本选择状态;源程序与结果来自当前发布文档,提交动作通过本地 API 进入 worker。
//! 输入法预编辑、光标、选择和控件捕获分别处理,避免把界面操作直接当成业务模型写入。
//!
//! Native developer workspace. Coordinates and scroll offsets are logical pixels.
use crate::{
activity::{Activity, Exchange},
@@ -36,7 +32,6 @@ 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,
@@ -45,7 +40,6 @@ pub enum Tab {
Settings,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
/// 管理面板中可区分的输入/列表/详情区域。
pub enum Region {
Transcript = 0,
Tree,
@@ -54,13 +48,11 @@ 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,
@@ -68,7 +60,6 @@ pub enum Focus {
Custom,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
/// 工具控件激活后执行的管理动作,避免在绘制时产生业务副作用。
pub enum Action {
ClearConsole,
ClearNetwork,
@@ -84,13 +75,11 @@ 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),
@@ -98,13 +87,11 @@ 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 {
@@ -115,7 +102,6 @@ impl Workspace {
});
Self { states, active: 0 }
}
/// 选择一个工具页并恢复它各自保留的输入、焦点和滚动状态。
pub fn select(&mut self, name: Option<&str>) {
let next = match name {
Some("Sources") => 1,
@@ -134,20 +120,17 @@ 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,
@@ -184,7 +167,6 @@ pub struct DeveloperToolsState {
draft: String,
}
impl Default for DeveloperToolsState {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
open: false,
@@ -224,7 +206,6 @@ impl Default for DeveloperToolsState {
}
}
#[derive(Clone, Debug, Default)]
/// 本帧工具面板与子区域的几何,用于绘制和原生事件命中。
pub struct DeveloperToolsLayout {
pub panel: Rect,
pub widgets: Vec<Target<Action>>,
@@ -235,7 +216,6 @@ 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;
@@ -270,13 +250,11 @@ 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
@@ -286,7 +264,6 @@ 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 {
@@ -309,18 +286,15 @@ 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;
@@ -359,7 +333,6 @@ impl DeveloperToolsState {
}
}
}
/// 执行已确认激活的管理控件动作,区分工具切换、源码操作和设置。
fn activate(
&mut self,
action: Option<Action>,
@@ -450,7 +423,6 @@ impl DeveloperToolsState {
None => {}
}
}
/// 结束捕获并按同一目标/启用条件决定是否执行操作。
pub fn release(&mut self, layout: &DeveloperToolsLayout, p: Point, activity: &Activity) {
self.pointer = p;
self.interaction.pointer_moved(p, &layout.widgets);
@@ -461,7 +433,6 @@ impl DeveloperToolsState {
self.activate(action, layout, p, activity);
}
}
/// 按工具控件焦点处理 Tab、确认等管理键盘交互。
pub fn keyboard_control(
&mut self,
event: &winit::event::KeyEvent,
@@ -520,7 +491,6 @@ impl DeveloperToolsState {
}
false
}
/// 更新工具内悬停、拖动、选择或分隔条位置。
pub fn move_pointer(
&mut self,
p: Point,
@@ -565,7 +535,6 @@ impl DeveloperToolsState {
None => {}
}
}
/// 将滚轮作用到当前工具的可滚动区域,钳制本工具偏移。
pub fn wheel(&mut self, layout: &DeveloperToolsLayout, p: Point, dx: f32, dy: f32) {
if let Some((_, r, _, _)) = layout
.regions
@@ -581,7 +550,6 @@ impl DeveloperToolsState {
}
}
}
/// 在当前选择/光标处插入输入文本,并更新光标与可见范围。
pub fn insert(&mut self, value: &str) {
match self.focus {
Some(Focus::Input) => {
@@ -623,11 +591,9 @@ 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];
@@ -642,7 +608,6 @@ 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);
@@ -657,7 +622,6 @@ 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 {
@@ -828,7 +792,6 @@ impl DeveloperToolsState {
}
self.reveal_cursor();
}
/// 初始化内置工具对应的页面和展示状态,避免重复建立同一组工具。
pub fn initialize_tools(&mut self, name: &str) {
let name = name.to_owned();
let proxy = self.proxy.clone();
@@ -860,11 +823,9 @@ 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;
@@ -906,7 +867,6 @@ impl DeveloperToolsState {
}
}
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
pub fn build(
width: u32,
height: u32,
-4
View File
@@ -1,9 +1,5 @@
//! 绘制 Console 输入、结果历史和对应命中区域。
//! 这里消费工具状态并生成显示列表;用户程序的编译执行由提交路径和 worker 完成。
//!
use super::*;
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
pub(super) fn build(
p: &mut Painter,
state: &mut DeveloperToolsState,
-4
View File
@@ -1,9 +1,5 @@
//! 绘制已观察到的 HTTP 活动、请求列表与详情。
//! 筛选、选择和滚动是工具视图状态;展示一条请求不会重发该请求。
//!
use super::*;
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
pub(super) fn build(
p: &mut Painter,
state: &mut DeveloperToolsState,
-18
View File
@@ -1,23 +1,16 @@
//! 开发工具画面的绘制辅助:文字行、按钮、分隔线和滚动条。
//! 绘制命令与可交互区域同步登记,保证屏幕上的管理控件能用同一几何命中。
//!
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,
@@ -26,17 +19,14 @@ 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,
@@ -142,7 +132,6 @@ 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,
@@ -181,7 +170,6 @@ impl Painter {
}
}
#[derive(Clone)]
/// 一行可绘制、可关联源码位置的工具文本。
pub(super) struct Line {
pub(super) value: String,
pub(super) color: Color,
@@ -190,13 +178,11 @@ 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(),
@@ -207,7 +193,6 @@ impl Line {
}
}
}
/// 按行绘制工具文本,返回后续布局需要的占用范围。
pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
out.extend(
value
@@ -216,7 +201,6 @@ 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" {
@@ -279,7 +263,6 @@ 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 {
@@ -287,7 +270,6 @@ 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,
-12
View File
@@ -1,6 +1,3 @@
//! 多行文本选择和光标导航。选择端点映射到合法字符边界,避免把 UTF-8 字节中间当成一个字符。
//! 工具只保留当前文本行和选择范围;复制操作取得的是所选内容,不是整份运行时状态。
//!
//! Read-only, grapheme-aware source selection, independent of console editing.
use crate::{
geometry::Point,
@@ -9,7 +6,6 @@ use crate::{
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Default)]
/// 行文本、锚点与选择端点,既供鼠标拖选也供键盘导航使用。
pub(super) struct SourceSelection {
lines: Vec<String>,
anchor: (usize, usize),
@@ -17,7 +13,6 @@ 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();
@@ -26,7 +21,6 @@ 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));
@@ -45,17 +39,14 @@ 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,
@@ -118,7 +109,6 @@ impl SourceSelection {
}
Some(row)
}
/// 返回排序后的有效选择范围,隐藏拖动方向差异。
fn range(&self, all: bool) -> ((usize, usize), (usize, usize)) {
if all {
return (
@@ -131,7 +121,6 @@ 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
@@ -146,7 +135,6 @@ 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 {
-4
View File
@@ -1,9 +1,5 @@
//! 绘制宿主设置页面和主题选择区域。
//! 这里只投影当前设置和交互目标,平台原生窗口行为由对应系统适配层负责。
//!
use super::*;
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
pub(super) fn build(p: &mut Painter, state: &mut DeveloperToolsState, viewport: Rect) {
let x = viewport.x;
let w = viewport.width;
-4
View File
@@ -1,9 +1,5 @@
//! 绘制 AST、当前源码快照和云文件视图。
//! 当前快照是发布源的只读投影;云文件 provider 的可用性与打开状态分别反馈,不把查看代码当成执行代码。
//!
use super::*;
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
pub(super) fn build(
p: &mut Painter,
state: &mut DeveloperToolsState,
-2
View File
@@ -1,5 +1,3 @@
//! 原生宿主模块入口。窗口、管理控件、系统适配和开发工具状态与用户业务界面分开维护。
//!
// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
pub(crate) mod activity;
pub(crate) mod app;
-5
View File
@@ -1,6 +1,3 @@
//! 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};
@@ -11,7 +8,6 @@ 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;
@@ -53,7 +49,6 @@ 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;
-9
View File
@@ -1,6 +1,3 @@
//! 宿主管理工具和页面列表的唯一侧栏。展开时占据页面左侧空间,折叠后只保留展开按钮区域。
//! 页面标题和身份来自发布帧;实际切换/关闭通过带版本的事件进入运行时,不在绘制时修改业务对象。
//!
//! 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::{
@@ -18,7 +15,6 @@ 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,
@@ -27,7 +23,6 @@ pub enum Action {
Close(u64),
}
/// 侧栏开合、控件交互和本帧目标集合,决定实际占用页面宽度。
pub struct State {
pub open: bool,
pub interaction: Interaction,
@@ -36,7 +31,6 @@ pub struct State {
}
impl Default for State {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
Self {
open: true,
@@ -48,12 +42,10 @@ 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
@@ -62,7 +54,6 @@ impl State {
}
}
/// 绘制工具入口、页面列表和展开控制,同时登记稳定的交互目标。
pub fn draw(
&mut self,
tabs: &[Tab],
-11
View File
@@ -1,32 +1,23 @@
//! 云文件浏览的 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>,
@@ -34,7 +25,6 @@ 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();
@@ -48,7 +38,6 @@ impl SourceWorkspace {
Err(error) => self.error = error.to_string(),
}
}
/// 更新选中文件的内容与打开状态,失败保持可读错误信息。
pub fn open(&mut self, id: &str) {
let Some(provider) = &mut self.provider else {
return;
-27
View File
@@ -1,15 +1,10 @@
//! 即时绘制控件的跨帧交互状态:稳定 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);
@@ -17,13 +12,11 @@ impl WidgetId {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// 分隔条和滚动条的操作方向。
pub enum Axis {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// 管理控件角色,决定焦点资格和系统光标。
pub enum Role {
Button,
Tab,
@@ -35,13 +28,11 @@ 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,
@@ -52,7 +43,6 @@ pub enum Cursor {
Disabled,
}
#[derive(Clone, Debug)]
/// 本帧可命中的管理控件,绑定几何、角色、启用条件与可选 action。
pub struct Target<A> {
pub id: WidgetId,
pub rect: Rect,
@@ -61,7 +51,6 @@ pub struct Target<A> {
pub action: Option<A>,
}
#[derive(Clone, Copy, Debug)]
/// 绘制使用的组合交互状态,避免按钮自己重新推断焦点/捕获。
pub struct VisualState {
pub hovered: bool,
pub pressed: bool,
@@ -73,7 +62,6 @@ 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>,
@@ -83,22 +71,18 @@ 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;
@@ -107,26 +91,22 @@ 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,
@@ -147,13 +127,11 @@ 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;
@@ -176,21 +154,18 @@ 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 {
@@ -202,11 +177,9 @@ 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
-2
View File
@@ -1,5 +1,3 @@
//! 宿主管理控件的共享组件入口。主题、绘制原语和跨帧交互状态分离,用户 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;
-9
View File
@@ -1,5 +1,3 @@
//! 宿主控件的颜色与状态调色。根据背景、强调色和交互状态生成可读颜色,不改变业务状态。
//!
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.
@@ -18,31 +16,25 @@ 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;
@@ -52,7 +44,6 @@ 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(
-22
View File
@@ -1,6 +1,3 @@
//! 用统一主题绘制宿主管理控件,并同步注册交互目标。
//! 每个控件的图形、角色、启用状态和 action 一起登记,绘制几何与命中几何保持一致。
//!
use super::{
Axis, Interaction, Role, Target, VisualState, WidgetId,
theme::{self, Theme},
@@ -12,7 +9,6 @@ use crate::{
};
use std::hash::Hash;
/// 一次按钮绘制的借用描述,包括显示、角色和启用/选中状态。
pub struct Button<'a> {
pub label: &'a str,
pub selected: bool,
@@ -20,7 +16,6 @@ pub struct Button<'a> {
pub role: Role,
}
impl<'a> Button<'a> {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
pub fn new(label: &'a str) -> Self {
Self {
label,
@@ -29,24 +24,20 @@ 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>>,
@@ -54,7 +45,6 @@ pub struct Ui<A> {
interaction: Interaction,
}
impl<A: Hash> Ui<A> {
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
pub fn new(theme: Theme, interaction: Interaction) -> Self {
Self {
list: DisplayList::default(),
@@ -63,7 +53,6 @@ impl<A: Hash> Ui<A> {
interaction,
}
}
/// 使用当前主题绘制控件填充、轮廓或文字原语。
pub fn fill(&mut self, r: Rect, color: Color) {
self.list.push(DrawCommand::FillRoundedRect {
rect: r,
@@ -71,7 +60,6 @@ impl<A: Hash> Ui<A> {
color,
});
}
/// 使用当前主题绘制控件填充、轮廓或文字原语。
fn outline(&mut self, r: Rect, color: Color, width: f32) {
self.list.push(DrawCommand::StrokeRoundedRect {
rect: Rect::new(
@@ -85,7 +73,6 @@ 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),
@@ -102,7 +89,6 @@ impl<A: Hash> Ui<A> {
color,
});
}
/// 把控件身份、几何、角色、启用状态和 action 一起加入本帧交互目标。
fn register(
&mut self,
id: WidgetId,
@@ -122,7 +108,6 @@ impl<A: Hash> Ui<A> {
});
response
}
/// 绘制对应宿主管理控件,并使用同一矩形注册命中/捕获所需的交互信息。
fn surface(&mut self, rect: Rect, s: VisualState, base: Option<Color>) {
let color = if !s.enabled {
base
@@ -142,7 +127,6 @@ 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(
@@ -184,7 +168,6 @@ 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),
@@ -208,7 +191,6 @@ 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),
@@ -219,7 +201,6 @@ 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),
@@ -231,7 +212,6 @@ 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),
@@ -269,7 +249,6 @@ 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,
@@ -294,7 +273,6 @@ 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);
-3
View File
@@ -1,11 +1,8 @@
//! 发布页面标签的数据结构。保留稳定页面身份、标题、选中状态和开发工具类别;页面的托管所有权由模型层管理。
//!
//! 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,
+1 -1