diff --git a/Cargo.toml b/Cargo.toml
index a7571f0..0c9ef48 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,4 @@
+# Cargo 包元数据与编译器最低版本;二进制入口默认取 src/main.rs。
[package]
name = "tessera"
version = "0.1.0"
@@ -6,24 +7,39 @@ rust-version = "1.91"
description = "Cross-platform desktop rendering client"
license = "MIT OR Apache-2.0"
+# 跨平台运行依赖:语言执行、文字排版、序列化、窗口/像素表面与轻量 HTTP 服务。
+# Aster 三个 crate 使用固定在本仓库 vendor 子模块中的本地源码,不从注册表选择同名实现。
[dependencies]
+# 应用边界的错误聚合与上下文,保留底层失败原因。
anyhow = "1.0"
+# Aster 字节码编译器,把前端结果交给同一套 VM 执行。
compiler = { path = "vendor/aster/crates/compiler" }
+# 系统字体发现、塑形与换行,用于测量和软件文字绘制。
cosmic-text = "0.19"
+# Aster 托管对象、类型系统、VM 和可恢复调用;依赖包名 core 在 Rust 中重命名为 endor_core。
endor_core = { package = "core", path = "vendor/aster/crates/core" }
+# Endor 源语法解析与原始位置诊断。
frontend = { path = "vendor/aster/crates/frontend" }
+# 派生序列化协议,rc 特性允许发布模型中的 Arc/Rc 数据参与序列化。
serde = { version = "1.0", features = ["derive", "rc"] }
+# HTTP 请求/响应与事件信封的 JSON 编解码。
serde_json = "1.0"
+# 把宿主软件光栅化得到的像素缓冲呈现到原生窗口。
softbuffer = "0.4.8"
+# 本地 Agent API 的 HTTP 监听与请求响应,不引入异步 Web 运行时。
tiny_http = "0.12"
+# 按 Unicode 字素边界处理编辑/选择,避免把组合字符拆开。
unicode-segmentation = "1.12"
+# 跨平台窗口、事件循环、DPI 与输入法事件。
winit = "0.30.13"
+# 仅 macOS 编译原生标题栏适配,显式开启所用 AppKit/Foundation API,其他平台不引入这些依赖。
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.5.2"
objc2-app-kit = { version = "0.2.2", default-features = false, features = ["NSApplication", "NSButton", "NSEvent", "NSView", "NSWindow"] }
objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSGeometry", "NSString", "NSUserDefaults"] }
+# 发布构建:thin LTO 与单代码生成单元偏重最终优化,strip 去除可执行文件符号以控制体积。
[profile.release]
lto = "thin"
codegen-units = 1
diff --git a/docs/code-comments.md b/docs/code-comments.md
index ebb996d..5572e5a 100644
--- a/docs/code-comments.md
+++ b/docs/code-comments.md
@@ -1,6 +1,6 @@
# Tessera 中文源码注释与阅读索引
-本轮只为 Tessera 主项目补充注释,Aster 子模块固定版本与内容均沿用主线。注释按模块职责、类型/函数入口、关键字段、执行阶段和失败/取消路径组织;没有改变接口、算法、常量或资源预算。
+本轮进一步细化 Tessera 自有代码的中文注释:覆盖所有 Rust 模块、运行时/API 测试、内置起始页、45 个 Endor 示例、Cargo 依赖配置及两个工具脚本。说明聚焦具体输入/输出、状态归属、计算步骤、依赖观察、取消/失败路径和资源预算;不修改接口、算法、常量或非注释程序文本。Aster 是独立维护的 vendor 子模块,其固定版本与内容沿用主线。
## 建议阅读顺序
@@ -24,7 +24,7 @@
## Rust 逐文件说明
-每行说明该文件本次新增注释解释的职责和目的。生产函数/方法均增加中文入口说明;复杂状态机另外补充阶段和不变量说明。集成测试文件增加整体阅读与检查点说明,测试程序文本和源码范围断言保持原样。
+每行说明该文件本次深化注释的职责和目的。生产函数/方法的入口说明与内部关键逻辑互相补充;VM 可调用适配器说明实际参数约定,而非仅重复 trait 名称。集成测试中的每个顶层测试/辅助函数都有具体场景和断言说明,测试程序字符串与断言保持原样。
| 文件 | 注释解释的职责与目的 |
| --- | --- |
@@ -73,7 +73,7 @@
| [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_host/tests.rs](../src/runtime/endor_host/tests.rs) | 说明全部运行时测试和辅助函数的场景/断言,并解释新主线性能基准的 DPI 分组、预热、三类计时边界和统计方式。 |
| [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 身份及有限坐标。 |
@@ -106,11 +106,68 @@
| 文件 | 本次改动及目的 |
| --- | --- |
| [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/build-macos-app.sh](../scripts/build-macos-app.sh) | 说明 macOS 构建环境前提、失败退出、锁定依赖、临时文件替换和 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) | 本逐文件说明与阅读顺序,覆盖本轮提交的文件及共同边界。 |
+| [Cargo.toml](../Cargo.toml) | 解释每个依赖负责的能力、Aster 本地路径依赖、macOS 条件依赖与 release 优化配置。 |
+| [docs/code-comments.md](code-comments.md) | 更新本逐文件说明、示例索引和实际核对方式,使本次注释范围与可验证结果对应。 |
-## 核对方式
+## Endor 示例逐文件说明
-检查差异仅新增独立注释行和注释相关空白,并与主线去除注释后的文本对比;同时运行格式、完整 GUI 回归、Clippy 和包含私有项的 rustdoc。脚本使用语法检查,不执行文档渲染或应用打包。具体检查结果在 PR 中记录。
+每个示例的类、方法及关键逻辑旁增加中文说明。`page.endor` 和 `hello.endor` 被测试固定断言源码行号,因此使用行尾注释,保留原始行数;其余示例使用独立注释行,程序符号与字面量不变。
+
+| 文件 | 注释解释的场景与目的 |
+| --- | --- |
+| [examples/async-file.endor](../examples/async-file.endor) | 异步 UTF-8 文件读取:在 Console 调用 reader.read(绝对路径),后台 provider 只交付文本,VM worker 执行业务回调。 |
+| [examples/async-network.endor](../examples/async-network.endor) | 异步 HTTP(S) 文本 GET:Console 调用 fetcher.fetch(url),沿用 owner/key 替换、状态与成功回调协议。 |
+| [examples/await-file.endor](../examples/await-file.endor) | 把 callback-free 文件 Task 与 ui.spawn 协作函数组合:await 保存执行位置,等待完成后继续,不阻塞 VM worker。 |
+| [examples/box-layout.endor](../examples/box-layout.endor) | 盒模型组合:容器与 Page/相机独立,border-box 尺寸、padding、margin 和对齐分别参与布局。 |
+| [examples/campus-lms.endor](../examples/campus-lms.endor) | 多页面校园演示:七个普通托管 Page 共享 CampusCourse/CampusWork 业务对象,所有课程与学生数据均虚构。 |
+| [examples/classes.endor](../examples/classes.endor) | 普通源类继承示例:GUI 点击执行继承的 activate,内部 self.change 再动态分派到 DemoCounter 覆盖。 |
+| [examples/constructor-contracts.endor](../examples/constructor-contracts.endor) | 构造器分段示例:原生可选参数/可变子项与源子类新增字段分别验证。 |
+| [examples/cooperative-tasks.endor](../examples/cooperative-tasks.endor) | 协作任务示例:两个 ui.spawn 各自保留执行帧,worker 按指令量子轮转推进。 |
+| [examples/events.endor](../examples/events.endor) | 同一托管对象系统中的事件阶段:祖先 capture → 目标 onEvent → 目标监听器 → 祖先 bubble。 |
+| [examples/exit-lifecycle.endor](../examples/exit-lifecycle.endor) | 正常退出与多窗口卸载示例:同一 ExitText 出现在两窗,onUnmount 按出现位置分别调用。 |
+| [examples/field-contracts.endor](../examples/field-contracts.endor) | 字段类型契约示例:CounterText 同时有原生文本载荷和源 Number 字段。 |
+| [examples/flex-constraints.endor](../examples/flex-constraints.endor) | flexBasis/flexMin/flexMax/flexShrink 约束同一主轴分配。 |
+| [examples/flex.endor](../examples/flex.endor) | 按 flex 权重分配有限主轴空间:固定项先占自然尺寸,剩余空间按正权重划分。 |
+| [examples/fonts.endor](../examples/fonts.endor) | 字体测量示例:中英文混排、重音字符和不同字宽均走系统字体塑形与回退。 |
+| [examples/function-types.endor](../examples/function-types.endor) | Function 与 Type 的契约示例:源函数、原生函数与绑定方法都可作为函数值,类型构造器保持 Type 身份。 |
+| [examples/gc-lifecycle.endor](../examples/gc-lifecycle.endor) | 托管对象循环与等待帧回收:先记 ui.stats().objects,再调用 mountCycle,观察 phase 与 pending.value.status()。 |
+| [examples/global-dependencies.endor](../examples/global-dependencies.endor) | 全局名称依赖示例:paint 读取 color 这个具体名字,而非依赖整个 globals 字典版本。 |
+| [examples/hello.endor](../examples/hello.endor) | 解释辅助 heading、兼容 main 入口、局部业务引用和原生布局,保留源码位置夹具。 |
+| [examples/layout-cache.endor](../examples/layout-cache.endor) | 测量与绘制依赖分离:改颜色应复用尺寸缓存,改模型宽度才需要重新测量。 |
+| [examples/layout-negotiation.endor](../examples/layout-negotiation.endor) | 测量/排列协商示例:arrange 指定子宽后需重新换行测量,高度变化继续影响父容器和后继位置,直到整树稳定。 |
+| [examples/main.endor](../examples/main.endor) | 与 page.endor 等价的 main 返回根入口:用于对比顶层显式 render 和兼容模块入口的绘图结果。 |
+| [examples/managed-types.endor](../examples/managed-types.endor) | 托管名义类型入门:ui 构造器与 TextBlock/Button/Page 返回同一套 VM 托管对象。 |
+| [examples/method-contracts.endor](../examples/method-contracts.endor) | 方法覆盖遵守参数可更宽、返回可更具体的规则。 |
+| [examples/mount-lifecycle.endor](../examples/mount-lifecycle.endor) | 业务对象与出现位置生命周期分离:同一 child 在根中出现两次,每次都获得独立 Mount 和通知。 |
+| [examples/native-signatures.endor](../examples/native-signatures.endor) | 原生回调也发布 Function 签名:state.set 接收一个 Event,increment 接收零参数。 |
+| [examples/native-subclasses.endor](../examples/native-subclasses.endor) | 源类继承原生视觉类型:原生载荷负责基础绘制,新增字段和覆盖方法由源类提供。 |
+| [examples/owned-tasks.endor](../examples/owned-tasks.endor) | owner 作用域决定取消时机:Page 关闭会取消其任务,隐藏 Block 和 ui 的任务可继续。 |
+| [examples/page.endor](../examples/page.endor) | 解释顶层脚本创建文字/矩形、Page 与 render 的关系,保留四行结构树夹具。 |
+| [examples/presentable.endor](../examples/presentable.endor) | 普通业务对象显式实现 Presentable 后,ui.present 可调用其 present(window) 生成视觉对象。 |
+| [examples/property-bindings.endor](../examples/property-bindings.endor) | 通用属性绑定:业务 Model 的三个零参只读方法分别计算 rect.width、rect.color 和 label.text。 |
+| [examples/scene-viewports.endor](../examples/scene-viewports.endor) | 一个二维世界可被多个有限视口观察,Scene2D 的对象位置共享,默认 Camera2D 各自独立。 |
+| [examples/scene3d.endor](../examples/scene3d.endor) | 同一 Scene3D 在透视/正交视口中展示;相机控制独立,网格业务变换由两边共享。 |
+| [examples/scroll-axis.endor](../examples/scroll-axis.endor) | 滚动轴策略影响可滚动方向与内容测量约束:vertical 限宽并允许长高,horizontal 允许横向展开。 |
+| [examples/scroll.endor](../examples/scroll.endor) | 同一 ScrollBlock 在两个原生窗口中展示:内容业务对象共享,滚动偏移按 Mount 独立保存。 |
+| [examples/source-hit.endor](../examples/source-hit.endor) | 自定义形状命中:对象原始矩形宽 160,但 paint 只画左侧直径 80 的圆,hitTest 也只接受圆内。 |
+| [examples/source-layout.endor](../examples/source-layout.endor) | 源视觉协议的最小组合:measure 决定尺寸,arrange 决定子项位置,Metrics 是被两者读取的业务模型。 |
+| [examples/source-paint.endor](../examples/source-paint.endor) | 源 paint 产出真正进入窗口的 DisplayList;允许构造并修改本次计算的新命令,禁止改已有业务对象。 |
+| [examples/student-grades.endor](../examples/student-grades.endor) | 学生成绩中心为纯内存演示,记录均虚构,不连接学校数据库。 |
+| [examples/tab-placement.endor](../examples/tab-placement.endor) | 页面位置与子项插入的代码示例:Page 方法覆盖计数,Container.insert 在指定位置创建稳定子槽。 |
+| [examples/tabs.endor](../examples/tabs.endor) | 代码与原生侧栏都操作真实 Page 对象;activate/close/open 改变展示关系,业务对象可继续被全局引用保留。 |
+| [examples/visual-protocols.endor](../examples/visual-protocols.endor) | 原生视觉协议直接返回托管数据:Constraints/MeasureResult、Bounds/Arrangement 和 DisplayList 都可在源码中查询。 |
+| [examples/window.endor](../examples/window.endor) | 托管 Window 资源与原生窗口相连:源码排出 resize/title 命令,成功刷新后由 GUI 线程执行。 |
+| [examples/windows.endor](../examples/windows.endor) | 两个原生窗口共用一个 Aster VM 和 count 业务对象,各自拥有独立 Page 与 Mount。 |
+| [examples/workspace.endor](../examples/workspace.endor) | 静态工作台组合示例:所有指标、进度与活动记录均为演示数据,不读取任务系统或网络。 |
+| [examples/world-ui.endor](../examples/world-ui.endor) | 世界空间 UI:普通 Endor 树先在独立 UI 分辨率中布局/绘制,再作为 Panel3D 平面的纹理参与三维深度。 |
+| [examples/wrapped-text.endor](../examples/wrapped-text.endor) | 多窗口共享长段落:同一文字对象在每个 Mount 按当前宽度独立换行,后继位置跟随实际高度。 |
+
+## 核对方式与边界
+
+- 与本次分支起点及最新主线比较,逐文件核对移除注释后的文本;Rust/Endor 另做词法扫描,完整保留字符串/原始字符串内容,确认新增说明没有进入程序字面量。
+- 使用 rustfmt 的 `--emit stdout`、`skip_children=true` 逐文件做 Rust 语法解析,丢弃输出,不格式化文件、不运行 Cargo、不生成编译产物。
+- 对工具脚本使用 `node --check` 和 `sh -n`,只检查语法,不执行渲染、构建或打包;Cargo 配置另以 TOML 解析核对。
+- 检查固定行号的示例夹具、测试依赖的源码替换片段和 `git diff --check`。这些检查能证明注释外代码保持一致及语法可解析,不能替代运行时回归结果。
+- 本次未执行编译、GUI 回归、Clippy 或 rustdoc;需要构建的验证应在用户已确认的远程 runner 上运行,不能把未执行步骤写成通过。实际交付结果记录在 PR 中。
diff --git a/examples/async-file.endor b/examples/async-file.endor
index 44a6add..4ba1cd0 100644
--- a/examples/async-file.endor
+++ b/examples/async-file.endor
@@ -1,18 +1,25 @@
+// 异步 UTF-8 文件读取:在 Console 调用 reader.read(绝对路径),后台 provider 只交付文本,VM worker 执行业务回调。
+// owner/key 相同的新读取替换旧任务;失败只记录 Task.error,不执行 apply。
+// 持有三个显示对象与当前任务;task 初值占位,第一次 read 后替换为真实 Task。
class FileReader {
output: TextBlock
problem: TextBlock
status: TextBlock
task: Object
+ // 仅成功时调用,result 在成功回调期间已可读;写原 TextBlock 触发显示刷新。
function apply(self: FileReader): Unit {
self.output.text = self.task.result()
}
+ // 以 self 为 owner、file 为 key 启动读取,清空旧输出,并把状态/错误文字绑定到当前任务。
function read(self: FileReader, path: String): Unit {
self.task = ui.readText(self, "file", path, self.apply)
self.output.text = ""
+ // Task.status/error 是零参数只读方法,可直接作为绑定计算;换任务后绑定也随本次调用更新。
self.status.bind("text", self.task.status)
self.problem.bind("text", self.task.error)
}
}
+// 先建立界面对象,保持 reader 为可从 Console 操作的全局业务入口。
output = TextBlock("尚未读取文件")
problem = TextBlock("")
status = TextBlock("")
diff --git a/examples/async-network.endor b/examples/async-network.endor
index e2f3e35..64dcda2 100644
--- a/examples/async-network.endor
+++ b/examples/async-network.endor
@@ -1,21 +1,28 @@
+// 异步 HTTP(S) 文本 GET:Console 调用 fetcher.fetch(url),沿用 owner/key 替换、状态与成功回调协议。
+// 完整响应保存为 String,界面只显示完成提示,避免把可能很大的响应放进文字控件。
+// response 是结果业务存储;output/problem/status 仅呈现任务进度与提示。
class TextFetcher {
output: TextBlock
problem: TextBlock
status: TextBlock
task: Object
response: String
+ // 成功后取出完整 UTF-8 结果,写 response 供 Console 查询;provider 失败不会进入此方法。
function apply(self: TextFetcher): Unit {
self.response = self.task.result()
self.output.text = "请求完成;在 REPL 查看 fetcher.response"
}
+ // 同一 self/request 身份的新请求取消旧交付,重置旧内容并把显示绑定到新任务。
function fetch(self: TextFetcher, url: String): Unit {
self.task = ui.fetchText(self, "request", url, self.apply)
self.response = ""
self.output.text = ""
+ // 绑定读取当前 Task 方法,完成和失败由 worker 的任务状态变更传播。
self.status.bind("text", self.task.status)
self.problem.bind("text", self.task.error)
}
}
+// 界面不自动访问网络,用户以 Console 明确指定 URL 后才创建任务。
output = TextBlock("尚未请求网络")
problem = TextBlock("")
status = TextBlock("")
diff --git a/examples/await-file.endor b/examples/await-file.endor
index ce66007..50858af 100644
--- a/examples/await-file.endor
+++ b/examples/await-file.endor
@@ -1,24 +1,31 @@
+// 把 callback-free 文件 Task 与 ui.spawn 协作函数组合:await 保存执行位置,等待完成后继续,不阻塞 VM worker。
+// readText 的 file 与 spawn 的 workflow 使用不同 key,分别管理读取和等待流程。
+// 保存 provider Task、协程 Task 和结果;两种任务都是托管对象,可以从 Console 独立查看。
class AwaitReader {
output: TextBlock
status: TextBlock
task: Object
file: Object
response: String
+ // await 在文件未完成时挂起并保留 self/操作数;恢复后只执行后半段一次,再把字符串作为 workflow.result。
function apply(self: AwaitReader): String {
self.response = await self.file
self.output.text = "读取完成;在 REPL 查看 reader.response 或 reader.task.result()"
return self.response
}
+ // 先创建读任务,再调度普通函数 apply;spawn 而非新 async 语法提供可暂停执行上下文。
function read(self: AwaitReader, path: String): Unit {
self.file = ui.readText(self, "file", path)
self.task = ui.spawn(self, "workflow", self.apply)
self.response = ""
self.output.text = "等待文件"
+ // 显示的是 workflow 状态,等待 provider 时它仍报告 running;结束后 result 可从 Task 或 response 读取。
self.status.bind("text", self.task.status)
}
}
output = TextBlock("在 REPL 调用 reader.read(绝对路径字符串)")
status = TextBlock("")
+// 占位值只用于满足初始字段构造;在 read 内被真实 Task 替换,页面建立不自动读文件。
reader = AwaitReader(output,status,ui.state(0),ui.state(0),"")
page = Page()
page.title = "await 文件"
diff --git a/examples/box-layout.endor b/examples/box-layout.endor
index ca42868..c49dd59 100644
--- a/examples/box-layout.endor
+++ b/examples/box-layout.endor
@@ -1,8 +1,12 @@
-// Containers compose independently of Page and of any camera.
+// 盒模型组合:容器与 Page/相机独立,border-box 尺寸、padding、margin 和对齐分别参与布局。
+// 同一文字共享内容,在左右不同有限分配中各自换行;外层 Overlay 负责叠放固定角标并裁剪溢出。
+// 内部布局基于容器自己的约束;Page 和相机只提供外部展示/投影上下文。
+// 文字 boxWidth=fill 占父分配的可用宽度,marginBottom 在文字边框盒外参与排布。
shared = TextBlock("同一个对象可以有多个 Mount。每个位置分别测量、换行和对齐。", 2)
shared.boxWidth = "fill"
shared.marginBottom = 8
+// 左 Column 使用 fill、min/max 宽度限制和四边边框;非对称 paddingLeft 覆盖统一 padding。
left = Column(16, 12, TextBlock("Row / Column", 2), shared, Button("继续探索"))
left.boxWidth = "fill"
left.minWidth = 180
@@ -15,24 +19,29 @@ left.borderColor = "#CBD5E1"
left.paddingLeft = 24
left.crossAlign = "center"
+// 右 Column 也引用同一 shared,但最大宽只有 240,因此对应 Mount 的换行高度可以不同。
right = Column(16, 12, TextBlock("共享内容,独立排版", 2), shared)
right.boxWidth = "fill"
right.maxWidth = 240
right.crossAlign = "end"
+// Row 分配有限 760 主轴空间,扣 padding/gap 后在两侧 fill 子项之间分配并服从上限。
content = Row(24, 24, left, right)
content.boxWidth = 760
content.crossAlign = "start"
+// 角标是 Overlay 的另一子项,alignSelf 与 margin 控制它在容器交叉轴上的位置。
badge = TextBlock("Endorphin · layout")
badge.marginRight = 12
badge.marginBottom = 12
badge.alignSelf = "end"
+// 固定 800x400 的 Overlay 不按线性顺序挤开兄弟,overflow=clip 同时约束绘制和命中。
surface = Overlay(0, 0, content, badge)
surface.boxWidth = 800
surface.boxHeight = 400
surface.mainAlign = "end"
surface.overflow = "clip"
+// Page 只提供展示入口,根容器自身的盒模型规则不依赖页面导航方式。
page = Page()
page.title = "嵌套盒模型"
page.render(surface)
diff --git a/examples/campus-lms.endor b/examples/campus-lms.endor
index 2675861..bc72c07 100644
--- a/examples/campus-lms.endor
+++ b/examples/campus-lms.endor
@@ -1,7 +1,9 @@
-// Endor Campus — a multi-page learning environment inspired by Canvas LMS.
-// Fictional student/course data. Paste the complete source into Console and Enter.
-// All seven tabs are ordinary managed Pages. There is no server or Canvas login.
+// 多页面校园演示:七个普通托管 Page 共享 CampusCourse/CampusWork 业务对象,所有课程与学生数据均虚构。
+// 无服务器、账号登录或真实上传;提交/评分按钮只应用预设字段值,其他页通过绑定读同一状态。
+// 作为一次完整输入提交;七页共用同一VM,会话重置后数据消失。
+// 白色圆角卡片容器,继承 Column 的排版,只覆盖自身背景绘制。
class CampusPanel: Column {
+ // 按当前出现位置 geometry 创建一条背景命令,子项由展示遍历继续绘制。
function paint(self: CampusPanel, mount: Mount, geometry: Bounds): DisplayList {
fill = PaintCommand()
fill.kind = "roundedRect"
@@ -14,18 +16,23 @@ class CampusPanel: Column {
return DisplayList(ObjectList(fill))
}
}
+// 通过额外 width 字段固定表格列的期望宽,保持成绩表头和行对齐。
class CampusCell: Column {
width: Number
+ // 先算子内容自然高度,再改新测量结果宽度,保留真实字体引起的高度。
function measure(self: CampusCell, mount: Mount, constraints: Constraints): MeasureResult {
result = Column.measure(self, mount, constraints)
result.width = self.width
return result
}
}
+// 页面内容里的演示导航栏,与应用自身的原生页面侧栏是两棵不同控件树。
class CampusSidebar: Column {
+ // 返回固定168x620期望尺寸,外层 ScrollBlock 承担超出区域的滚动。
function measure(self: CampusSidebar, mount: Mount, constraints: Constraints): MeasureResult {
return MeasureResult(168, 620)
}
+ // 为导航区创建深红背景,位置/大小来自当前Mount,文字等子项保持独立命令。
function paint(self: CampusSidebar, mount: Mount, geometry: Bounds): DisplayList {
fill = PaintCommand()
fill.kind = "roundedRect"
@@ -38,11 +45,14 @@ class CampusSidebar: Column {
return DisplayList(ObjectList(fill))
}
}
+// Button 子类增加背景字段,固定导航按钮尺寸,并保留原生事件目标能力。
class CampusNav: Button {
background: String
+ // 导航按钮期望144x40,确保四个入口宽高一致。
function measure(self: CampusNav, mount: Mount, constraints: Constraints): MeasureResult {
return MeasureResult(144, 40)
}
+ // 取原生按钮背景命令改色/圆角;文字由后面 replace 后的子项绘制,不在此重复生成。
function paint(self: CampusNav, mount: Mount, geometry: Bounds): DisplayList {
fill = Button.paint(self, mount, geometry).commands.at(0)
fill.color = self.background
@@ -50,6 +60,7 @@ class CampusNav: Button {
return DisplayList(ObjectList(fill))
}
}
+// 课程业务记录,grade/progress 被多个页面绑定读取,其余静态字段构建时直接读取。
class CampusCourse {
title: String
code: String
@@ -57,6 +68,7 @@ class CampusCourse {
grade: String
progress: String
}
+// 实验状态集中在一个对象,course 指向同一课程模型,评分因此能影响全站课程摘要。
class CampusWork {
status: String
score: String
@@ -65,12 +77,14 @@ class CampusWork {
todo: String
result: String
course: CampusCourse
+ // 模拟提交只改状态、文件名提示、待办和结果文字,不访问磁盘或网络。
function submit(self: CampusWork, event: Event): Unit {
self.status = "已提交 · 等待评分"
self.submission = "lab01.endor · 已提交示例内容"
self.todo = "已完成本次实验提交"
self.result = "示例提交成功,可以切到作业中心查看同一条记录。"
}
+ // 应用预设92分和反馈,并更新所属课程成绩/进度;绑定让仪表盘、课程、成绩页同步观察。
function assess(self: CampusWork, event: Event): Unit {
self.status = "已评分"
self.score = "92 / 100"
@@ -81,6 +95,7 @@ class CampusWork {
self.course.progress = "已完成 4 / 6 个学习单元"
self.result = "已应用预设评分;仪表盘、课程主页和成绩总览同步更新。"
}
+ // 恢复初始实验与课程状态,保留所有原对象和绑定,便于反复演示。
function reset(self: CampusWork, event: Event): Unit {
self.status = "未提交"
self.score = "— / 100"
@@ -92,20 +107,25 @@ class CampusWork {
self.result = "已恢复实验、课程成绩和学习进度的示例状态。"
}
}
+// 用统一包装构造表格单元格;传入的content是已有Block引用。
function campusCell(content: Block, width: Number): Block {
return CampusCell(0, 0, content, width)
}
+// 显式事件适配器把click的Event与Page.open可选Window参数分开,不能直接把Event误当窗口。
class CampusTarget {
page: Page
- // Consume the click Event here; Page.open's optional argument is a Window.
+ // 事件参数在适配器这一层消化,Page.open 只接收其声明的可选 Window。
+ // 消费事件但不传给Page.open;无参open表示在合适窗口打开/激活目标页。
function open(self: CampusTarget, event: Event): Unit { self.page.open() }
}
+// 为普通按钮创建持有目标Page的适配器,并把其绑定方法注册为click回调。
function campusLink(label: String, target: Page): Button {
button = Button(label)
action = CampusTarget(target)
button.on("click", action.open)
return button
}
+// 构造带指定背景的导航按钮,替换原生文字子槽为白字,然后复用事件适配器连接目标页。
function campusNav(label: String, target: Page, background: String): Button {
button = CampusNav(label, background)
button.replace(0, TextBlock(label, 1, "#FFFFFF"))
@@ -113,6 +133,7 @@ function campusNav(label: String, target: Page, background: String): Button {
button.on("click", action.open)
return button
}
+// 用目标Page和四个色调参数构建整块导航;不同页面复用目标资源但用不同高亮背景。
function campusNavigation(home: Page, course: Page, assignments: Page, grades: Page, homeTone: String, courseTone: String, workTone: String, gradeTone: String): Block {
return CampusSidebar(12, 10,
TextBlock("Endor", 2, "#FFFFFF"),
@@ -125,6 +146,7 @@ function campusNavigation(home: Page, course: Page, assignments: Page, grades: P
TextBlock("2026 秋季学期", 1, "#E7BAC0"),
TextBlock("本地演示", 1, "#E7BAC0"))
}
+// 把演示导航与标题/正文横排,外层有限ScrollBlock启用双轴,容纳固定宽度演示内容。
function campusShell(navigation: Block, title: String, subtitle: String, body: Block): Block {
scroll = ScrollBlock(1120, 680,
Row(24, 28, navigation,
@@ -135,6 +157,7 @@ function campusShell(navigation: Block, title: String, subtitle: String, body: B
scroll.axis = "both"
return scroll
}
+// 卡片静态显示课程代码/标题/教师,grade 用绑定随评分变化,入口按钮引用真实课程Page。
function campusCourseCard(course: CampusCourse, page: Page, color: String): Block {
return CampusPanel(0, 0,
RectBlock(248, 44, 8, color),
@@ -145,6 +168,7 @@ function campusCourseCard(course: CampusCourse, page: Page, color: String): Bloc
ui.textOf(course, "grade", 2, color),
campusLink("进入课程", page)))
}
+// 成绩表一行复用固定列宽单元格,当前grade为响应式文字,详情链接指向指定课程页。
function campusGradeRow(course: CampusCourse, target: Page): Block {
return Column(0, 12,
Row(0, 0,
@@ -154,6 +178,7 @@ function campusGradeRow(course: CampusCourse, target: Page): Block {
campusLink("课程详情", target)),
RectBlock(752, 1, 0, "#EDF0F3"))
}
+// 组合课程概况/进度和静态学习单元;上方两个链接复用作业/成绩Page,不创建新页面。
function campusCourseBody(course: CampusCourse, intro: String, moduleOne: String, moduleTwo: String, moduleThree: String, assignments: Page, grades: Page): Block {
return Column(0, 20,
Row(0, 16, campusLink("作业与提交", assignments), campusLink("查看成绩", grades)),
@@ -171,11 +196,13 @@ function campusCourseBody(course: CampusCourse, intro: String, moduleOne: String
RectBlock(752, 1, 0, "#EDF0F3"),
TextBlock(moduleThree, 1, "#526372")))
}
-// The shared object graph is created once; each Page projects those same objects.
+// 共享对象图只构造一次,各Page的文字绑定投影同一业务字段。
+// 只创建三份课程模型和一份实验模型,后续各页读取这些对象,避免页面间状态副本不一致。
campusProgramming = CampusCourse("计算机程序设计", "CS 101", "陈老师 · 周二 / 周四", "86.0%", "已完成 3 / 6 个学习单元")
campusMathematics = CampusCourse("高等数学 I", "MATH 101", "周老师 · 周一 / 周三", "91.0%", "已完成 4 / 6 个学习单元")
campusEnglish = CampusCourse("学术英语", "ENG 102", "许老师 · 周五", "89.0%", "已完成 3 / 5 个学习单元")
campusWork = CampusWork("未提交", "— / 100", "提交后可在此查看教师反馈。", "尚无提交内容", "1 项实验待提交", "这是一个本地提交与评分演示。", campusProgramming)
+// 先创建全部七个Page,允许导航按钮互相引用;随后逐一render建立内容。
campusDashboard = Page()
campusDashboard.title = "Campus · 仪表盘"
campusProgrammingPage = Page()
@@ -190,10 +217,12 @@ campusGrades = Page()
campusGrades.title = "Campus · 成绩总览"
campusWorkPage = Page()
campusWorkPage.title = "实验 01 · 提交详情"
+// 预建四种高亮导航树,课程三页共享course导航,Page切换时各自拥有独立Mount。
campusHomeNav = campusNavigation(campusDashboard, campusProgrammingPage, campusAssignments, campusGrades, "#A64350", "#852B38", "#852B38", "#852B38")
campusCourseNav = campusNavigation(campusDashboard, campusProgrammingPage, campusAssignments, campusGrades, "#852B38", "#A64350", "#852B38", "#852B38")
campusAssignmentNav = campusNavigation(campusDashboard, campusProgrammingPage, campusAssignments, campusGrades, "#852B38", "#852B38", "#A64350", "#852B38")
campusGradeNav = campusNavigation(campusDashboard, campusProgrammingPage, campusAssignments, campusGrades, "#852B38", "#852B38", "#852B38", "#A64350")
+// 仪表盘展示课程卡和当前实验摘要,status/todo均从同一campusWork绑定读取。
campusDashboard.render(campusShell(campusHomeNav, "仪表盘", "林沐,欢迎回来。这里是你的课程与近期学习安排。",
Column(0, 20,
Row(0, 16,
@@ -206,6 +235,7 @@ campusDashboard.render(campusShell(campusHomeNav, "仪表盘", "林沐,欢迎
ui.textOf(campusWork, "todo", 1, "#7C8793"),
campusLink("打开作业详情", campusWorkPage)),
TextBlock("虚构课程与学生数据 · Endor 本地多页面演示", 1, "#8C959F"))))
+// 程序设计课程附加实验入口;数学和英语页复用相同概况结构但读取不同课程模型。
campusProgrammingPage.render(campusShell(campusCourseNav, "计算机程序设计", "课程 / CS 101 / 主页",
Column(0, 16,
campusCourseBody(campusProgramming, "通过实际编程理解对象、函数与交互。", "01 对象与字段 · 已完成", "02 函数与方法 · 已完成", "03 事件与可视界面 · 进行中", campusAssignments, campusGrades),
@@ -214,6 +244,7 @@ campusMathPage.render(campusShell(campusCourseNav, "高等数学 I", "课程 / M
campusCourseBody(campusMathematics, "学习极限、微分与积分的基本方法。", "01 函数与极限 · 已完成", "02 导数及其应用 · 已完成", "03 积分方法 · 进行中", campusAssignments, campusGrades)))
campusEnglishPage.render(campusShell(campusCourseNav, "学术英语", "课程 / ENG 102 / 主页",
campusCourseBody(campusEnglish, "阅读学术文本,练习清晰的论证与表达。", "01 Academic reading · 已完成", "02 Evidence and arguments · 已完成", "03 Research writing · 进行中", campusAssignments, campusGrades)))
+// 作业中心只有程序设计实验为共享动态模型,其余两项是固定演示文字。
campusAssignments.render(campusShell(campusAssignmentNav, "作业中心", "本学期 / 全部课程 / 作业与提交",
Column(0, 20,
CampusPanel(20, 12,
@@ -227,6 +258,7 @@ campusAssignments.render(campusShell(campusAssignmentNav, "作业中心", "本
CampusPanel(20, 12,
TextBlock("Reading response", 1, "#263444"),
TextBlock("ENG 102 · 已评分 · 88 / 100", 1, "#7C8793")))))
+// 成绩总览复用课程grade绑定和实验score/feedback,评分无需逐页手动写UI。
campusGrades.render(campusShell(campusGradeNav, "成绩总览", "2026 秋季学期 / 当前课程成绩",
Column(0, 20,
CampusPanel(20, 16,
@@ -242,12 +274,14 @@ campusGrades.render(campusShell(campusGradeNav, "成绩总览", "2026 秋季学
Row(0, 24, TextBlock("实验 01:对象与函数"), ui.textOf(campusWork, "score", 1, "#315E86")),
ui.textOf(campusWork, "feedback", 1, "#6D7987"),
campusLink("打开作业与反馈", campusWorkPage)))))
+// 三个操作按钮连接同一个实验对象的submit/assess/reset,均为真实源方法回调。
campusSubmitButton = Button("模拟提交实验")
campusSubmitButton.on("click", campusWork.submit)
campusAssessButton = Button("应用示例评分")
campusAssessButton.on("click", campusWork.assess)
campusResetButton = Button("恢复演示数据")
campusResetButton.on("click", campusWork.reset)
+// 详情页组合要求、提交状态、操作按钮和反馈;所有输入仍只是预设本地状态转换。
campusWorkPage.render(campusShell(campusAssignmentNav, "实验 01:对象与函数", "CS 101 / 作业 / 提交详情",
Column(0, 20,
Row(0, 16, campusLink("返回课程", campusProgrammingPage), campusLink("全部作业", campusAssignments), campusLink("成绩总览", campusGrades)),
@@ -265,4 +299,5 @@ campusWorkPage.render(campusShell(campusAssignmentNav, "实验 01:对象与函
Row(0, 24, TextBlock("成绩与反馈"), ui.textOf(campusWork, "score", 2, "#315E86")),
ui.textOf(campusWork, "feedback", 1, "#6D7987")),
ui.textOf(campusWork, "result", 1, "#7C8793"))))
+// 逐页render会改变当前活动页,最后显式选择仪表盘作为打开后的首页。
campusDashboard.activate()
diff --git a/examples/classes.endor b/examples/classes.endor
index 70f527d..6da88c5 100644
--- a/examples/classes.endor
+++ b/examples/classes.endor
@@ -1,28 +1,37 @@
+// 普通源类继承示例:GUI 点击执行继承的 activate,内部 self.change 再动态分派到 DemoCounter 覆盖。
+// 两个 textOf 共享同一模型字段,修改一次业务值会更新两个展示位置。
+// 定义业务状态和公共事件入口,Number/String 字段在构造与赋值时按声明契约检查。
class Counter {
value: Number
lastEvent: String
+ // 保存实际事件种类,再通过 self 调用可覆盖的方法;接收者保持具体子类身份。
function activate(self: Counter, event: Event): Unit {
self.lastEvent = event.kind
self.change()
}
+ // 基类默认行为把值置为 10,子类可复用入口并替换这部分策略。
function change(self: Counter): Unit {
self.value = 10
}
}
+// 继承 Counter 的字段与 activate,仅替换 change;构造参数仍按继承字段顺序传入。
class DemoCounter: Counter {
+ // 覆盖把值设为 12,演示从继承方法内部发起的调用也会选中实际接收者的方法。
function change(self: DemoCounter): Unit {
self.value = 12
}
}
+// 创建唯一模型,之后所有绑定和按钮回调都指向它,无需把 GUI 状态复制到另一份存储。
model = DemoCounter(9, "等待点击")
first = ui.textOf(model, "value", 2)
second = ui.textOf(model, "value", 2)
eventName = ui.textOf(model, "lastEvent")
button = Button("调用继承方法,再分派到覆盖方法")
+// 注册绑定方法,运行时将 click 的 Event 传给 activate;最后 Page.render 才把对象树显示出来。
button.onClick(model.activate)
page = Page()
page.render(Column(24, 16, TextBlock("Endor class:继承、self 与动态方法分派", 2), Row(0, 16, first, second), button, eventName))
diff --git a/examples/constructor-contracts.endor b/examples/constructor-contracts.endor
index 7447085..aab8dac 100644
--- a/examples/constructor-contracts.endor
+++ b/examples/constructor-contracts.endor
@@ -1,11 +1,20 @@
+// 构造器分段示例:原生可选参数/可变子项与源子类新增字段分别验证。
+// 省略 RectBlock 的 radius/color 不应使字符串 tag 被错当成原生颜色参数。
+// 在原生矩形后增加 tag 字段;位置构造仍保留 width/height 及可选圆角/颜色规则。
class TaggedRect: RectBlock { tag: String }
+// 在可变数量 Block 子项之后接收源 tag,子项列表与字段不混用。
class TaggedColumn: Column { tag: String }
+// 保留原生事件 kind,再追加业务字符串字段。
class TaggedEvent: Event { tag: String }
+// 原生列表成员之后追加 tag,列表元素仍可保存不同托管类型。
class TaggedList: ObjectList { tag: String }
+// 将需要更新的矩形作为强业务引用保存,按钮回调只修改它的颜色。
class Actions {
rect: TaggedRect
+ // 写原对象的 color,挂载显示由依赖跟踪刷新,不需要重新构造整个页面。
function recolor(self: Actions): Unit { self.rect.color = "#237A45" }
}
+// 这里刻意省略原生可选项,用末尾字符串初始化子类字段;后面的列表/Column 做对应可变参数演示。
rect = TaggedRect(180, 60, "省略 radius/color 后的字段")
event = TaggedEvent("click", "事件字段")
items = TaggedList(rect, event, "列表成员之后的字段")
diff --git a/examples/cooperative-tasks.endor b/examples/cooperative-tasks.endor
index 48e18d6..665cab5 100644
--- a/examples/cooperative-tasks.endor
+++ b/examples/cooperative-tasks.endor
@@ -1,5 +1,10 @@
+// 协作任务示例:两个 ui.spawn 各自保留执行帧,worker 按指令量子轮转推进。
+// 这里刻意重复增量语句,方便看到暂停点;取消不回滚已经发生的 steps 写入,单个原生调用仍是原子边界。
+// 每个实例有独立 steps 状态,两个任务不会把局部帧或计数存储混在一起。
class CooperativeWork {
steps: Object
+ // 普通零参数方法被 spawn 调度后可跨量子继续;每条 increment 更新同一实例的计数。
+ // 连续语句无需手工 yield,VM 按累计预算暂停,恢复时不会从第一条重放。
function run(self: CooperativeWork): Unit {
self.steps.increment()
self.steps.increment()
@@ -67,10 +72,12 @@ class CooperativeWork {
self.steps.increment()
}
}
+// 创建两个独立 owner,虽然 key 都叫 work,也不会相互替换;owner/key 组合才是任务身份。
left = CooperativeWork(ui.state(0))
right = CooperativeWork(ui.state(0))
leftTask = ui.spawn(left,"work",left.run)
rightTask = ui.spawn(right,"work",right.run)
+// 状态文字绑定到 Task.status,业务进度可在 Console 查询 left/right 的 steps.value。
leftStatus = TextBlock("")
rightStatus = TextBlock("")
leftStatus.bind("text",leftTask.status)
diff --git a/examples/events.endor b/examples/events.endor
index 740d84c..28562e6 100644
--- a/examples/events.endor
+++ b/examples/events.endor
@@ -1,3 +1,6 @@
+// 同一托管对象系统中的事件阶段:祖先 capture → 目标 onEvent → 目标监听器 → 祖先 bubble。
+// EventLog 用一个递增序列记录实际次序;改变监听器/传播标记可对比跳过与停止的语义。
+// 保存各阶段序号与最近 phase;count 是共享状态,其他字段直接写在本模型中。
class EventLog {
count: Object
captureStep: Number
@@ -5,24 +8,29 @@ class EventLog {
targetStep: Number
bubbleStep: Number
phase: String
+ // 祖先捕获阶段先递增序列,再保存 captureStep 和 Event.phase。
function capture(self: EventLog, event: Event): Unit {
self.count.increment()
self.captureStep = self.count.value
self.phase = event.phase
}
+ // 目标监听器的执行点;它排在目标类型 onEvent 方法之后。
function target(self: EventLog, event: Event): Unit {
self.count.increment()
self.targetStep = self.count.value
self.phase = event.phase
}
+ // 祖先冒泡阶段记录最后序号,未被 stopPropagation 时才到达。
function bubble(self: EventLog, event: Event): Unit {
self.count.increment()
self.bubbleStep = self.count.value
self.phase = event.phase
}
}
+// 覆盖目标方法,比额外 on(click,...) 监听器更早处理目标阶段事件。
class EventButton: Button {
model: EventLog
+ // 把模型序列递增后写 methodStep,供断言与 capture/target/bubble 比较。
function onEvent(self: EventButton, event: Event): Unit {
self.model.count.increment()
self.model.methodStep = self.model.count.value
@@ -32,6 +40,8 @@ class EventButton: Button {
model = EventLog(ui.state(0), 0, 0, 0, 0, "等待点击")
button = EventButton("捕获 → 对象方法 → 监听器 → 冒泡", model)
root = Column(24, 16, TextBlock("同一对象系统中的事件分派", 2), button, ui.textOf(model.count, "value"), ui.textOf(model, "phase"))
+// capture 选项显式选择捕获;不带选项的祖先监听器默认在冒泡阶段执行。
+// 返回的令牌可 disconnect,同次派发中断开尚未执行的监听器也应生效。
capture = root.on("click", model.capture, "capture")
target = button.on("click", model.target)
bubble = root.on("click", model.bubble)
diff --git a/examples/exit-lifecycle.endor b/examples/exit-lifecycle.endor
index fe2f6fe..a7f153e 100644
--- a/examples/exit-lifecycle.endor
+++ b/examples/exit-lifecycle.endor
@@ -1,17 +1,24 @@
+// 正常退出与多窗口卸载示例:同一 ExitText 出现在两窗,onUnmount 按出现位置分别调用。
+// 退出先取消任务、关闭窗口,再进行允许的卸载回调;已停止 VM 的清理不能重新进入源代码。
+// 在原生文字类型上增加共享日志,用来观察每个 Mount 的卸载通知。
class ExitText: TextBlock {
log: Object
+ // 每次真实卸载递增同一 log;普通重绘不调用,重复 shutdown 也不应重复调用。
function onUnmount(self: ExitText, mount: Mount): Unit {
self.log.increment()
}
}
log = ui.state(0)
+// 文字业务对象只创建一次,Page 与另一窗都引用它,但两边有各自的 Mount 生命周期。
watched = ExitText("关闭副窗口可观察卸载计数;退出应用也会卸载剩余位置",2,log)
page = Page()
page.title = "退出生命周期"
page.render(Column(24,16,watched,ui.textOf(log,"value")))
+// 创建同 VM 内的另一原生窗口;迁移 other 后仍保留 watched 的业务身份。
second = Window()
second.setTitle("Exit lifecycle secondary")
other = Page()
other.render(watched)
other.moveToWindow(second)
+// 应用级长定时任务用于观察退出会取消尚未完成的工作,不让退出过程等满 30 秒。
pending = ui.sleep(ui,"pending",30000)
diff --git a/examples/field-contracts.endor b/examples/field-contracts.endor
index d841082..a6c683d 100644
--- a/examples/field-contracts.endor
+++ b/examples/field-contracts.endor
@@ -1,12 +1,18 @@
+// 字段类型契约示例:CounterText 同时有原生文本载荷和源 Number 字段。
+// 绑定显示读取该字段,GUI 回调与 Console 赋值受同一契约约束。
+// 增加 value:Number;构造时先传原生 text,再传源字段值。
class CounterText: TextBlock {
value: Number
+ // 收到 click 后把已有数值改为 10,依赖文字随之更新,不修改原生 text 内容。
function change(self: CounterText, event: Event): Unit {
self.value = 10
}
}
+// 声明 target 必须是 Block,可接收 CounterText,因为它是 TextBlock 的子类。
class Model {
target: Block
}
+// 构造唯一受检对象,Model、事件回调和 textOf 均引用它。
counter = CounterText("共享的原生子类对象", 9)
model = Model(counter)
button = Button("把 Number 字段改成 10")
diff --git a/examples/flex-constraints.endor b/examples/flex-constraints.endor
index 8d32743..669f7b2 100644
--- a/examples/flex-constraints.endor
+++ b/examples/flex-constraints.endor
@@ -1,11 +1,17 @@
+// flexBasis/flexMin/flexMax/flexShrink 约束同一主轴分配。
+// 增长达到上限或收缩达到下限后,剩余空间/缺额重新分给其他尚可调整的项,固定项保持 60。
+// 给 Row 一个有限 340x60 的期望尺寸,便于精确观察分配数值。
class Strip: Row {
+ // 源测量返回固定尺寸;后续排列仍使用原生 Row 的 padding/gap 与 flex 规则。
function measure(self: Strip, mount: Mount, constraints: Constraints): MeasureResult {
return MeasureResult(340, 60)
}
}
+// 分别持有两个可调整矩形,按钮切换增长/收缩策略,而不是手动写最终 bounds。
class Actions {
first: RectBlock
second: RectBlock
+ // 两项 basis=100,按1:3分增长;首项 max=105 触顶后,多余增长转给第二项。
function grow(self: Actions): Unit {
self.first.flexMin = 0
self.first.flexMax = 105
@@ -14,6 +20,7 @@ class Actions {
self.first.flex = 1
self.second.flex = 3
}
+ // 两项 basis=200 造成缺额,以 flexShrink*basis 分摊;首项 min=150 后第二项继续收缩到90。
function shrink(self: Actions): Unit {
self.first.flexMax = 300
self.first.flexMin = 150
@@ -25,6 +32,7 @@ class Actions {
self.second.flexShrink = 1
}
}
+// 自然宽20/40与basis可不同;fixed不设置权重,保持原始60宽。
first = RectBlock(20, 20, 4, "#A4C8F0")
second = RectBlock(40, 20, 4, "#A6D8B2")
fixed = RectBlock(60, 20, 4, "#E8C990")
@@ -33,6 +41,7 @@ grow = Button("增长:上限后重新分配")
grow.onClick(actions.grow)
shrink = Button("收缩:最小尺寸后重新分配")
shrink.onClick(actions.shrink)
+// 先初始化增长策略再挂载,首次发布即采用105/135/60,不需等待按钮点击。
actions.grow()
strip = Strip(10, 10, first, second, fixed)
page = Page()
diff --git a/examples/flex.endor b/examples/flex.endor
index 3902161..cd475e9 100644
--- a/examples/flex.endor
+++ b/examples/flex.endor
@@ -1,8 +1,14 @@
+// 按 flex 权重分配有限主轴空间:固定项先占自然尺寸,剩余空间按正权重划分。
+// paragraph 只有一份业务内容,但在两个 Row 的 Mount 中各自按真实分配宽度测量/换行。
+// 只持有首个 Row 的 filler,修改它的权重来间接改变同排文字分配。
class Actions {
filler: RectBlock
+ // 将 filler 权重恢复为 1,与 paragraph=1 近似等分可用空间。
function balanced(self: Actions): Unit { self.filler.flex = 1 }
+ // filler 权重改为 3,paragraph 在首排获得剩余空间的四分之一,换行高度随之增加。
function narrow(self: Actions): Unit { self.filler.flex = 3 }
}
+// 共享文字的 flex=1 是业务布局策略,具体几何与自然尺寸缓存仍属于各 Mount。
paragraph = TextBlock("GUI 和代码操控同一套 Aster 内存对象系统。对象只有一份,但每次挂载都有自己的约束和布局。这里用同一个 TextBlock 在两个容器中显示不同的换行。修改权重会重新测量文字高度,随后更新相邻布局。GUI 和代码操控同一套 Aster 内存对象系统。对象只有一份,但每次挂载都有自己的约束和布局。这里用同一个 TextBlock 在两个容器中显示不同的换行。修改权重会重新测量文字高度,随后更新相邻布局。")
paragraph.flex = 1
filler = RectBlock(20, 20)
@@ -12,6 +18,7 @@ balanced = Button("等比例分配")
balanced.onClick(actions.balanced)
narrow = Button("文字只占四分之一")
narrow.onClick(actions.narrow)
+// 首排两个可伸缩子项,次排只有文字 flex 与固定 300px 兄弟,因此首排按钮不会改次排宽度。
first = Row(10, 10, paragraph, filler)
second = Row(10, 10, paragraph, RectBlock(300, 20))
page = Page()
diff --git a/examples/fonts.endor b/examples/fonts.endor
index 3c994ca..808e6fb 100644
--- a/examples/fonts.endor
+++ b/examples/fonts.endor
@@ -1,5 +1,11 @@
+// 字体测量示例:中英文混排、重音字符和不同字宽均走系统字体塑形与回退。
+// 布局使用实际字形宽度,iiii 与 WWWW 后的色块位置应不同,不能用字符数量估算宽度。
+// 字号2与独立颜色用于标题,中文是否可见取决于运行环境可用CJK字体。
let title = ui.text("Hello Endor · 你好,Tessera", 2, "#242422")
+// 同一文字控件包含中英文和重音字符,用于观察字体回退与基线。
let detail = ui.text("中文 English 123 — café", 1, "#606060")
+// 两组相同尺寸色块跟在窄/宽字符后,12px gap 保持一致,差异来自真实文字测量。
let row = ui.row(0, 12, ui.text("iiii"), ui.rect(24, 20, 4, "#DCE8FF"), ui.text("WWWW"), ui.rect(24, 20, 4, "#DCE8FF"))
+// 根Column统一padding和间距,render后这些对象才有具体Mount与绘图命令。
let page = ui.page()
page.render(ui.column(24, 12, title, detail, row))
diff --git a/examples/function-types.endor b/examples/function-types.endor
index b45f4c9..f7a2c3d 100644
--- a/examples/function-types.endor
+++ b/examples/function-types.endor
@@ -1,13 +1,22 @@
+// Function 与 Type 的契约示例:源函数、原生函数与绑定方法都可作为函数值,类型构造器保持 Type 身份。
+// 参数和返回注解在真实调用边界校验,事件注册同样使用这些签名。
+// Number 输入原样返回,用于说明参数类型在进入函数体前检查。
function identity(value: Number): Number { return value }
+// 接受 Function 值并零参数调用,结果仍需符合本函数的 Number 返回契约。
function callNumber(callback: Function): Number { return callback() }
+// 接受声明 Visual 能力的对象,返回 Block;调用时两侧契约都要成立。
function visual(value: Visual): Block { return value }
+// 保留共享 state,read 作为普通绑定函数使用,click 作为接收 Event 的事件回调。
class Actions {
state: Object
+ // 读取模型值而不修改业务状态,保存 actions.read 时接收者已绑定。
function read(self: Actions): Number { return self.state.value }
+ // 事件入口可忽略 Event 内容,但参数契约仍要求它是 Event,再修改同一 state。
function click(self: Actions, event: Event): Unit { self.state.increment() }
}
state = ui.state(0)
actions = Actions(state)
+// 保存绑定方法对象,之后无参调用仍使用 actions 作为 self;不是重新按字符串查找对象。
savedFunction = actions.read
initial = identity(callNumber(savedFunction))
button = Button("调用带类型检查的对象方法")
diff --git a/examples/gc-lifecycle.endor b/examples/gc-lifecycle.endor
index fd972bc..b4c9e1e 100644
--- a/examples/gc-lifecycle.endor
+++ b/examples/gc-lifecycle.endor
@@ -1,12 +1,13 @@
-// Record ui.stats().objects, call mountCycle(), then inspect phase.value and
-// pending.value.status(). Close the tab: the referenced Block still waits.
-// releaseCycle() removes both external references; the next objects query
-// should return to the same baseline before the five-minute wait finishes.
+// 托管对象循环与等待帧回收:先记 ui.stats().objects,再调用 mountCycle,观察 phase 与 pending.value.status()。
+// 关闭临时页仍可因业务引用保留等待任务;releaseCycle 清掉外部引用后,缓存不应让整个引用环常驻。
+// 比较操作前后对象数时,关闭页面只解除展示根;holder/pending 外部引用也释放后才应回到基线。
holder = ui.state(0)
pending = ui.state(0)
phase = ui.state(0)
+// 通过 peer 指回包含自己的 Page,配合等待协程形成包含视觉缓存读依赖的引用环。
class CycleRect: RectBlock {
peer: Object
+ // sleep 归属该 rect,await 保留协程帧;先写 phase=1,只有实际恢复后才写 2 并改 Page 标题。
function wait(self: CycleRect): Unit {
gate = ui.sleep(self,"sleep",300000)
phase.set(1)
@@ -14,15 +15,18 @@ class CycleRect: RectBlock {
phase.set(2)
self.peer.title = "等待已结束"
}
+ // 故意读取 peer.title,让布局依赖包含循环中的 Page;缓存应借用观察,不增加长期 GC 根。
function measure(self: CycleRect, mount: Mount, constraints: Constraints): MeasureResult {
observed = self.peer.title
return MeasureResult(180,40)
}
+ // 绘制也读取 peer.title,检查绘制缓存与等待帧不会共同意外保活循环。
function paint(self: CycleRect, mount: Mount, geometry: Bounds): DisplayList {
observed = self.peer.title
return RectBlock.paint(self,mount,geometry)
}
}
+// 创建临时 Page 和 CycleRect,相互引用后挂载,再把 Page/Task 分别放入两个外部 holder。
function mountCycle(): Unit {
transient = Page()
transient.title = "临时引用环"
@@ -32,11 +36,13 @@ function mountCycle(): Unit {
phase.set(0)
pending.set(ui.spawn(rect,"workflow",rect.wait))
}
+// 先关闭临时页,随后去掉两个外部引用;等待依赖无需等满五分钟,失去可达性后应整体回收。
function releaseCycle(): Unit {
holder.value.close()
holder.set(0)
pending.set(0)
}
+// 基线页面始终存在,用它比较操作前后的托管对象数,不把临时页面当作基线。
base = Page()
base.title = "GC 基线"
base.render(TextBlock("关闭临时 Page 后应回收引用环",2))
diff --git a/examples/global-dependencies.endor b/examples/global-dependencies.endor
index e09cccc..4e4091d 100644
--- a/examples/global-dependencies.endor
+++ b/examples/global-dependencies.endor
@@ -1,11 +1,16 @@
+// 全局名称依赖示例:paint 读取 color 这个具体名字,而非依赖整个 globals 字典版本。
+// Console 求值 42 仅改变最后结果 _,不应重绘;给 color 赋新值才使绘制失效。
color = "#ED6A5A"
+// 保持原生几何,覆盖颜色策略,使示例只观察全局读取的依赖粒度。
class GlobalPaint: RectBlock {
+ // 调用原生基类产出新命令,读取当前 color 并写到新命令;不在只读阶段修改全局。
function paint(self: GlobalPaint, mount: Mount, geometry: Bounds): DisplayList {
drawing = RectBlock.paint(self,mount,geometry)
drawing.commands.at(0).color = color
return drawing
}
}
+// 显示固定大小矩形和操作提示,后续通过 Console 改同一个会话的 color。
rect = GlobalPaint(480,120,12,"#DDEEFF")
page = Page()
page.title = "全局名字依赖"
diff --git a/examples/hello.endor b/examples/hello.endor
index 63aa6d3..11fcf45 100644
--- a/examples/hello.endor
+++ b/examples/hello.endor
@@ -1,10 +1,10 @@
-function heading(value: Text): View {
- return ui.text(value, 2, "#242422")
+function heading(value: Text): View { // 辅助函数把字符串包装成大字号 TextBlock;返回值仍是托管视觉引用。
+ return ui.text(value, 2, "#242422") // 这里只创建文字对象,具体宽高由实际字体和出现位置约束测量。
}
-function main(): View {
- let title = heading("Hello Endor")
- let rectangle = ui.rect(240, 80, 12, "#DCE8FF")
- let detail = ui.row(0, 12, ui.text("Aster VM"), ui.spacer(8), ui.text("Tessera UI"))
- return ui.column(24, 12, title, rectangle, detail)
+function main(): View { // 兼容模块入口 main 返回视觉根,宿主负责把它显示到页面,不需要源代码显式 render。
+ let title = heading("Hello Endor") // 调用上述辅助函数,局部变量持有同一托管文字对象。
+ let rectangle = ui.rect(240, 80, 12, "#DCE8FF") // 矩形使用逻辑尺寸,圆角与颜色由原生载荷保存。
+ let detail = ui.row(0, 12, ui.text("Aster VM"), ui.spacer(8), ui.text("Tessera UI")) // Row 依次摆放文字、固定间隔和文字;共享几何仍由每次出现位置承担。
+ return ui.column(24, 12, title, rectangle, detail) // Column 的 24px 内边距和 12px 间隔决定最终命令位置;返回后发布快照拥有所需数据。
}
diff --git a/examples/layout-cache.endor b/examples/layout-cache.endor
index 4c07404..36f17b7 100644
--- a/examples/layout-cache.endor
+++ b/examples/layout-cache.endor
@@ -1,32 +1,44 @@
+// 测量与绘制依赖分离:改颜色应复用尺寸缓存,改模型宽度才需要重新测量。
+// ViewportBand 还显式读取实际页面视口,使原生 resize 可通过依赖触发重测。
+// 只负责测量需要的宽度,和绘制颜色分成两个业务对象以便观察独立失效。
class WidthModel {
width: Number
+ // 点击把模型宽度置为 260;源 measure 读取过该字段,缓存因此失效。
function expand(self: WidthModel, event: Event): Unit {
self.width = 260
}
}
+// 保存 paint 使用的颜色,不参与 CachedRect 的测量。
class Tint {
color: String
+ // 只改颜色;正常情况下需重绘而不重新执行无关源测量。
function blue(self: Tint, event: Event): Unit {
self.color = "#526AC7"
}
}
+// 原生矩形的尺寸/颜色策略分别由源协议覆盖,业务模型通过字段持有。
class CachedRect: RectBlock {
dimensions: WidthModel
tint: Tint
+ // 读取 dimensions.width 返回固定高 60;实际读观察由运行时缓存记录,不手工声明依赖表。
function measure(self: CachedRect, mount: Mount, constraints: Constraints): MeasureResult {
return MeasureResult(self.dimensions.width, 60)
}
+ // 复用原生矩形命令,再改新命令的颜色;修改新结果被允许,但 paint 不能修改已有业务对象。
function paint(self: CachedRect, mount: Mount, geometry: Bounds): DisplayList {
drawing = RectBlock.paint(self, mount, geometry)
drawing.commands.at(0).color = self.tint.color
return drawing
}
}
+// 宽度来自 ui.viewport 的当前逻辑页面区域,显示窗口变化也能成为测量依赖。
class ViewportBand: RectBlock {
+ // 返回可用视口宽度,最终仍服从父容器约束;外层 padding 会减少分配宽度。
function measure(self: ViewportBand, mount: Mount, constraints: Constraints): MeasureResult {
return MeasureResult(ui.viewport().width, 24)
}
}
+// 创建相互独立的宽度/颜色模型并绑定两个按钮,用相同对象观察缓存与可见结果。
dimensions = WidthModel(200)
tint = Tint("#ED6A5A")
rect = CachedRect(80, 20, 0, "#ED6A5A", dimensions, tint)
diff --git a/examples/layout-negotiation.endor b/examples/layout-negotiation.endor
index 82b806c..ac96927 100644
--- a/examples/layout-negotiation.endor
+++ b/examples/layout-negotiation.endor
@@ -1,24 +1,34 @@
+// 测量/排列协商示例:arrange 指定子宽后需重新换行测量,高度变化继续影响父容器和后继位置,直到整树稳定。
+// paragraph 被两个容器共享;每个 Mount 使用独立约束,不能把新宽度写回共享文字状态。
+// 保存第一个段落出现位置的分配宽度,按钮只改策略值。
class Policy {
width: Number
+ // 选择 280 逻辑像素宽度,段落变高,后继文字下移。
function narrow(self: Policy, event: Event): Unit { self.width = 280 }
+ // 选择 560 逻辑像素宽度,段落变矮;另一个容器宽度不跟着改变。
function wide(self: Policy, event: Event): Unit { self.width = 560 }
}
+// 在原生 Column 排列基础上覆盖第一个稳定子槽的宽度。
class ArrangedColumn: Column {
policy: Policy
+ // 先取得包含全部子槽的 Arrangement,仅改第一子项分配;保留其他槽使排列验证完整。
function arrange(self: ArrangedColumn, mount: Mount, geometry: Bounds): Arrangement {
result = Column.arrange(self, mount, geometry)
result.children.at(0).width = self.policy.width
return result
}
}
+// 通过源 measure 改整个容器的期望宽度,以对比 arrange 分配路径。
class MeasuredColumn: Column {
width: Number
+ // 调用原生测量后修改新结果 width;这个新结果不属于已有业务对象,允许在只读计算中填写。
function measure(self: MeasuredColumn, mount: Mount, constraints: Constraints): MeasureResult {
result = Column.measure(self, mount, constraints)
result.width = self.width
return result
}
}
+// 两个容器使用同一个 paragraph,分别由 Policy=280 与 measure.width=360 约束。
policy = Policy(280)
paragraph = TextBlock("源码 arrange 改变分配宽度后,文字会按实际宽度重新测量。增加的高度继续影响容器和相邻对象,直到整棵布局稳定后才发布。这里与下面使用的是同一个 TextBlock;每个 Mount 都有自己的约束与几何。同一套内存对象不需要复制业务状态,也不通过丢弃传播来结束更新。")
tail = TextBlock("这一行跟随上方段落。")
@@ -26,10 +36,12 @@ arranged = ArrangedColumn(0, 12, paragraph, tail, policy)
measured = MeasuredColumn(0, 12, paragraph, TextBlock("这一段的宽度来自源码 measure。"), 360)
narrow = Button("arrange 为 280")
wide = Button("arrange 为 560")
+// 保留事件订阅引用,两个按钮调用同一 Policy 的不同方法。
narrowConnection = narrow.on("click", policy.narrow)
wideConnection = wide.on("click", policy.wide)
page = Page()
page.title = "约束协商"
page.render(Column(24, 16, Row(0, 12, narrow, wide), arranged, measured))
+// 成功输入结束后宿主才消费原生窗口命令;窗口实际尺寸再回传为下一次布局约束。
ui.window().resize(850, 700)
ui.window().setTitle("测量与排列直到稳定")
diff --git a/examples/main.endor b/examples/main.endor
index 6c17a7d..3859ea5 100644
--- a/examples/main.endor
+++ b/examples/main.endor
@@ -1,5 +1,9 @@
+// 与 page.endor 等价的 main 返回根入口:用于对比顶层显式 render 和兼容模块入口的绘图结果。
+// 返回 Column 视觉对象,宿主选择/挂载页面;这里的 title 和 rectangle 只在 VM 内建立业务引用。
function main(): View {
+ // 文字内容与矩形参数和 page 示例一致,最终绘图命令应等价。
let heading = ui.text("Hello Endor", 2, "#242422")
let rectangle = ui.rect(240, 80, 12, "#DCE8FF")
+ // 布局边距与间隔保持一致;不同 VM 的对象/Mount 身份独立,命令相同不表示共享对象。
return ui.column(24, 12, heading, rectangle)
}
diff --git a/examples/managed-types.endor b/examples/managed-types.endor
index 91bc438..9e2670b 100644
--- a/examples/managed-types.endor
+++ b/examples/managed-types.endor
@@ -1,9 +1,14 @@
+// 托管名义类型入门:ui 构造器与 TextBlock/Button/Page 返回同一套 VM 托管对象。
+// 两个文字绑定读同一 counter;原生点击和 Console 都能操作该对象并传播显示变化。
+// state 提供可写 value 和 increment 方法,textOf 建立显式字段到文本的依赖。
counter = ui.state(9)
first = ui.textOf(counter, "value", 2)
second = ui.textOf(counter, "value", 2)
title = TextBlock("Endor:同一份对象,代码与 GUI 共同操作", 2)
button = Button("增加计数")
+// 零参数源回调委托原生 state.increment;事件分发器根据签名调用,不强塞 Event 参数。
function increment(): Unit { counter.increment() }
+// 把实际函数作为回调而非函数名文本;render 之前只创建业务对象,render 后才有出现位置。
button.onClick(increment)
page = Page()
page.render(Column(24, 16, title, Row(0, 16, first, second, button)))
diff --git a/examples/method-contracts.endor b/examples/method-contracts.endor
index 28432af..19b7dff 100644
--- a/examples/method-contracts.endor
+++ b/examples/method-contracts.endor
@@ -1,23 +1,33 @@
+// 方法覆盖遵守参数可更宽、返回可更具体的规则。
+// WideRect 复用基类原生测量后返回 Sized 子类结果;事件改变基础 width 后按同一路径重测。
+// MeasureResult 的名义子类,不增加字段,用于演示返回值协变。
class Sized: MeasureResult {}
+// 用源方法显式委托原生测量,保留 Mount/Constraints 的基类契约。
class BaseRect: RectBlock {
+ // 原生方法按给定出现位置与约束测量,不把几何存进共享对象。
function measure(self: BaseRect, mount: Mount, constraints: Constraints): MeasureResult {
return RectBlock.measure(self, mount, constraints)
}
}
+// 覆盖参数放宽为 Object,返回缩窄为 Sized;不能反过来缩窄参数或放宽返回。
class WideRect: BaseRect {
+ // 调用基类后复制测量数值到新的 Sized;新结果仍满足 MeasureResult 协议。
function measure(self: WideRect, mount: Object, constraints: Object): Sized {
measured = BaseRect.measure(self, mount, constraints)
return Sized(measured.width, measured.height)
}
}
+// 保存具体 WideRect 和计数器,使宽度修改和点击次数同时作为业务变化传播。
class Actions {
rect: WideRect
count: Object
+ // 先改 width 再递增计数,新的布局刷新会使用上述动态测量覆盖。
function widen(self: Actions): Unit {
self.rect.width = 260
self.count.increment()
}
}
+// 初始矩形宽 180;按钮事件完成后应变为 260,同时文字显示计数。
rect = WideRect(180, 60, 8, "#ED6A5A")
count = ui.state(0)
actions = Actions(rect, count)
diff --git a/examples/mount-lifecycle.endor b/examples/mount-lifecycle.endor
index 4acd2cc..40f19a7 100644
--- a/examples/mount-lifecycle.endor
+++ b/examples/mount-lifecycle.endor
@@ -1,3 +1,6 @@
+// 业务对象与出现位置生命周期分离:同一 child 在根中出现两次,每次都获得独立 Mount 和通知。
+// 普通数据变化不会重新挂载;页面关闭时子项先卸载,父项随后卸载,旧 Mount 不因重开而复活。
+// order 是统一时序,mounted/unmounted 是子项次数;latest 保存最近通知的实际 Mount。
class LifecycleLog {
order: Object
mounted: Object
@@ -8,8 +11,10 @@ class LifecycleLog {
childUnmounted: Number
latest: Object
}
+// 共享文字用 log 记录两个出现位置,源钩子接收的是当前具体 Mount。
class WatchedText: TextBlock {
log: LifecycleLog
+ // 记录顺序和次数,保存 Mount 并修改原文字;运行时在回调后重新收敛才发布。
function onMount(self: WatchedText, mount: Mount): Unit {
self.log.order.increment()
self.log.mounted.increment()
@@ -17,6 +22,7 @@ class WatchedText: TextBlock {
self.log.latest.set(mount)
self.setText("同一个对象,两个独立挂载位置")
}
+ // 记录已经脱离的出现位置;保留 Mount 可查询 mounted,但不能再当有效 bounds 使用。
function onUnmount(self: WatchedText, mount: Mount): Unit {
self.log.order.increment()
self.log.unmounted.increment()
@@ -24,17 +30,21 @@ class WatchedText: TextBlock {
self.log.latest.set(mount)
}
}
+// 父容器也记录钩子序号,便于对比挂载与子先父后的卸载顺序。
class WatchedColumn: Column {
log: LifecycleLog
+ // 记录父挂载顺序,不修改容器结构;子项随后各自执行通知。
function onMount(self: WatchedColumn, mount: Mount): Unit {
self.log.order.increment()
self.log.parentMounted = self.log.order.value
}
+ // 父卸载时子项卸载已完成,序列号应位于所有子项通知之后。
function onUnmount(self: WatchedColumn, mount: Mount): Unit {
self.log.order.increment()
self.log.parentUnmounted = self.log.order.value
}
}
+// 多个 state 容器分别保存计数/最后 Mount,root 最后参数是源 log 字段,前面才是可变视觉子项。
log = LifecycleLog(ui.state(0), ui.state(0), ui.state(0), 0, 0, 0, 0, ui.state(0))
child = WatchedText("等待挂载", 2, log)
root = WatchedColumn(24, 16, child, child, TextBlock("挂载次数"), ui.textOf(log.mounted, "value"), TextBlock("卸载次数"), ui.textOf(log.unmounted, "value"), log)
diff --git a/examples/native-signatures.endor b/examples/native-signatures.endor
index a86ebe1..71b30ce 100644
--- a/examples/native-signatures.endor
+++ b/examples/native-signatures.endor
@@ -1,12 +1,19 @@
+// 原生回调也发布 Function 签名:state.set 接收一个 Event,increment 接收零参数。
+// 同一事件注册机制按实际签名调用;label 绑定再把事件 kind 转成可显示的字符串。
+// 保留最后一次收到的 Event 对象,初值 kind=waiting;不是只存一份格式化事件文本。
received = ui.state(Event("waiting"))
count = ui.state(0)
+// 只读计算提取当前事件种类,作为 label.text 绑定的依赖。
function eventKind(): String { return received.value.kind }
label = TextBlock("waiting")
subscription = label.bind("text", eventKind)
withEvent = Button("传递 Event 给原生 set")
+// received.set 是一参数原生方法,分发器传入实际 click Event。
firstConnection = withEvent.on("click", received.set)
withoutEvent = Button("调用零参数原生 increment")
+// count.increment 的签名为零参数,分发器调用时不传多余 Event。
secondConnection = withoutEvent.on("click", count.increment)
+// 把已绑定状态与两种回调放到同一页面,便于对比输入效果。
page = Page()
page.title = "原生方法签名"
page.render(Column(24, 16, TextBlock("源码和原生回调共用参数契约"), label, ui.textOf(count, "value"), withEvent, withoutEvent))
diff --git a/examples/native-subclasses.endor b/examples/native-subclasses.endor
index cfd2c02..8b8735c 100644
--- a/examples/native-subclasses.endor
+++ b/examples/native-subclasses.endor
@@ -1,33 +1,43 @@
+// 源类继承原生视觉类型:原生载荷负责基础绘制,新增字段和覆盖方法由源类提供。
+// label 在树中出现多次但仍是同一对象,原生方法和源字段写入共同更新各出现位置。
+// 扩展 TextBlock 的业务字段;其文本/颜色仍由原生父类存储与绘制。
class StatusText: TextBlock {
activeColor: String
+ // 用继承的 setText/setColor 改原对象;activeColor 是构造时追加的源字段。
function activate(self: StatusText, event: Event): Unit {
self.setText("继承的原生文本已更新")
self.setColor(self.activeColor)
}
}
+// 继续继承源子类,添加 message 并覆盖事件入口,保留 TextBlock 的原生能力。
class CustomStatus: StatusText {
message: String
+ // 显式调用基类 setText 以示范原生未绑定方法的接收者传递,再用继承方法改颜色。
function activate(self: CustomStatus, event: Event): Unit {
TextBlock.setText(self, self.message)
self.setColor(self.activeColor)
}
}
+// 按钮保存目标 Block,事件到来时把同一 Event 转交给目标的动态 activate。
class ActionButton: Button {
target: Block
+ // 不复制目标或创建新展示对象,直接调用目标业务方法,选择实际类型的覆盖实现。
function activate(self: ActionButton, event: Event): Unit {
self.target.activate(event)
}
}
+// Page 也可增加源字段;caption 用于构建页内标题,不替代原生窗口标题。
class CustomPage: Page {
caption: String
}
+// 前面位置参数供原生 TextBlock 构造,后面的颜色/消息供继承字段;所有引用都保留托管类型。
label = CustomStatus("点击按钮更新同一个自定义文本对象", 2, "#242422", "#237A45", "源码子类与原生载荷共同工作")
button = ActionButton("调用源码子类方法", label)
button.onClick(button.activate)
diff --git a/examples/owned-tasks.endor b/examples/owned-tasks.endor
index 47d64b5..1ec05de 100644
--- a/examples/owned-tasks.endor
+++ b/examples/owned-tasks.endor
@@ -1,15 +1,22 @@
+// owner 作用域决定取消时机:Page 关闭会取消其任务,隐藏 Block 和 ui 的任务可继续。
+// 每个 owner 下同 key 的重复点击替换旧定时器;任务生命周期不由画面是否可见推断。
+// 把 owner 与要修改的 state 分开持有,使同一个调度方法可用于三个作用域。
class Actions {
owner: Object
state: Object
+ // 定时任务成功交付后修改共享计数,零参数签名符合 timer 回调要求。
function complete(self: Actions): Unit { self.state.increment() }
+ // 每次启动 1500ms 的 load 定时器;相同 owner/key 只有最新任务继续等待。
function schedule(self: Actions, event: Event): Unit {
ui.after(self.owner, "load", 1500, self.complete)
}
}
state = ui.state(0)
+// 该 Block 从未进入页面树,但全局引用仍持有它,因此它拥有的任务可执行。
hidden = TextBlock("这个 Block 没有挂载,也能拥有任务")
page = Page()
page.title = "任务生命周期"
+// 三个 Action 共享一个 state,却分别归属 Page、Block、应用,便于比较关闭页面的取消边界。
pageActions = Actions(page, state)
blockActions = Actions(hidden, state)
appActions = Actions(ui, state)
@@ -20,5 +27,6 @@ pageConnection = pageButton.on("click", pageActions.schedule)
blockConnection = blockButton.on("click", blockActions.schedule)
appConnection = appButton.on("click", appActions.schedule)
page.render(Column(24, 16, TextBlock("1.5 秒后更新同一个状态对象;重复点击只保留最新任务"), ui.textOf(state, "value"), pageButton, blockButton, appButton))
+// 额外应用级任务延迟 500ms,用于展示没有点击也可按 worker 空闲轮询交付。
initial = ui.after(ui, "initial", 500, appActions.complete)
ui.window().setTitle("Owned Tasks")
diff --git a/examples/page.endor b/examples/page.endor
index 89607e7..82fb1c5 100644
--- a/examples/page.endor
+++ b/examples/page.endor
@@ -1,4 +1,4 @@
-let heading = ui.text("Hello Endor", 2, "#242422")
-let rectangle = ui.rect(240, 80, 12, "#DCE8FF")
-let myui = ui.page()
-myui.render(ui.column(24, 12, heading, rectangle))
+let heading = ui.text("Hello Endor", 2, "#242422") // 顶层脚本不需要 main:先创建文字对象;字号 2 和十六进制颜色传给原生文字构造器。
+let rectangle = ui.rect(240, 80, 12, "#DCE8FF") // 创建固定逻辑尺寸矩形;12 为圆角半径,业务对象尚未出现在任何页面中。
+let myui = ui.page() // 创建托管 Page 资源;它负责持有展示根和页面生命周期。
+myui.render(ui.column(24, 12, heading, rectangle)) // 把文字和矩形放入 Column,padding=24、gap=12;render 选择此页并生成出现位置及发布帧。
diff --git a/examples/presentable.endor b/examples/presentable.endor
index 54ea7c1..ef25a07 100644
--- a/examples/presentable.endor
+++ b/examples/presentable.endor
@@ -1,27 +1,38 @@
+// 普通业务对象显式实现 Presentable 后,ui.present 可调用其 present(window) 生成视觉对象。
+// 生成的视图保存 source 指向业务模型,绑定和按钮继续使用原对象,展示不是业务数据副本。
+// name/color 是业务字段,零参读取方法提供只读绑定,update 提供事件修改。
class Product {
name: String
color: String
+ // 返回名称并记录实际属性读取,后续 name 修改会使文本绑定重算。
function label(self: Product): String { return self.name }
+ // 只读返回颜色,绑定到新建色块的 color。
function shade(self: Product): String { return self.color }
+ // 点击更新原业务对象,现有绑定会同步文字和矩形,不重新调用整个 present 构造树。
function update(self: Product, event: Event): Unit {
self.name = "原业务对象已更新"
self.color = "#237A45"
}
+ // 在指定 Window 上下文创建文字、色块和按钮,连接原 self 的方法后返回组合根。
function present(self: Product, window: Window): Block {
title = TextBlock(self.name)
swatch = RectBlock(180,48,8,self.color)
+ // 绑定令牌本身不承载另一份业务状态,绑定计算仍调用同一 Product。
titleBinding = title.bind("text",self.label)
colorBinding = swatch.bind("color",self.shade)
button = Button("调用业务对象方法")
connection = button.on("click",self.update)
result = Column(24,16,title,swatch,button)
+ // 保留原始业务身份,便于代码从展示根回到来源;contextWindow 仅记录创建时的窗口编号。
result.source = self
result.contextWindow = window.id()
return result
}
}
+// 显式声明能力并验证方法契约;只有同名 present 不会自动成为 Presentable。
ui.implement(Product,Presentable)
product = Product("普通对象的显式展示能力", "#526AC7")
+// 先获得视觉根,再由 ui.open 把它挂载到页面;最后设置 Page 标题。
view = ui.present(product)
page = ui.open(view)
page.title = "Presentable"
diff --git a/examples/property-bindings.endor b/examples/property-bindings.endor
index 356e6b9..384c093 100644
--- a/examples/property-bindings.endor
+++ b/examples/property-bindings.endor
@@ -1,10 +1,17 @@
+// 通用属性绑定:业务 Model 的三个零参只读方法分别计算 rect.width、rect.color 和 label.text。
+// 绑定在目标隐藏时也会更新;两个页面共享同一组 Block,不需要两份模型。
+// width/color/label 是唯一业务来源,绑定计算只读,expand 是显式事件写入入口。
class Model {
width: Number
color: String
label: String
+ // 返回 Number,目标 width 属性同时做类型/尺寸验证。
function getWidth(self: Model): Number { return self.width }
+ // 返回颜色文本;依赖只包含实际读取的模型属性。
function getColor(self: Model): String { return self.color }
+ // 返回当前说明文字,供 TextBlock.text 绑定使用。
function getLabel(self: Model): String { return self.label }
+ // 一个事件同时改三个属性,随后绑定传播,布局与绘制在完整刷新后发布。
function expand(self: Model, event: Event): Unit {
self.width = 260
self.color = "#237A45"
@@ -14,6 +21,7 @@ class Model {
model = Model(120, "#526AC7", "点击更新共享对象")
rect = RectBlock(120, 40, 8, "#526AC7")
label = TextBlock("初始化")
+// 保存三个可断开的绑定令牌;令牌不强持有隐藏目标,目标仍需由业务对象图保留。
widthBinding = rect.bind("width", model.getWidth)
colorBinding = rect.bind("color", model.getColor)
labelBinding = label.bind("text", model.getLabel)
@@ -23,6 +31,7 @@ root = Column(24, 16, label, rect, button)
page = Page()
page.title = "通用属性绑定"
page.render(root)
+// 第二页面使用同一根容器,切页只更换 Mount 展示上下文;最后 activate 回第一页面。
second = Page()
second.title = "同一组 Block"
second.render(root)
diff --git a/examples/scene-viewports.endor b/examples/scene-viewports.endor
index a7e9e92..0897bce 100644
--- a/examples/scene-viewports.endor
+++ b/examples/scene-viewports.endor
@@ -1,7 +1,10 @@
-// The scene has world positions; each finite viewport has its own camera.
+// 一个二维世界可被多个有限视口观察,Scene2D 的对象位置共享,默认 Camera2D 各自独立。
+// 内层视口也可作为世界子项;HUD 在外层 Overlay 中,所以不会随世界相机变换。
+// 场景保存世界对象,视口负责有限窗口与相机变换。
count = ui.state(0)
increment = Button("共享计数 +1")
increment.onClick(count.increment)
+// 业务卡片带世界 x/y 和边框,count 文字与按钮共同保留同一 state。
card = Column(20, 12, TextBlock("世界里的 UI", 2), ui.textOf(count, "value", 3), increment)
card.boxWidth = 220
card.x = -60
@@ -9,17 +12,21 @@ card.y = 40
card.borderLeft = 2
card.borderColor = "#8CA5B8"
+// 有限内层视口看自己的小场景,再以世界坐标放进外层 Scene2D。
inner = CanvasViewport(200, 100, Scene2D(RectBlock(120, 60, 8, "#D4E6D9")))
inner.x = 260
inner.y = 40
+// 两个外层视口引用同一 world,卡片/内层视口因此各产生两个出现位置。
world = Scene2D(card, inner)
+// 左右视口尺寸和相机平移/缩放不同;显式赋同一 camera 才会共享导航状态。
left = CanvasViewport(460, 320, world)
left.camera.x = -100
right = CanvasViewport(320, 320, world)
right.camera.x = -80
right.camera.zoom = 0.65
-// Set right.camera = left.camera to share a view explicitly.
+// 如需联动,在 Console 将 right.camera 赋为 left.camera;共享场景本身不会自动共享相机。
+// 先横排两个视图,再把固定 HUD 叠在它们之上,输入命中也使用组合后的几何。
views = Row(0, 20, left, right)
hud = Column(16, 8, TextBlock("一个世界,多个视角", 2), TextBlock("滚轮平移 · 空格/中键拖动 · 捏合或 Cmd/Ctrl+滚轮缩放"))
hud.alignSelf = "end"
diff --git a/examples/scene3d.endor b/examples/scene3d.endor
index 7fee8fb..f8fcea6 100644
--- a/examples/scene3d.endor
+++ b/examples/scene3d.endor
@@ -1,19 +1,27 @@
+// 同一 Scene3D 在透视/正交视口中展示;相机控制独立,网格业务变换由两边共享。
+// 三维内容与普通 Endor 二维控件一起排版,viewport 有限尺寸不限制场景世界坐标。
+// 保存左侧视口和共享立方体;动作可改相机位置或真实网格变换。
class SceneActions {
view: Viewport3D
cube: Mesh3D
+ // 把左相机放到 z=6 正面,保持相机既有 target。
function front(self: SceneActions): Unit {
self.view.camera.x = 0
self.view.camera.y = 0
self.view.camera.z = 6
}
+ // 把同一相机移到侧上方,右侧正交相机保持原设置。
function side(self: SceneActions): Unit {
self.view.camera.x = 6
self.view.camera.y = 2
self.view.camera.z = 0
}
+ // 修改共享 cube 的欧拉旋转,因此两个视口都看到物体变化。
function rotate(self: SceneActions): Unit { self.cube.rotate(0, 15, 0) }
+ // 示范 Event.hit.mesh 的真实托管网格材质写入;此文件尚未把 select 注册到视口。
function select(self: SceneActions, event: Event): Unit { event.hit.mesh.color = "#6B9B81" }
}
+// 先构造并变换两个 Box3D,再由 Group3D 组成场景节点;几何与相机是不同对象。
cube = Box3D(1.6, 1.6, 1.6, "#6687AB")
cube.rotationY = 25
cube.rotationX = 15
@@ -21,11 +29,13 @@ small = Box3D(0.7, 0.7, 0.7, "#DBA36E")
small.x = 1.8
small.z = -0.8
group = Group3D(cube, small)
+// 薄盒作地面,方向光与 ambient 一起影响材质颜色,不是二维背景矩形。
floor = Box3D(6, 0.08, 4, "#D8DEE6")
floor.y = -1.1
light = DirectionalLight(-1, -2, -3, "#FFFFFF", 0.9)
scene = Scene3D(group, floor, light)
scene.ambient = 0.35
+// 左视口使用默认透视相机,右视口显式换成 OrthographicCamera 并设置可见高度。
perspective = Viewport3D(480, 360, scene)
perspective.camera.z = 6
orthographic = Viewport3D(320, 360, scene)
@@ -34,6 +44,7 @@ orthographic.camera.height = 5
orthographic.camera.x = 4
orthographic.camera.y = 2
orthographic.camera.z = 5
+// 按钮绑定零参数方法;页面文字说明原生默认控制器的交互,代码可继续替换 controller。
actions = SceneActions(perspective, cube)
front = Button("正面")
front.onClick(actions.front)
diff --git a/examples/scroll-axis.endor b/examples/scroll-axis.endor
index 628bdaa..355c238 100644
--- a/examples/scroll-axis.endor
+++ b/examples/scroll-axis.endor
@@ -1,21 +1,31 @@
+// 滚动轴策略影响可滚动方向与内容测量约束:vertical 限宽并允许长高,horizontal 允许横向展开。
+// 同一 paragraph 出现两次,文字内容共享,换行/偏移/范围在各 Mount 中独立。
+// 通过具体出现位置的 Mount 操作偏移;ScrollBlock.axis 则是对象级策略,可影响其所有出现位置。
class Actions {
vertical: ScrollBlock
horizontal: ScrollBlock
+ // 只对 vertical 第一个 Mount 累加纵偏移,运行时按当前范围夹取。
function down(self: Actions): Unit { self.vertical.mounts().at(0).scrollBy(0, 60) }
+ // 只对 horizontal 第一个 Mount 累加横偏移,禁止轴仍保持零。
function right(self: Actions): Unit { self.horizontal.mounts().at(0).scrollBy(60, 0) }
+ // 分别把两个出现位置移回原点,不修改共享段落文字。
function reset(self: Actions): Unit {
self.vertical.mounts().at(0).scrollTo(0, 0)
self.horizontal.mounts().at(0).scrollTo(0, 0)
}
+ // 把左对象策略改为双轴,下一次求解重新计算约束和范围。
function both(self: Actions): Unit { self.vertical.axis = "both" }
}
paragraph = TextBlock("这里展示的是同一个 TextBlock。左侧只允许纵向滚动,文字会按照这个 Mount 的实际视口宽度换行;右侧只允许横向滚动,文字保留横向展开。对象的文字和业务状态只有一份,每次展示的约束、几何和滚动位置分别保存。修改滚动轴也会重新测量受影响内容,并将已经不再允许的偏移夹回零。布局传播持续到受影响部分稳定,不会按固定跳数丢弃后续更新。这里展示的是同一个 TextBlock,两个视图可以拥有不同的换行结果。")
+// 纵向区域padding后内容宽为244,长文字按此有限宽换行。
vertical = ScrollBlock(260, 140, paragraph)
vertical.padding = 8
vertical.axis = "vertical"
+// 横向区域沿滚动轴允许内容自然展开,所以同一文字可以比左侧更宽、更矮。
horizontal = ScrollBlock(260, 140, paragraph)
horizontal.padding = 8
horizontal.axis = "horizontal"
+// 绑定四个零参数按钮回调;最终把两视图与控制区放在普通 Column/Row 中。
actions = Actions(vertical, horizontal)
down = Button("向下滚动")
down.onClick(actions.down)
diff --git a/examples/scroll.endor b/examples/scroll.endor
index 6db93f6..f6aad2c 100644
--- a/examples/scroll.endor
+++ b/examples/scroll.endor
@@ -1,10 +1,16 @@
+// 同一 ScrollBlock 在两个原生窗口中展示:内容业务对象共享,滚动偏移按 Mount 独立保存。
+// 滚动只改变投影/裁剪,不复制业务计数;被裁剪内容不能通过原始矩形接受点击。
+// 保留一个源 paint 委托,检验源命令与原生命令应用相同滚动裁剪。
class Painted: RectBlock {
+ // 使用当前 geometry 交给原生基类;外层滚动变换/clip 由展示流程统一组合。
function paint(self: Painted, mount: Mount, geometry: Bounds): DisplayList {
return RectBlock.paint(self,mount,geometry)
}
}
+// 三个按钮复用同一个 Action,任意可见按钮点击都递增同一 count。
class Actions {
count: Object
+ // 签名接受 Event,业务逻辑只需修改计数,不直接处理滚动位置。
function click(self: Actions, event: Event): Unit { self.count.increment() }
}
count = ui.state(0)
@@ -15,12 +21,14 @@ last = Button("底部按钮")
firstConnection = first.on("click",actions.click)
middleConnection = middle.on("click",actions.click)
lastConnection = last.on("click",actions.click)
+// 高内容夹有三个按钮与矩形,使固定320x180 scroller产生可观察的横纵滚动范围。
body = Column(12,12,first,Painted(520,180,8,"#DCE8FF"),middle,RectBlock(520,180,8,"#E3EFE5"),last)
scroller = ScrollBlock(320,180,body)
root = Column(24,16,TextBlock("同一 Block,两处 Mount 独立滚动"),ui.textOf(count,"value"),scroller)
page = Page()
page.title = "滚动示例"
page.render(root)
+// 另一窗口复用 root/scroller;它们的第二个 Mount 偏移与主窗口第一个 Mount 无关。
other = Window()
other.setTitle("Scroll Secondary")
second = Page()
diff --git a/examples/source-hit.endor b/examples/source-hit.endor
index 5124764..8113a45 100644
--- a/examples/source-hit.endor
+++ b/examples/source-hit.endor
@@ -1,23 +1,31 @@
+// 自定义形状命中:对象原始矩形宽 160,但 paint 只画左侧直径 80 的圆,hitTest 也只接受圆内。
+// 圆外部分可落到父容器监听器;按下与释放须经相同合法目标复核。
+// 扩展矩形的绘制、命中和事件方法,用同一个 hits 状态观察交互结果。
class CircleTarget: RectBlock {
hits: Object
+ // point 已转换到该出现位置局部坐标;使用圆心(40,40)、半径40,而不是整个 160x80 外框。
function hitTest(self: CircleTarget, mount: Mount, point: Point): Bool {
return point.inCircle(40, 40, 40)
}
+ // 把原生命令宽度缩为 80、圆角半径设为 40,得到与 hitTest 对应的圆形区域。
function paint(self: CircleTarget, mount: Mount, geometry: Bounds): DisplayList {
drawing = RectBlock.paint(self, mount, geometry)
drawing.commands.at(0).width = 80
drawing.commands.at(0).radius = 40
return drawing
}
+ // 圆内点击递增自身计数并停止向祖先传播;父容器不会为同一次点击再递增。
function onEvent(self: CircleTarget, event: Event): Unit {
self.hits.increment()
event.stopPropagation()
}
}
hits = ui.state(0)
+// 父监听器单独计数;它和圆形目标显示在同页,便于比较穿过透明区的输入。
background = ui.state(0)
circle = CircleTarget(160, 80, 0, "#237A45", hits)
root = Column(24, 16, circle, TextBlock("点击圆内计入圆;透明右半区域交给父容器"), TextBlock("圆内点击"), ui.textOf(hits, "value"), TextBlock("父容器点击"), ui.textOf(background, "value"))
+// 订阅在父 Column 上,圆外有效区域可回退到父目标;源命中只读检查不执行这些业务回调。
connection = root.on("click", background.increment)
page = Page()
page.render(root)
diff --git a/examples/source-layout.endor b/examples/source-layout.endor
index 733831a..c0880cd 100644
--- a/examples/source-layout.endor
+++ b/examples/source-layout.endor
@@ -1,25 +1,35 @@
+// 源视觉协议的最小组合:measure 决定尺寸,arrange 决定子项位置,Metrics 是被两者读取的业务模型。
+// 运行时在只读求解中跟踪这些读取,事件修改模型后重新布局;布局方法不能直接改业务字段。
+// 保存宽度与左偏移;两字段只在事件回调中修改。
class Metrics {
width: Number
left: Number
+ // 同时改变测量和排列依赖,整帧需收敛后一起发布新尺寸/位置。
function expand(self: Metrics, event: Event): Unit {
self.width = 260
self.left = 68
}
}
+// 复用原生矩形载荷,仅覆盖尺寸策略;同一 rect 放在不同父容器可得到不同测量。
class MeasuredRect: RectBlock {
+ // 首次测量前结构 Mount 链接已建立,因此可从 parent.block 读取当前出现位置的父策略。
+ // 此阶段还不能查询最终 bounds,返回值使用 MeasureResult 名义类型。
function measure(self: MeasuredRect, mount: Mount, constraints: Constraints): MeasureResult {
parent = mount.parent()
return MeasureResult(parent.block.metrics.width, 60)
}
}
+// 在原生排列结果上改第一个子槽 x,其他子项仍用 Column 的堆叠规则。
class OffsetColumn: Column {
metrics: Metrics
+ // Arrangement 里的 x 是当前排列使用的坐标;读取 metrics.left 成为排列依赖。
function arrange(self: OffsetColumn, mount: Mount, geometry: Bounds): Arrangement {
positions = Column.arrange(self, mount, geometry)
positions.children.at(0).x = self.metrics.left
return positions
}
}
+// 将原生参数与源字段传给具体子类,button 事件更新模型,Page.render 建立完整展示关系。
metrics = Metrics(200, 44)
rect = MeasuredRect(80, 20, 0, "#526AC7")
button = Button("更新布局")
diff --git a/examples/source-paint.endor b/examples/source-paint.endor
index a8ba888..1a53c3a 100644
--- a/examples/source-paint.endor
+++ b/examples/source-paint.endor
@@ -1,15 +1,22 @@
+// 源 paint 产出真正进入窗口的 DisplayList;允许构造并修改本次计算的新命令,禁止改已有业务对象。
+// 同一 rect 出现两次,两份命令随同一 tint 更新;业务身份共享,几何按 Mount 分开。
+// 原生矩形测量不变,仅将绘制颜色切换为源字段 tint。
class TintRect: RectBlock {
tint: String
+ // 从原生基类取得当前出现位置的矩形命令,再修改本次返回列表中的颜色并交回。
function paint(self: TintRect, mount: Mount, geometry: Bounds): DisplayList {
drawing = RectBlock.paint(self, mount, geometry)
drawing.commands.at(0).color = self.tint
return drawing
}
}
+// 在原生文字前增加圆角底色,背景大小使用实际分配 geometry 而不是固定猜测值。
class Badge: TextBlock {
background: String
+ // 先完整填写 roundedRect 命令,再取原生文字命令改成白色;列表顺序决定先画背景后画文字。
function paint(self: Badge, mount: Mount, geometry: Bounds): DisplayList {
fill = PaintCommand()
+ // 命令 kind 决定宿主解码所需字段,坐标/宽高来自本次 Mount,radius/color 由业务策略提供。
fill.kind = "roundedRect"
fill.x = geometry.x
fill.y = geometry.y
@@ -17,17 +24,21 @@ class Badge: TextBlock {
fill.height = geometry.height
fill.radius = 6
fill.color = self.background
+ // 复用原生文字塑形和换行所需参数,避免手工文字命令漏掉边界。
text = TextBlock.paint(self, mount, geometry).commands.at(0)
text.color = "#FFFFFF"
return DisplayList(ObjectList(fill, text))
}
}
+// 保存目标 TintRect,onEvent 是托管事件目标方法覆盖,不额外创建另一份状态。
class RecolorButton: Button {
target: TintRect
+ // 事件阶段可修改已有业务字段;依赖跟踪检测 tint 变化后重绘两个出现位置。
function onEvent(self: RecolorButton, event: Event): Unit {
self.target.tint = "#526AC7"
}
}
+// 两次使用 rect 是同一对象,Badge 与按钮独立;根容器只组合它们,不复制业务数据。
rect = TintRect(200, 60, 12, "#DDEEFF", "#ED6A5A")
badge = Badge("Endor 源码正在绘制这个背景", 2, "#237A45")
button = RecolorButton("通过对象方法改变绘制颜色", rect)
diff --git a/examples/student-grades.endor b/examples/student-grades.endor
index a792ce6..b1fd5b0 100644
--- a/examples/student-grades.endor
+++ b/examples/student-grades.endor
@@ -1,14 +1,19 @@
-// Student grades demo. Fictional records; in-memory only.
-// Paste the entire file into Tessera Console, then press Enter.
+// 学生成绩中心为纯内存演示,记录均虚构,不连接学校数据库。
+// 按钮操作 GradeCenter/Student 原对象;成绩行与摘要用 textOf 绑定,同一学生在全部/复核视图中复用业务身份。
+// 整个文件作为一次完整源码输入提交;状态只存在本次 VM,会话重置后不保留。
+// 为 Column 增加期望宽度字段,使手工表格各列对齐,而非让文字自然宽度决定列边界。
class GradeCell: Column {
width: Number
+ // 先保留 Column 的自然高度,再把新测量结果宽度设为字段值,最终仍由父约束协商。
function measure(self: GradeCell, mount: Mount, constraints: Constraints): MeasureResult {
size = Column.measure(self, mount, constraints)
size.width = self.width
return size
}
}
+// 背景绘制由源覆盖,子项仍按普通 Column 排列与绘制。
class GradePanel: Column {
+ // 按本次 geometry 生成白色圆角底板;不重绘或复制子项命令,后代由统一遍历处理。
function paint(self: GradePanel, mount: Mount, geometry: Bounds): DisplayList {
fill = PaintCommand()
fill.kind = "roundedRect"
@@ -21,6 +26,7 @@ class GradePanel: Column {
return DisplayList(ObjectList(fill))
}
}
+// 姓名/学号和各科成绩/状态是业务字段;数值修改受 Number 声明检查。
class Student {
name: String
number: String
@@ -29,9 +35,11 @@ class Student {
english: Number
status: String
}
+// 用统一无 padding/gap 的单格容器包装任意 Block,末尾 width 是源字段构造参数。
function gradeCell(content: Block, width: Number): Block {
return GradeCell(0, 0, content, width)
}
+// 姓名/学号直接取初值,成绩和状态用响应式 textOf;行末分隔线与固定列宽保持表格结构。
function gradeRow(student: Student): Block {
return Column(0, 12,
Row(0, 0,
@@ -43,6 +51,7 @@ function gradeRow(student: Student): Block {
gradeCell(ui.textOf(student, "status", 1, "#526C83"), 160)),
RectBlock(730, 1, 0, "#EDF0F3"))
}
+// 保存全部/复核两棵现成子树与可替换 table,同时持有要复核的真实 Student。
class GradeCenter {
table: Column
all: Column
@@ -50,20 +59,24 @@ class GradeCenter {
student: Student
pending: Number
message: String
+ // 替换稳定槽0的内容为全部行,复用已有学生,不重新请求/重建数据。
function showAll(self: GradeCenter, event: Event): Unit {
self.table.replace(0, self.all)
self.message = "正在查看全部 6 位学生"
}
+ // 切换到预先构造的复核行;这是特定示例子树,不是动态过滤算法。
function showAttention(self: GradeCenter, event: Event): Unit {
self.table.replace(0, self.attention)
self.message = "林沐的数学成绩有一条复核记录"
}
+ // 写预设数学成绩68及状态,清待复核数;相关绑定在两个视图都读同一 studentLin。
function review(self: GradeCenter, event: Event): Unit {
self.student.math = 68
self.student.status = "已复核"
self.pending = 0
self.message = "已录入复核结果:林沐 · 数学 58 → 68"
}
+ // 恢复初始学生值/计数并显示全部列表,便于重复观察同一操作。
function reset(self: GradeCenter, event: Event): Unit {
self.student.math = 58
self.student.status = "待复核"
@@ -72,6 +85,7 @@ class GradeCenter {
self.message = "已恢复示例数据"
}
}
+// 下面六条数据均本地构造;allGradeRows 与 attentionGradeRows 对 studentLin 建立不同视觉行但共享其业务值。
studentLin = Student("林沐", "2026001", 86, 58, 91, "待复核")
studentChen = Student("陈知夏", "2026002", 94, 96, 93, "已确认")
studentXu = Student("许星河", "2026003", 82, 88, 85, "已确认")
@@ -81,15 +95,18 @@ studentLu = Student("陆景行", "2026006", 79, 84, 87, "已确认")
allGradeRows = Column(0, 14, gradeRow(studentLin), gradeRow(studentChen), gradeRow(studentXu), gradeRow(studentZhou), gradeRow(studentSu), gradeRow(studentLu))
attentionGradeRows = Column(0, 14, gradeRow(studentLin))
gradeTable = Column(0, 0, allGradeRows)
+// table 最初含一个稳定子槽,后续 replace(0,...) 正是切换这个槽。
gradeCenter = GradeCenter(gradeTable, allGradeRows, attentionGradeRows, studentLin, 1, "正在查看全部 6 位学生")
showAllGrades = Button("全部学生")
showAttentionGrades = Button("查看复核记录")
reviewGrade = Button("录入复核成绩")
resetGrades = Button("恢复示例数据")
+// 四个回调均接受 Event,绑定到同一 GradeCenter 接收者。
showAllGrades.on("click", gradeCenter.showAll)
showAttentionGrades.on("click", gradeCenter.showAttention)
reviewGrade.on("click", gradeCenter.review)
resetGrades.on("click", gradeCenter.reset)
+// 表头复用与数据行完全相同的列宽,摘要仅 pending 响应式,6/3是固定示例统计。
gradeHeader = Row(0, 0,
gradeCell(TextBlock("学生", 1, "#7A8390"), 150),
gradeCell(TextBlock("学号", 1, "#7A8390"), 120),
@@ -101,6 +118,7 @@ gradeSummary = Row(0, 40,
Column(0, 6, TextBlock("学生人数", 1, "#7A8390"), TextBlock("6", 2)),
Column(0, 6, TextBlock("考试科目", 1, "#7A8390"), TextBlock("3", 2)),
Column(0, 6, TextBlock("待复核", 1, "#7A8390"), ui.textOf(gradeCenter, "pending", 2)))
+// 最终一次 render 组合摘要、控制区和表格面板;message 绑定显示最近业务操作结果。
gradesPage = Page()
gradesPage.title = "学生成绩管理中心"
gradesPage.render(Column(32, 20,
diff --git a/examples/tab-placement.endor b/examples/tab-placement.endor
index 677a904..c3a42dc 100644
--- a/examples/tab-placement.endor
+++ b/examples/tab-placement.endor
@@ -1,16 +1,23 @@
+// 页面位置与子项插入的代码示例:Page 方法覆盖计数,Container.insert 在指定位置创建稳定子槽。
+// 当前侧栏提供选择/关闭,文案中的拖放场景也可经版本化事件 API 测试;不要把它理解成已实现的侧栏拖动手势。
+// 继承 Page 后观察 moveToWindow/moveTab 的真实方法调用,保留原生迁移/重排行为。
class TrackedPage: Page {
calls: Object
+ // 先递增 calls,再显式调用基类执行跨窗迁移;Page 身份保留,旧 Mount 上下文会替换。
function moveToWindow(self: TrackedPage, window: Window): Page {
self.calls.increment()
return Page.moveToWindow(self, window)
}
+ // position 是所属窗口内最终零基位置,重排不等于创建新的业务 Page。
function moveTab(self: TrackedPage, position: Number): Page {
self.calls.increment()
return Page.moveTab(self, position)
}
}
+// 保存容器业务对象,按钮插入只改变其子槽顺序,已有 child 的槽与 Mount 尽量保留。
class Actions {
list: Column
+ // 在索引 1 前插入新 TextBlock,返回的稳定槽可供 remove 等后续操作使用。
function insert(self: Actions, event: Event): Unit {
self.list.insert(1, TextBlock("插入的新对象"))
}
@@ -20,6 +27,7 @@ list = Column(0, 8, TextBlock("第一个子项"), TextBlock("最后一个子项"
actions = Actions(list)
button = Button("在位置 1 插入 Block")
connection = button.on("click", actions.insert)
+// A/C/目标页共享同一根及 list;各页激活时使用独立展示上下文,业务插入结果共享。
shared = Column(24, 16, TextBlock("拖动 Tab 重排、移入另一个窗口,或拖出独立窗口"), TextBlock("Page 方法调用次数"), ui.textOf(calls,"value"), button, list)
first = TrackedPage(calls)
first.title = "A · 共享内容"
@@ -30,6 +38,7 @@ second.render(Column(24, 16, TextBlock("这是 B,Page 身份随 Tab 移动保
third = TrackedPage(calls)
third.title = "C · 同一对象"
third.render(shared)
+// 建立目标窗口并指定桌面位置,MoveTo 使用物理桌面坐标且可能受窗口管理器调整。
other = Window()
other.resize(720,520)
other.setTitle("Tab Target")
@@ -38,6 +47,7 @@ target = TrackedPage(calls)
target.title = "目标窗口"
target.render(shared)
target.open(other)
+// 恢复源窗口首个页面,最后排出 resize/title/move/focus,便于手动或 API 验证。
first.activate()
ui.window().resize(800,600)
ui.window().setTitle("Tab Source")
diff --git a/examples/tabs.endor b/examples/tabs.endor
index 917e608..afe823a 100644
--- a/examples/tabs.endor
+++ b/examples/tabs.endor
@@ -1,4 +1,7 @@
-// Code and tab chrome operate the same Page objects.
+// 代码与原生侧栏都操作真实 Page 对象;activate/close/open 改变展示关系,业务对象可继续被全局引用保留。
+// 三个页面共存于同一 VM,最后 first.activate 选择初始展示页。
+// 当前宿主通过侧栏管理页面;Page 的代码接口保持同一业务身份。
+// 创建持有计数器与按钮的页面;切到其他页不等于销毁 count。
first = Page()
first.title = "共享计数"
count = ui.state(0)
@@ -6,12 +9,15 @@ increment = Button("加一")
connection = increment.on("click", count.increment)
first.render(Column(24,16,TextBlock("切换页面后,计数对象继续存在"),ui.textOf(count,"value"),increment))
+// 另一页演示 Page 生命周期与快捷键;界面提示是示例文案,实际导航位于当前宿主侧栏。
second = Page()
second.title = "另一个页面"
second.render(Column(24,16,TextBlock("点击上方 Tab 切换;× 关闭页面"),TextBlock("Ctrl+Tab 切换,Cmd/Ctrl+W 关闭;代码可调用 first.open() 重开")))
+// 独立内容页面仍用同一个运行时的原生类型和资源注册表。
third = Page()
third.title = "窗口与布局"
third.render(Column(24,16,TextBlock("每个 Tab 都是同一个 VM 中的 Page 对象"),RectBlock(240,80,8,"#237A45")))
+// 重新激活第一页面,原生侧栏和源查询可观察相同 active 状态。
first.activate()
ui.window().setTitle("Endor Pages")
diff --git a/examples/visual-protocols.endor b/examples/visual-protocols.endor
index 48824c4..afe3dd3 100644
--- a/examples/visual-protocols.endor
+++ b/examples/visual-protocols.endor
@@ -1,3 +1,6 @@
+// 原生视觉协议直接返回托管数据:Constraints/MeasureResult、Bounds/Arrangement 和 DisplayList 都可在源码中查询。
+// onMount 作为业务阶段收集结果;实际布局/绘制计算仍受只读规则约束。
+// 把协议探测结果保存到普通业务字段,textOf 将其中一部分显示出来。
class VisualResults {
width: Number
height: Number
@@ -5,8 +8,10 @@ class VisualResults {
childY: Number
color: String
}
+// 继承原生矩形,在自身挂载后用该 Mount 显式调用 measure/paint。
class ProbeRect: RectBlock {
results: VisualResults
+ // 有限最大宽 120 约束自然宽 200;另用自定义 Bounds 查询绘制颜色,把返回结果写进共享 results。
function onMount(self: ProbeRect, mount: Mount): Unit {
measured = self.measure(mount, Constraints(0, 0, 120, 80))
self.results.width = measured.width
@@ -15,14 +20,17 @@ class ProbeRect: RectBlock {
self.results.color = drawing.commands.at(0).color
}
}
+// 在挂载回调中探测原生排列,复用当前结构的稳定子槽。
class ProbeColumn: Column {
results: VisualResults
+ // 给定绝对起点(20,30)及容器范围,原生 padding=24 使首子项位于(44,54)。
function onMount(self: ProbeColumn, mount: Mount): Unit {
positions = self.arrange(mount, Bounds(20, 30, 500, 300))
self.results.childX = positions.children.at(0).x
self.results.childY = positions.children.at(0).y
}
}
+// 先创建结果容器,再将它作为源子类字段共享;root 的最后一个参数是该字段,不是视觉子项。
results = VisualResults(0, 0, 0, 0, "等待挂载")
rect = ProbeRect(200, 60, 12, "#DDEEFF", results)
root = ProbeColumn(24, 16, TextBlock("对象的原生视觉协议", 2), rect, TextBlock("约束测量宽度"), ui.textOf(results, "width"), TextBlock("绘制颜色"), ui.textOf(results, "color"), results)
diff --git a/examples/window.endor b/examples/window.endor
index f4626d3..f78dc63 100644
--- a/examples/window.endor
+++ b/examples/window.endor
@@ -1,17 +1,25 @@
+// 托管 Window 资源与原生窗口相连:源码排出 resize/title 命令,成功刷新后由 GUI 线程执行。
+// 实际可用视口再返回布局约束,原生窗口尺寸不是业务对象上凭空写入的一份几何副本。
+// 保存目标 Window,两个按钮对同一资源请求不同尺寸。
class WindowActions {
window: Window
+ // 请求 900x600 逻辑尺寸,最终大小仍由窗口管理器决定。
function compact(self: WindowActions, event: Event): Unit {
self.window.resize(900, 600)
}
+ // 请求 1120x720;同次输入多次 resize 可由命令队列合并到最后值。
function wide(self: WindowActions, event: Event): Unit {
self.window.resize(1120, 720)
}
}
+// 原生矩形的测量覆盖展示父约束如何随窗口变化。
class WindowBand: RectBlock {
+ // 取当前出现位置 maxWidth 作为期望宽度,固定高 32;不读取其他窗口的尺寸。
function measure(self: WindowBand, mount: Mount, constraints: Constraints): MeasureResult {
return MeasureResult(constraints.maxWidth, 32)
}
}
+// ui.window 返回当前主窗口托管资源;page.open 指定同一资源,后面的 title/resize 为原生命令。
window = ui.window()
actions = WindowActions(window)
band = WindowBand(80, 32, 0, "#237A45")
diff --git a/examples/windows.endor b/examples/windows.endor
index 620e2e4..b608051 100644
--- a/examples/windows.endor
+++ b/examples/windows.endor
@@ -1,18 +1,25 @@
-// Both native windows use one Aster VM and one shared business object.
+// 两个原生窗口共用一个 Aster VM 和 count 业务对象,各自拥有独立 Page 与 Mount。
+// 任一窗口点击都修改同一计数,两个 textOf 显示随之更新。关闭窗口解除 Page 归属,持有的业务对象仍可重开。
+// 跨窗共享的是业务对象,不是某个窗口中的具体 Mount。
+// 唯一计数来源;两个按钮直接使用同一零参数原生 increment 方法。
count = ui.state(0)
leftButton = Button("共享计数 +1")
leftConnection = leftButton.on("click",count.increment)
+// 先在主窗口建立页面,render 默认选择该页并挂载它的内容。
left = Page()
left.title = "主窗口页面"
left.render(Column(24,16,TextBlock("两个窗口操作同一个计数对象"),ui.textOf(count,"value"),leftButton))
+// Window() 分配同 VM 中的独立窗口资源,不创建第二个运行时。
other = Window()
rightButton = Button("另一个窗口 +1")
rightConnection = rightButton.on("click",count.increment)
right = Page()
right.title = "独立窗口页面"
right.render(Column(24,16,TextBlock("关闭窗口会解除 Page 归属,保留的对象仍可重开"),ui.textOf(count,"value"),rightButton))
+// 把页面放到 other,上下文更换将重建出现位置,业务 count 仍不变。
right.open(other)
+// 窗口命令针对各自资源编号排队;成功输入后再交 GUI,另一窗口设置自己的尺寸。
ui.window().setTitle("Endor Main")
other.setTitle("Endor Shared")
other.resize(960,640)
diff --git a/examples/workspace.endor b/examples/workspace.endor
index 44ad3c3..78314ec 100644
--- a/examples/workspace.endor
+++ b/examples/workspace.endor
@@ -1,6 +1,8 @@
-// Paste the complete file into Console in Script mode, then Run.
-// All numbers, progress values and activity entries below are demo data.
+// 静态工作台组合示例:所有指标、进度与活动记录均为演示数据,不读取任务系统或网络。
+// 辅助函数返回视觉子树,最后统一 Page.render;它们是普通 Endor 函数,不是宿主特殊组件语法。
+// 将整个文件作为完整输入提交;这里没有实时指标提供方,全部数据由下面的常量生成。
+// 把标签、大数值、说明和色条纵向组合,参数控制文字与强调颜色。
function metric(label: Text, value: Text, note: Text, color: Text): View {
return ui.column(0, 6,
ui.text(label, 1, "#637082"),
@@ -10,11 +12,14 @@ function metric(label: Text, value: Text, note: Text, color: Text): View {
)
}
+// 生成项目标题和状态,再用两个相邻矩形表示已完成/剩余长度;filled/remaining 是逻辑像素。
function project(name: Text, state: Text, detail: Text, filled: Number, remaining: Number, color: Text): View {
+ // 项目名和状态横排,状态颜色与进度前景相同。
let caption = ui.row(0, 12,
ui.text(name, 1, "#243047"),
ui.text(state, 1, color)
)
+ // 两块矩形间 gap=0 合成连续条,条宽由调用方传入的演示数值决定。
let progress = ui.row(0, 0,
ui.rect(filled, 6, 3, color),
ui.rect(remaining, 6, 3, "#E5EAF2")
@@ -26,6 +31,7 @@ function project(name: Text, state: Text, detail: Text, filled: Number, remainin
)
}
+// 组合小圆点、时间戳、标题与说明;spacer 仅作视觉对齐,不代表真实事件时间线。
function activity(time: Text, title: Text, detail: Text, color: Text): View {
let dot = ui.column(0, 0, ui.spacer(7), ui.rect(6, 6, 3, color))
let stamp = ui.row(0, 8, dot, ui.text(time, 1, "#8490A0"))
@@ -36,6 +42,7 @@ function activity(time: Text, title: Text, detail: Text, color: Text): View {
)
}
+// 头部三个文本使用字号/颜色层级,后续 statistics 复用 metric 返回三组指标。
let heading = ui.column(0, 6,
ui.text("ENDOR STUDIO / WORKSPACE", 1, "#5263D6"),
ui.text("项目工作台", 2, "#243047"),
@@ -48,6 +55,7 @@ let statistics = ui.row(0, 20,
metric("页面草稿", "08", "等待下一次灵感", "#D9A34A")
)
+// 纵向复用 project,示例名称与比例是常量,不做进度计算或持久化。
let projects = ui.column(0, 14,
ui.text("项目进度", 1, "#243047"),
project("Aster", "进行中", "语言解析 / 编译 / 执行", 240, 60, "#6574E8"),
@@ -55,6 +63,7 @@ let projects = ui.column(0, 14,
project("设计文档", "草稿", "Block / Page / 对象模型", 108, 192, "#D9A34A")
)
+// 三条 activity 组成静态活动区,再与项目区横排成 body。
let timeline = ui.column(0, 14,
ui.text("最近活动", 1, "#243047"),
activity("10:24", "更新字体", "中英文混排预览", "#6574E8"),
@@ -64,6 +73,7 @@ let timeline = ui.column(0, 14,
let body = ui.row(0, 20, projects, timeline)
+// 底部提示以细色块和文字组合,明确说明页面还未接业务交互。
let note = ui.row(0, 12,
ui.rect(4, 46, 2, "#6574E8"),
ui.column(0, 6,
@@ -72,6 +82,7 @@ let note = ui.row(0, 12,
)
)
+// 最终根统一设置 24px padding 与20px间距;创建辅助子树本身不会提前发布页面。
let page = ui.page()
page.render(ui.column(24, 20,
heading,
diff --git a/examples/world-ui.endor b/examples/world-ui.endor
index 763d50a..2fdbd2d 100644
--- a/examples/world-ui.endor
+++ b/examples/world-ui.endor
@@ -1,8 +1,12 @@
-// A normal Endor UI tree rendered on a transformed plane in the 3D world.
+// 世界空间 UI:普通 Endor 树先在独立 UI 分辨率中布局/绘制,再作为 Panel3D 平面的纹理参与三维深度。
+// 面板内还有 CanvasViewport,输入需经外层射线、面板 UV 和内层二维相机逐层转换。
+// 面板内容仍是完整 Endor UI,可共享对象、绑定、源视觉协议与嵌套视口。
+// 分别持有外层三维视口、面板变换和面板内二维画布,避免一次动作同时驱动多层相机。
class WorldActions {
view: Viewport3D
panel: Panel3D
canvas: CanvasViewport
+ // 重置三维相机位置与观察目标,不改变面板或内层画布业务状态。
function reset(self: WorldActions): Unit {
self.view.camera.x = 0
self.view.camera.y = 0.5
@@ -11,19 +15,24 @@ class WorldActions {
self.view.camera.targetY = 0
self.view.camera.targetZ = 0
}
+ // 只旋转 Panel3D 平面,面板内 UI 树仍用自己的逻辑宽高。
function turn(self: WorldActions): Unit { self.panel.rotate(0, 15, 0) }
+ // 找到画布当前 Mount,按该出现位置的有限尺寸 fitView,而非用外层三维视口大小。
function fit(self: WorldActions): Unit { self.canvas.mounts().at(0).fitView() }
}
count = ui.state(0)
increment = Button("点击:世界里的真实按钮")
increment.onClick(count.increment)
+// 二维场景卡片使用带符号世界坐标;canvas 再提供有限可见区域和独立 camera。
card = Column(10, 6, TextBlock("CanvasViewport", 1), RectBlock(180, 26, 6, "#9CB8A8"))
card.boxWidth = 240
card.x = 10
card.y = 4
canvas = CanvasViewport(280, 80, Scene2D(card))
+// 普通 Column、文字、按钮和二维画布组成 UI 树,按钮仍修改同一个 count。
ui_root = Column(8, 8, TextBlock("世界空间 UI", 2), ui.textOf(count, "value", 2), increment, canvas)
ui_root.crossAlign = "stretch"
+// 4x3 是三维世界尺寸,320x240 是 UI 布局/纹理分辨率;两者独立控制显示比例与清晰度。
panel = Panel3D(4, 3, ui_root)
panel.uiWidth = 320
panel.uiHeight = 240
@@ -34,8 +43,10 @@ cube.x = 2
cube.rotationY = 25
floor = Box3D(10, 0.08, 5, "#D8DEE6")
floor.y = -1.8
+// 面板与普通网格/地面共处一个深度场景,实体可遮挡面板,也阻挡被遮住的 UI 命中。
scene = Scene3D(panel, cube, floor, DirectionalLight(-1, -2, -3, "#FFFFFF", 0.9))
scene.ambient = 0.35
+// 外层 Viewport3D 作为二维布局里的 flex 子项;相机控制外部观察,而非 UI 内部滚动。
view = Viewport3D(960, 520, scene)
view.flex = 1
view.camera.y = 0.5
@@ -47,6 +58,7 @@ turn = Button("转动 UI 面板")
turn.onClick(actions.turn)
fit = Button("适配面板内的画布")
fit.onClick(actions.fit)
+// 控制按钮保持在二维页面空间,外层三维内容作为一个区域随布局伸展。
body = Column(24, 16, TextBlock("UI → 3D 世界 → UI → Canvas", 2), view, Row(0, 12, reset, turn, fit), TextBlock("空白处左键拖动旋转 3D · 中键/空格平移 · 滚轮缩放\n面板里的按钮、文本、布局和画布都由同一份 Endor 对象驱动。"))
body.flex = 1
body.crossAlign = "stretch"
diff --git a/examples/wrapped-text.endor b/examples/wrapped-text.endor
index ea416cb..3437f02 100644
--- a/examples/wrapped-text.endor
+++ b/examples/wrapped-text.endor
@@ -1,18 +1,28 @@
+// 多窗口共享长段落:同一文字对象在每个 Mount 按当前宽度独立换行,后继位置跟随实际高度。
+// 源子类显式委托原生 measure/paint,保留系统字体塑形、组合字符和 emoji 的行为。
+// 没有独立宽度副本;约束与换行结果属于出现位置,文本内容属于共享对象。
class Paragraph: TextBlock {
+ // 将当前 Mount 与 Constraints 交给原生文字测量,宽度变化触发重测高度。
function measure(self: Paragraph, mount: Mount, constraints: Constraints): MeasureResult {
return TextBlock.measure(self, mount, constraints)
}
+ // 使用本次 geometry 画文字,和原生测量采用相同换行区域。
function paint(self: Paragraph, mount: Mount, geometry: Bounds): DisplayList {
return TextBlock.paint(self, mount, geometry)
}
}
+// 保存主窗口、共享文字和原全文,四个按钮分别改窗口尺寸或改文字内容。
class Actions {
window: Window
paragraph: TextBlock
full: String
+ // 请求主窗口变窄;真正的 viewport 由原生尺寸回传后影响换行。
function narrow(self: Actions, event: Event): Unit { self.window.resize(800, 650) }
+ // 请求主窗口变宽,文字高度随之减小,不改另一窗口尺寸。
function wide(self: Actions, event: Event): Unit { self.window.resize(1200, 650) }
+ // 用短文本替换同一对象内容,两个窗口的文字都重新测量。
function short(self: Actions, event: Event): Unit { self.paragraph.setText("短文本:同一个对象、同一个 Mount。") }
+ // 恢复保存的 full 字符串,不创建新的 Paragraph。
function restore(self: Actions, event: Event): Unit { self.paragraph.setText(self.full) }
}
full = "GUI 和代码操控同一套 Aster 内存对象。这个段落同时出现在两个窗口:对象内容相同,每个 Mount 根据自己的可用宽度换行。窗口变窄,段落高度增加,后面的对象向下移动;窗口变宽,段落高度减小,后面的对象向上移动。对象身份和已有 Mount 身份保持稳定。English words wrap at word boundaries; a_long_unbroken_identifier_also_wraps_when_it_cannot_fit_on_one_line. 中文、组合字符 é 与 emoji 👩💻 使用同一套字体整形。再次修改文字会重新计算高度,并更新两个窗口,不需要复制业务对象。"
@@ -30,6 +40,7 @@ restoreConnection = restore.on("click", actions.restore)
page = Page()
page.title = "约束下的文字换行"
page.render(Column(24, 16, Row(0, 12, narrow, wide, short, restore), paragraph, tail))
+// 新窗口有独立可用宽度,但 second.render 仍用同一 paragraph,所以两个 Mount 可有不同高度。
other = Window()
other.resize(720, 500)
other.setTitle("同一个段落 · 另一组宽度")
@@ -38,6 +49,7 @@ second = Page()
second.title = "共享 Paragraph"
second.render(Column(24, 16, paragraph))
second.open(other)
+// 恢复主页面活动状态并设置原生窗口尺寸/标题/桌面位置,focus 命令在成功输入后交宿主执行。
page.activate()
ui.window().resize(800, 650)
ui.window().setTitle("文字换行与相邻布局")
diff --git a/scripts/build-macos-app.sh b/scripts/build-macos-app.sh
index 49040fd..2273d27 100755
--- a/scripts/build-macos-app.sh
+++ b/scripts/build-macos-app.sh
@@ -1,16 +1,21 @@
#!/bin/sh
-# Native macOS build and app bundle. No Docker, mirror configuration, or tests.
+# macOS 应用打包入口:构建 release 可执行文件并组装最小 Tessera.app;脚本本身不运行测试或部署。
+# 执行会产生构建产物,应在已确认的 macOS 构建环境中调用。
+# 任一命令失败或使用未定义变量即退出,避免在失败编译后继续打包旧文件。
set -eu
# 此入口只能在 macOS 使用;构建授权由调用方确认,脚本不自动执行测试或部署。
if [ "$(uname -s)" != Darwin ]; then
echo "Build this app bundle on macOS." >&2
exit 1
fi
+# 以脚本所在目录的父目录作为仓库根;清空这一次 cd 的 CDPATH,防止用户 shell 配置改变目录解析。
cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
# 锁定依赖并生成 release 可执行文件;附加参数原样交给 Cargo。
cargo build --release --locked "$@"
+# 从 Cargo 包元数据读取首个 version 值,空值由 test 拒绝,不生成缺版本的 bundle。
version=$(awk -F '"' '$1 ~ /^version[[:space:]]*=/ { print $2; exit }' Cargo.toml)
test -n "$version"
+# macOS bundle 约定:Contents/MacOS 保存入口二进制,Contents/Info.plist 保存系统识别信息。
bundle="$(pwd)/target/release/Tessera.app"
mkdir -p "$bundle/Contents/MacOS"
# 先写临时文件再重命名,避免复制途中将半个二进制留作 bundle 的主程序。
@@ -31,5 +36,7 @@ cat > "$bundle/Contents/Info.plist.new" <NSHighResolutionCapable
PLIST
+# 元数据完整写入后再替换正式文件;这只是单文件替换,不表示整个 bundle 的构建具有事务回滚。
mv -f "$bundle/Contents/Info.plist.new" "$bundle/Contents/Info.plist"
+# 标准输出返回最终 app 路径,便于构建调用方定位产物。
printf '%s\n' "$bundle"
diff --git a/scripts/render-uml.mjs b/scripts/render-uml.mjs
index a1257d8..1a43b4a 100644
--- a/scripts/render-uml.mjs
+++ b/scripts/render-uml.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
-// Documentation-only tool. Install pinned packages in a separate temporary directory;
-// UML_RENDER_TOOLS points there. No app dependencies or remote rendering service.
+// 文档 UML 批量渲染入口:读取仓库内 .puml,在 rendered 中写入 SVG/PNG。
+// 固定版本的工具包安装到独立临时目录,由 UML_RENDER_TOOLS 指定;不会修改应用依赖。
import { createRequire } from 'node:module';
import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
@@ -9,13 +9,14 @@ 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');
+// 从工具目录的 package.json 创建专用 require,所有渲染依赖均从该目录解析,不依赖运行时 cwd。
const require = createRequire(path.join(path.resolve(tools), 'package.json'));
const { createCanvas } = require('@napi-rs/canvas');
const { DOMImplementation, XMLSerializer, DOMParser } = require('@xmldom/xmldom');
const sharp = require('sharp');
-// PlantUML's official JS engine uses DOM construction and Canvas text metrics.
-// Supply a real XML DOM and native Canvas metrics, not approximate text widths.
+// 官方 PlantUML JS 引擎依赖 DOM、Graphviz 和 Canvas 文本度量。
+// 在导入引擎前补齐全局环境,让 Node 进程可执行原本面向浏览器的布局过程。
globalThis.PLANTUML_VIZ = require('@viz-js/viz');
globalThis.window = globalThis;
globalThis.XMLSerializer = XMLSerializer;
@@ -23,15 +24,17 @@ globalThis.DOMParser = DOMParser;
const document = new DOMImplementation().createDocument(null, 'root', null);
const element = document.createElement.bind(document);
const elementNS = document.createElementNS.bind(document);
+// 创建只用于文本度量的 1x1 Canvas。此对象不承载最终图片像素,字体实际尺寸由原生引擎返回。
function measurementCanvas() {
// PlantUML 的文本布局需要真实字体宽高,统一用本机 Canvas 测量而非固定字符宽度。
const canvas = createCanvas(1, 1);
const context = canvas.getContext('2d');
const measure = context.measureText.bind(context);
+ // 包装测量结果,修正 PlantUML 用于行距的 ascent/descent;保留其他真实 metrics 字段。
context.measureText = text => {
const metrics = measure(text);
- // PlantUML uses this height for line spacing. Font-wide ascent/descent
- // keeps adjacent class fields from colliding when glyphs lack descenders.
+ // 优先使用整套字体的上下界,而非当前字符串字形界;
+ // 否则不带下伸部的文字可能使相邻类字段的行距过小而重叠。
return {
...metrics,
width: metrics.width,
@@ -41,11 +44,13 @@ function measurementCanvas() {
};
return canvas;
}
+// 普通标签交给 XML DOM,只有 canvas 替换为可实际 measureText 的原生对象。
document.createElement = tag => tag === 'canvas' ? measurementCanvas() : element(tag);
document.createElementNS = (namespace, tag) => {
// 为无浏览器 DOM 补齐样式与 getBBox,满足官方渲染引擎的 SVG 文本布局需求。
const node = elementNS(namespace, tag);
node.style = {};
+ // 无浏览器布局树时只模拟当前文字节点的包围盒:字体来自 SVG 属性,y 从基线向上偏移。
node.getBBox = () => {
const context = measurementCanvas().getContext('2d');
context.font = `${node.getAttribute('font-size') || 14}px ${node.getAttribute('font-family') || 'Arial'}`;
@@ -63,20 +68,23 @@ document.body = element('body');
document.documentElement.appendChild(document.body);
globalThis.document = document;
const { renderToString } = await import(pathToFileURL(require.resolve('@plantuml/core')));
+// 仓库位置由脚本 URL 确定,与调用者工作目录无关;输出目录不存在时创建。
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const source = path.join(root, 'docs/architecture/uml');
const output = path.join(source, 'rendered');
await mkdir(output, { recursive: true });
+// 按文件名排序使处理顺序/日志稳定,只处理本目录的 .puml,不递归扫描产物目录。
for (const filename of (await readdir(source)).filter(name => name.endsWith('.puml')).sort()) {
// 每张图独立渲染并检查错误文本;错误图不能被当作正常产物覆盖已有预览。
const text = await readFile(path.join(source, filename), 'utf8');
+ // 将引擎 resolve/reject 回调适配为 Promise,逐图等待;渲染失败直接终止,不覆盖该图产物。
const rendered = await new Promise((resolve, reject) => renderToString(text.split('\n'), resolve, reject));
const xml = new DOMParser().parseFromString(rendered, 'image/svg+xml');
const labels = Array.from(xml.getElementsByTagName('text'), node => node.textContent).join('\n');
if (xml.documentElement.tagName !== 'svg' || /syntax error|error line|assumed diagram type|an error has occurr/i.test(labels)) {
throw new Error(`${filename}: renderer returned an error diagram\n${labels}`);
}
- // Explicit opaque paper keeps arrows/labels legible in dark Markdown viewers.
+ // 在最前方加入不透明白纸背景,保证深色 Markdown 查看器中的箭头与文字仍可辨认。
const paper = xml.createElementNS('http://www.w3.org/2000/svg', 'rect');
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);
@@ -84,7 +92,7 @@ for (const filename of (await readdir(source)).filter(name => name.endsWith('.pu
// 同时保存可缩放 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.
+ // 以 192 DPI 光栅化为 PNG 供通用预览;SVG 同时保留矢量文字和缩放能力。
const png = sharp(Buffer.from(svg), { density: 192 });
await png.png().toFile(path.join(output, `${stem}.png`));
const metadata = await sharp(path.join(output, `${stem}.png`)).metadata();
diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs
index f642b14..426773d 100644
--- a/src/bridge/mod.rs
+++ b/src/bridge/mod.rs
@@ -1,4 +1,5 @@
//! 语言桥接模块入口。集中导出 Endor 类型和宿主实现之间的适配层。
//!
-// Module ownership follows the directory; crate-root aliases keep internal call sites stable.
+// 目录确定模块归属;main 中的 crate 根别名保持内部调用路径稳定。
+/// 注册托管类型/宿主方法,验证参数并连接业务对象、Mount、视觉协议与窗口/任务资源。
pub(crate) mod ui_bridge;
diff --git a/src/bridge/ui_bridge.rs b/src/bridge/ui_bridge.rs
index 3752eae..a27d987 100644
--- a/src/bridge/ui_bridge.rs
+++ b/src/bridge/ui_bridge.rs
@@ -30,6 +30,7 @@ pub const MAX_UI_NODES: usize = 4096;
pub const MAX_PAGES: usize = 64;
pub const MAX_DIMENSION: f64 = 1_000_000.0;
+// 编译器已知宿主全局类型/能力名,同时用于 install_globals;仅列名不代表绕过实际 VM 名义类型查找。
pub const TYPE_NAMES: &[&str] = &[
"Object",
"Type",
@@ -102,7 +103,9 @@ struct Session {
current: u64,
master: ObjectRef,
next_window: u64,
+ // 跨窗口共用 Mount 高水位,避免某窗口取消/重建后与其他窗口已暴露出现 ID 重合。
next_mount: u64,
+ // 正常关闭副窗口后暂存尚待发布的 Close 等命令;Presentation 已移除也不能丢掉最后的原生退役请求。
retired_commands: Vec,
refresh_yields: usize,
refresh_restarts: usize,
@@ -127,6 +130,7 @@ impl std::ops::Deref for Session {
type Target = crate::presentation::Presentation;
/// 将包装类型的只读访问转交给内部当前上下文。
fn deref(&self) -> &Self::Target {
+ // Session 解引用始终指向 current Presentation;调用源回调前切换窗口,结束后恢复主窗口,不能把它当全局唯一展示。
self.presentations
.get(&self.current)
.expect("selected presentation")
@@ -211,9 +215,10 @@ enum Operation {
}
impl Operation {
- /// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
+ /// 返回该操作显式参数数量范围;这里不计接收者、Type 首参和源子类字段。
fn arity(self) -> endor_core::CallArity {
use Operation::*;
+ // 此 arity 只含显式参数;TypeMethod 另加 receiver,NativeFactory 另加 Type 和子类字段外壳。
let (minimum, maximum) = match self {
IsInstance | IsSubtype | Bind | Implement | Implements | Supports => (2, 2),
Stats | Viewport | Window | PageWindow | Mounts => (0, 0),
@@ -254,6 +259,7 @@ struct Function {
operation: Operation,
owner: Option,
session: Shared,
+ // 原生构造的实际源子类身份;自由 ui 工厂为 None,类型构造器显式传入 Some。
requested_type: Option,
}
@@ -291,7 +297,7 @@ impl NativeCallable for TypeMethod {
fn signature(&self) -> Option {
Some(self.signature.clone())
}
- /// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
+ /// 在 Operation 的显式参数范围两端各加一,计入 VM 传入的隐式接收者。
fn arity(&self) -> Option {
let arity = self.operation.arity();
Some(endor_core::CallArity::range(
@@ -300,8 +306,9 @@ impl NativeCallable for TypeMethod {
))
}
- /// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
+ /// 校验首参是预期名义类型实例,再剥离接收者,复用 Function 分派执行同一 Operation。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result {
+ // 名义方法统一存在类型上,VM 把 receiver 放首参;校验后剥离 receiver 再交 Function 的显式参数分派。
let owner = object(args.first())?;
if !cx.store().is_instance(owner, self.expected)? {
return Err(invalid("incorrect receiver type for visual method"));
@@ -333,7 +340,7 @@ impl NativeCallable for NativeFactory {
fn signature(&self) -> Option {
self.signature.clone()
}
- /// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
+ /// 要求原生必填参数再加一个请求 Type;上界暂开放,子类字段与原生选项在 invoke 内分别校验。
fn arity(&self) -> Option {
Some(endor_core::CallArity::range(
self.operation.arity().required + 1,
@@ -341,7 +348,7 @@ impl NativeCallable for NativeFactory {
))
}
- /// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
+ /// 把请求类型的字段后缀与原生选项前缀分开验证,分配真实原生子类并初始化字段,避免非法字段先触发构造副作用。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result {
let requested = object(args.first())?;
let fields = cx
@@ -350,12 +357,14 @@ impl NativeCallable for NativeFactory {
.fields()
.to_vec();
let values = &args[1..];
+ // 构造实参末尾是类型声明的继承/自有字段,前缀才是原生构造选项;checked_sub 防止缺字段造成下溢。
let native_count = values
.len()
.checked_sub(fields.len())
.ok_or(invalid("missing subclass field arguments"))?;
cx.store()
.check_class_field_arguments(requested, &values[native_count..])?;
+ // 外壳允许宽变参,但原生前缀仍按具体 Operation 完整签名检查,不能因子类存在绕过类型或 arity。
self.options
.as_ref()
.ok_or(invalid("native factory is not initialized"))?
@@ -375,6 +384,7 @@ impl NativeCallable for NativeFactory {
let NativeResult::Return(Value::Object(instance)) = result else {
return Err(invalid("native constructor did not return an instance"));
};
+ // 分配器必须返回精确 requested 类型,不能只返回基类实例;之后才初始化子类字段。
if cx.store().type_of(instance)? != Some(requested) {
return Err(invalid(
"native constructor returned an incorrect nominal type",
@@ -429,6 +439,7 @@ fn dimension(value: Option<&Value>) -> Result {
let Some(Value::Number(number)) = value else {
return Err(invalid("expected a finite nonnegative dimension"));
};
+ // 分别支持大整数、十进制和二进制实数表示,不接受非实值;转 f64 后检查有限与范围,再降 f32。
let value = match number.as_real() {
Some(EndorReal::Integer(v)) => v.to_string().parse::().ok(),
Some(EndorReal::Decimal(v)) => v.literal().parse::().ok(),
@@ -461,6 +472,7 @@ fn bind(
session: session.clone(),
requested_type: None,
},
+ // 绑定方法显式捕获 owner 与 desktop,让托管 GC 识别函数的真实强边,Rust Shared 会话本身不隐式保活 VM 对象。
vec![owner, desktop],
))?;
attach_desktop_type(cx, function, "Function", desktop)?;
@@ -498,6 +510,7 @@ fn new_camera2d(
.unwrap_or(fallback);
cx.set_attr(camera, name, crate::native_visual::value(value))?;
}
+ // 初始化完所有相机属性后走完整相机校验,尤其 zoom 必须处于 0.01..100,不能只通过一般坐标范围。
crate::viewport::camera(cx.store(), camera)?;
Ok(camera)
}
@@ -530,6 +543,7 @@ fn new_typed_block(
.desktop;
let expected = desktop_type(cx.store(), type_name, desktop)?;
let type_object = requested.unwrap_or(expected);
+ // 叶/容器原生种类对应不同类型基类,禁止用 TextBlock 分配器创建并非其子类的名义对象。
if !cx.store().is_subtype(type_object, expected)? {
return Err(invalid("native constructor cannot allocate this subclass"));
}
@@ -537,6 +551,7 @@ fn new_typed_block(
cx.attach_type(block, type_object)?;
{
let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?;
+ // 节点弱表受宿主预算限制;实际死亡弱项通过后续 reclaim 压缩,不能把弱索引当永不增长的免费结构。
if state.nodes.len() >= MAX_UI_NODES {
return Err(VmError::ExecutionLimit("4096 node budget"));
}
@@ -572,6 +587,7 @@ fn create_window(
if state.exiting {
return Err(invalid("runtime is exiting; window creation is forbidden"));
}
+ // 同一 VM 最多16窗口(含主壳);ID 单调分配并限制到 i64 正范围,关闭后不复用旧资源身份。
if state.presentations.len() >= 16 {
return Err(VmError::ExecutionLimit("16 window budget"));
}
@@ -582,6 +598,7 @@ fn create_window(
.ok_or(invalid("window identities exhausted"))?;
let (window, desktop, tabs) = crate::window::create(cx, state.master, id, definition)?;
let mut presentation = crate::presentation::Presentation::new(desktop, tabs);
+ // 新窗口即使暂无内容也需发布有限空帧,让 GUI 根据投影创建 OS 窗口,而不是在 VM 中直接创建。
presentation.force_frame = true;
state.presentations.insert(id, presentation);
state.next_window = id;
@@ -610,6 +627,7 @@ fn remove_page(
) -> Result<(), VmError> {
let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?;
crate::window::unregister_page(cx, window, page)?;
+ // 双层 pending 区分无请求与请求清空;移除当前/待选页时改选剩余第一页,或显式 pending=None页面。
if view.pending.unwrap_or(view.active) == Some(page) {
let next = crate::window::pages(cx.store(), window)?.first().copied();
cx.set_attr(
@@ -631,6 +649,7 @@ fn show_in(
let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?;
let old = page_window(cx.store(), &state, page)?;
let id = match target {
+ // 显式目标窗口既要未关闭,也要对应本会话 Presentation 的同一 desktop,拒绝其他 VM 的窗口身份。
Some(window) => {
let data = cx
.store()
@@ -648,11 +667,13 @@ fn show_in(
}
id
}
+ // 不指定窗口时优先保留 Page 现有归属;未打开过的新页才进入 current,而非每次 activate 都迁移到主窗口。
None => old.unwrap_or(state.current),
};
if let Some(old) = old
&& old != id
{
+ // 跨窗口移动先撤源窗口登记与必要活动页选择,再在目标登记同一 Page,业务内容对象不复制。
remove_page(
cx,
state
@@ -669,6 +690,7 @@ fn show_in(
let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?;
crate::window::register_page(cx, window, page)?;
cx.set_attr(view.desktop, "pending", Value::Object(page))?;
+ // 仅记录展示请求并由 desktop.pending 强持有 Page;实际 active、Mount、布局和 published_version 留待刷新提交。
view.pending = Some(Some(page));
Ok(())
}
@@ -684,17 +706,18 @@ struct PresentResult {
session: Shared,
}
impl NativeCallable for PresentResult {
- /// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
+ /// 接收 present 委托返回的唯一视觉对象,作为后续展示处理的输入。
fn arity(&self) -> Option {
Some(endor_core::CallArity::exact(1))
}
- /// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
+ /// 验证 present 结果为 Block;open 模式必要时包装 Page 并展示,否则只返回转换得到的视觉对象。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result {
let [Value::Object(root)] = args else {
return Err(invalid("Presentable.present must return a Block"));
};
let is_page = cx.store().payload::(*root, "Block")?.page;
+ // present 委托返回值必须为 Block;open=true 必要时包 Page 并展示,open=false 只返回转换结果。
let result = if self.open {
let page = if is_page {
*root
@@ -714,7 +737,7 @@ impl NativeCallable for PresentResult {
}
}
-/// 根据 Presentable 能力把任意源值转换成可展示内容,避免只允许某个原生 Block 类型。
+/// 返回已有 Block,或把其他值的可读表示包装成 TextBlock;Presentable 委托由外层调用分派负责。
fn present_value(
cx: &mut NativeContext<'_>,
value: &Value,
@@ -730,6 +753,7 @@ fn present_value(
{
return Ok(*block);
}
+ // 非 Block 的标量/对象转可读文字,Presentable 方法委托在 calls.rs 的更外层处理,本函数不自己调用 present。
let value = match value {
Value::Nil => "nil".into(),
Value::Bool(v) => v.to_string(),
@@ -771,7 +795,9 @@ struct LayoutContext {
full_viewport: Option,
desktop: ObjectRef,
window: u64,
+ // 布局同步返回的生命周期序列拥有根,暂留到安全提交边界执行/释放,取消时也必须清理。
hooks: Option,
+ // 布局准备可能暂改 Mount payload 与 desktop.mounts,检查点保证失败或尺寸重定向能恢复未发布关系。
checkpoint: crate::mount::Checkpoint,
}
@@ -796,6 +822,7 @@ fn invoke_member(
arguments: &[Value],
instruction_budget: usize,
) -> Result {
+ // 使用 VM 正常绑定规则创建临时方法值;无论 call 成功/失败,先保存 result 再释放 bound 根。
let bound = vm.bind_attr(owner, method)?;
let result = match &bound {
Value::Object(function) => vm
@@ -821,11 +848,13 @@ fn project_tabs(
) -> Result> {
let window = object(Some(&vm.get_attr(state.desktop, "window")?))?;
let pages = crate::window::pages(vm.object_store(), window)?;
+ // 标签选中反映待提交页面(若有请求),否则沿用 active;页面标题和顺序仍从真实窗口注册表提取。
let active = state.pending.unwrap_or(state.active);
let mut live = Vec::new();
let mut tabs = Vec::new();
for (index, page) in pages.iter().copied().enumerate() {
let weak = vm.object_store().downgrade(page)?;
+ // 同一 Page 弱身份复用该窗口标签 ID,新增页用高水位;移出后 live 更新会移除旧映射,不靠标题匹配。
let id = if let Some((_, id)) = state.tab_ids.iter().find(|(w, _)| *w == weak) {
*id
} else {
@@ -836,6 +865,7 @@ fn project_tabs(
state.next_tab
};
live.push((weak, id));
+ // 标题显示过滤控制字符并限制120个 Unicode 字符,缺失/空标题用默认序号;不修改原 Page.title。
let title = match vm.get_attr(page, "title") {
Ok(Value::Text(title)) if !title.is_empty() => title
.chars()
@@ -863,6 +893,7 @@ impl UiBridge {
/// already closed before source unmount hooks run, so they cannot remount.
/// 按窗口和展示生命周期执行有界退出清理,允许调用方根据故障状态决定是否执行回调。
pub fn shutdown(&self, vm: &mut Vm, callbacks: bool) -> Result<(usize, usize, usize)> {
+ // 退出先关闭新任务入口并取消旧任务,再标 session.exiting,避免卸载回调重新创建任务/窗口。
let tasks = self.tasks.quiesce(vm)?;
let mut drivers = Vec::new();
let mut mounts = 0;
@@ -877,6 +908,7 @@ impl UiBridge {
let view = state.presentations.get_mut(id).expect("known window");
let desktop = view.desktop;
mounts += view.mounts.active.len();
+ // 正常域用空结构 synchronize 安排孩子先卸载的 hooks;故障域直接 retire,不能再次执行用户代码。
if callbacks {
if let Some(hooks) = view.mounts.synchronize(
vm,
@@ -892,6 +924,7 @@ impl UiBridge {
view.mounts.retire(vm, desktop)?;
}
let window = object(Some(&vm.get_attr(desktop, "window")?))?;
+ // 所有窗口先标 closed 并断 Page 拥有关系,然后才在下面执行用户卸载 hooks,阻止回调重新打开已关闭窗口。
crate::window::retire(vm, window)?;
vm.set_attr(desktop, "pending", Value::Nil)?;
vm.set_attr(desktop, "focus", Value::Nil)?;
@@ -902,6 +935,7 @@ impl UiBridge {
Ok(ids.len())
})();
let mut result = prepared.map(|windows| (windows, mounts, tasks));
+ // 若之前清理/回调失败,余下 hooks 不再执行,但所有驱动根仍逐项 release,避免失败路径泄漏。
for (id, hooks) in drivers {
if result.is_ok() {
let call = (|| -> Result<()> {
@@ -951,6 +985,7 @@ impl UiBridge {
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
let mut ids: Vec<_> = state.presentations.keys().copied().collect();
+ // 优先处理切离旧 active 页的窗口,使跨窗口迁移先退旧出现再挂新出现;同优先级按窗口 ID 稳定排序。
ids.sort_by_key(|id| {
let view = &state.presentations[id];
(
@@ -1009,6 +1044,7 @@ impl UiBridge {
vm.set_attr(desktop, "focus", Value::Nil)?;
vm.set_attr(desktop, "focusRequested", Value::Bool(false))?;
// Draining is not publication: failed-domain effects are discarded.
+ // 故障宿主退役的命令只丢弃不发布,防止失败域遗留 Resize/Focus 等影响当前原生状态。
let _ = crate::window::take_commands(vm, window)?;
let windows = object(Some(&vm.get_attr(state.master, "windows")?))?;
vm.dict_delete(windows, &Value::Text(id.to_string()))?;
@@ -1023,6 +1059,7 @@ impl UiBridge {
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
+ // 正常 finish_closed 只移除已标 closing 的副窗口,主壳保持到 OS 退出;其最后命令转存 retired_commands 待发布。
let ids: Vec<_> = state
.presentations
.iter()
@@ -1048,6 +1085,7 @@ impl UiBridge {
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
+ // 先取已移除窗口的最后命令,再合并仍活动窗口 outbox;按窗口/窗口内序号排序,不丢失正常 Close 请求。
let mut commands = std::mem::take(&mut state.retired_commands);
for view in state.presentations.values() {
let window = object(Some(&vm.get_attr(view.desktop, "window")?))?;
@@ -1071,6 +1109,7 @@ impl UiBridge {
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
+ // 尺寸比较在分配新输入对象前完成,避免相同 resize 通知改变 VM 版本而触发自我失效。
if state.layout.viewport == viewport {
return Ok(false);
}
@@ -1081,6 +1120,7 @@ impl UiBridge {
vm.set_attr(input, "height", Value::Number((size.height as f64).into()))?;
vm.set_attr(state.desktop, "viewport", Value::Object(input))
})();
+ // desktop 已建立 viewport 强边后释放局部根;字段/安装失败也先释放 input,再传播保存的错误。
vm.release(input)?;
result?;
} else {
@@ -1089,6 +1129,7 @@ impl UiBridge {
state.layout.viewport = viewport;
// A fresh Console domain can own window inputs without replacing the
// preserved displayed page until it actually opens/renders something.
+ // 空的新 Console 域可以记录逻辑尺寸,但未展示任何 Page 时不应覆盖用户仍看到的旧域画面。
state.force_frame |= state.active.is_some() || state.pending.flatten().is_some();
Ok(true)
}
@@ -1102,6 +1143,7 @@ impl UiBridge {
Value::Text("View".into()),
vm.get_attr(self.ui, "Block")?,
)?;
+ // 全局名引用 ui 中同一批名义类型/函数对象,模块和 Console 不会各复制一份类型体系。
for name in TYPE_NAMES.iter().chain(QUERY_NAMES) {
let value = vm.get_attr(self.ui, name)?;
vm.dict_set(globals, Value::Text((*name).into()), value)?;
@@ -1126,6 +1168,7 @@ impl UiBridge {
let Ok(state) = self.session.lock() else {
return false;
};
+ // 拖放源与目标都必须匹配各自发布版本,且真实 Page 注册顺序仍对应标签弱身份表,防止用陈旧列表下标重排。
for (id, version) in [
(input.window, input.layout_version),
(target.window, target.layout_version),
@@ -1173,6 +1216,7 @@ impl UiBridge {
if state.published_version != input.layout_version {
bail!("event targets an obsolete presentation");
}
+ // input.mount 在标签路径表示 Tab ID,先映回弱 Page,再确认仍在当前窗口 pages 集合,不是 Mount 几何下标。
let weak = state
.tab_ids
.iter()
@@ -1215,12 +1259,14 @@ impl UiBridge {
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
page_window(vm.object_store(), &state, page)?
};
+ // 源 moveToWindow 可覆盖并拒绝/改投其他窗口;只有确认实际归属仍为预期目标才继续重排与激活。
if destination == Some(target.window) {
let pages = crate::window::pages(vm.object_store(), window)?;
let from = pages
.iter()
.position(|candidate| *candidate == page)
.ok_or_else(|| anyhow::anyhow!("Page is no longer open"))?;
+ // 同窗口拖动的 index 是移除前插入缝位置,向后拖时先减一;跨窗口新加入则不用这一偏移修正。
let position = if input.window == target.window && from < target.index {
target.index - 1
} else {
@@ -1248,6 +1294,7 @@ impl UiBridge {
_ => "activate",
};
let value = invoke_member(vm, page, method, &[], 1_000_000)?;
+ // detach 返回值可能被源覆盖成其他值,仅确为 Window 且带屏幕位置才调用 moveTo;最后统一释放原返回值。
let placed = (|| -> Result<()> {
if input.kind == "tab-detach"
&& let Some([x, y]) = input.screen_position
@@ -1289,6 +1336,7 @@ impl UiBridge {
{
bail!("event belongs to another window");
}
+ // 窗口关闭通过名义 close 方法,保留源覆盖语义;直接调用宿主原生退役只用于故障清理路径。
if input.kind == "window-close" {
let desktop = self
.session
@@ -1312,6 +1360,7 @@ impl UiBridge {
vm.release_value(result?)?;
return Ok(());
}
+ // 内置工具打开走同一 VM 的 developerTools 能力,返回值只作临时根使用,不跨到 GUI。
if let Some(name) = input.kind.strip_prefix("developer-open:") {
let function = object(Some(&vm.get_attr(self.ui, "developerTools")?))?;
let page = vm.call(function, &[Value::Text(name.into())])?;
@@ -1336,6 +1385,7 @@ impl UiBridge {
) {
return self.dispatch_tab(vm, input);
}
+ // 普通指针输入取同一锁内的版本/目标/完整出现候选快照;随后不持 session 锁执行源 hitTest 或事件回调。
let (weak, desktop, candidates, viewport, surface) = {
let state = self
.session
@@ -1365,6 +1415,7 @@ impl UiBridge {
let point = input
.point
.ok_or_else(|| anyhow::anyhow!("scroll has no pointer"))?;
+ // scroll 不只校验 Mount 身份,还要指针仍在页面视口和出现裁剪中;相机/面板映射后的增量才可交给 scrollBy。
if !data.mounted
|| data.version != input.layout_version
|| !surface.contains(point, viewport)
@@ -1402,6 +1453,7 @@ impl UiBridge {
vm.release(mount)?;
return result;
}
+ // 坐标点击重新执行源协议选择按下和释放目标,二者同出现才分发;无坐标输入使用已捕获的弱目标。
let mount = if let (Some(press), Some(release)) = (input.press_point, input.point) {
if [press, release]
.iter()
@@ -1420,6 +1472,7 @@ impl UiBridge {
};
let result = (|| {
let data = vm.payload::(mount, "Mount")?.clone();
+ // 3D 视口点击先生成 RayHit3D 并附到 Event.hit,再沿视口出现传播,不能只按二维外框假定击中某网格。
if data.geometry.scene3d_viewport
&& input.kind == "click"
&& let Some(point) = input.point
@@ -1451,6 +1504,7 @@ impl UiBridge {
if !data.geometry.interactive {
bail!("Mount does not accept input");
}
+ // 普通点击/焦点先登记展示焦点请求,再按 kind 调 Block.click 或 Mount.requestFocus;生效仍需后续完整刷新。
vm.set_attr(
desktop,
"focus",
@@ -1483,6 +1537,7 @@ impl UiBridge {
}
/// 整理不再有效的弱索引与展示根,并配合 VM 回收,避免派生缓存延长业务对象生命周期。
pub fn reclaim(&self, vm: &mut Vm, _roots: &mut Vec) -> Result<()> {
+ // 先回收托管环,再扫描弱节点表,只保留能升级的身份;render_cache.prune 同步清除死亡业务索引。
vm.collect_cycles();
let mut state = self
.session
@@ -1511,6 +1566,7 @@ impl UiBridge {
let window = object(Some(&vm.get_attr(presentation.desktop, "window")?))?;
// A failed first render must not become visible on the next input
// merely because show() already registered its Page with the Window.
+ // 第一次 render 失败时撤销尚未发布的 Page 登记,避免下一次无关输入把失败页意外显示出来。
if presentation.active.is_none() && presentation.last_tabs.is_empty() {
let unpublished = crate::window::pages(vm.object_store(), window)?.to_vec();
for page in unpublished {
@@ -1518,6 +1574,7 @@ impl UiBridge {
}
}
let pages = crate::window::pages(vm.object_store(), window)?;
+ // 失败输入已成功执行的关闭/迁移语句不回滚;重新选择仍在真实窗口注册表中的页,修复 pending 与拥有关系。
let next = if presentation.closing {
None
} else {
@@ -1554,6 +1611,7 @@ impl UiBridge {
}
/// 重新评估已失效的属性绑定,并把结果计入会话统计。
pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> {
+ // 只在锁内快照弱节点与宿主桌面集合,实际绑定回调在锁外执行,避免回调访问 UI 时死锁。
let (nodes, desktops) = {
let state = self
.session
@@ -1571,11 +1629,13 @@ impl UiBridge {
}
/// 排空一次 VM 变化记录并通知各窗口,提取活动页面快照,判断本次是否需要布局与发布。
pub fn prepare_frame(&self, vm: &mut Vm) -> Result