1. 核心设计哲学:即时模式 vs 保留模式 #
现代图形界面开发主要分为两大流派:保留模式(Retained-Mode, RM) 与 即时模式(Immediate-Mode, IM)。
| 对比维度 | 保留模式 (DOM / React / Flutter / Qt) | 即时模式 (moon-egui) |
|---|---|---|
| 对象生命周期 | 长期持有庞大的堆对象树,组件有 mount/update/unmount 生命周期 |
零持久化组件对象:每帧按需执行纯函数表达式,求值完成即析构 |
| 状态同步 | 双向同步:需要脏标记检查、Virtual DOM Diffing、事件订阅与回调绑定 | 状态就地声明:业务状态即是真实状态,代码即界面,彻底告别同步地狱 |
| 内存与 GC 开销 | 随组件复杂度线性膨胀,高频更新容易引发浏览器 GC 垃圾回收卡顿 | 零 GC 抖动:基础结构体栈上分配,百万节点下帧耗时仅 0.1ms |
| 代码表达 | 分层深、闭包与事件处理链复杂,AI 代码生成容易产生生命周期死锁 | 线性扁平直观:if ui.button("保存").clicked() { save() } |
moon-egui 中,一个交互式按钮本质上是一个瞬态函数调用。在单次循环遍历中,完成尺寸布局推进、AABB 碰撞检测、绘制指令排队并即时返回当帧交互结果。
2. 为什么选择基于 MoonBit 构建? #
传统的即时模式库(如著名的 C++ Dear ImGui)极度依赖全局静态指针与原始内存偏移,难以优雅实现多实例隔离、多线程并发和纯无头(Headless)CI 自动化单元测试。
- 代数数据类型 (Algebraic Data Types):绘制指令以带标签的强类型枚举
DrawCmd表达,模式匹配保证 100% 穷尽性检查,杜绝空指针异常; - 纯函数式单实例可重入:
UIContext是独立的数据结构,同一进程或宿主中可以同时运行无数个完全隔离的画布实例; - 强大的死代码消除 (Dead Code Elimination):仅打包用户实际调用的控件,产出的 WebAssembly 模块极限精简(小于 50KB gzip),极速冷启动;
- 面向 WebAssembly 原生优化:针对 32 位/64 位线性内存进行高速数据对齐,执行效率逼近原生 C++。
3. 填补的核心生态空白 #
在 moon-egui 诞生之前,MoonBit 生态在纯 Canvas / WebAssembly 领域缺乏原生的 UI 基础设施。游戏开发、CAD 矢量画板、音频合成器或物理仿真应用在画布内部寸步难行,不得不回退到笨重的外部 HTML DOM 浮层覆盖方案。
moon-egui 提供了纯 Canvas 内部的高性能完整交互套件,涵盖窗口、菜单、微调滑块、文本编辑与富图元绘制,彻底打破了生态瓶颈。
4. 5 阶段逐帧生命周期流水线 #
在每一帧渲染循环中(Web 侧由 requestAnimationFrame 调度驱动),系统经历 5 个确定性的执行阶段:
┌────────────────────────────────────────────────────────────────────────┐
│ 阶段 1:输入数据采集 (Input Gathering) │
│ 宿主浏览器系统事件 ──► 归一化为平台无关的 `RawInput` 状态结构 │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 阶段 2:上下文帧初始化 (Context Frame Init) │
│ • 双缓冲滚动:将上一帧激活 ID 列表交换至 last_active 备查 │
│ • 排版游标归位,清空 DrawCmd 指令缓冲,计算帧时间差 delta_time │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 阶段 3:即时逻辑求值 (IM Evaluation) │
│ • 执行用户 UI 代码:`ui.window(...)`, `ui.button(...)`, `ui.slider(...)` │
│ • 针对归一化指针坐标实时完成 AABB 碰撞命中测试 │
│ • 流式游标排版引擎顺序推进,动态测量并锁定各控件外接矩形 │
│ • 将几何渲染指令追加至当前图层的绘制队列中 │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 阶段 4:图层合成与排序 (Layer Composition) │
│ • 按照窗口聚焦层深(Z-Index)对窗口进行层级重排 │
│ • 校验并扁平化各级视口裁切矩形(Scissor Clipping) │
└───────────────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 阶段 5:后端渲染分发 (Backend Render Dispatch) │
│ • 将纯粹指令列表 `DrawList` 批量提交至 HTML5 Canvas 2D / WebGL 后端 │
│ • 硬件光栅化绘图,全流程零 DOM 操作 │
└────────────────────────────────────────────────────────────────────────┘
5. 跨平台 DrawCmd 矢量绘制指令协议 #
计算内核只输出几何指令列表,与具体的渲染 API 彻底解耦:
pub enum DrawCmd {
Rect(rect : Rect, color : Color, rounding : Double)
RectStroke(rect : Rect, color : Color, stroke_width : Double, rounding : Double)
Text(pos : Vec2, text : String, color : Color, font_size : Double)
Circle(center : Vec2, radius : Double, color : Color)
Line(from : Vec2, to : Vec2, color : Color, width : Double)
ClipRect(rect : Rect)
RestoreClip
}
基于这一协议,同一份 MoonBit 核心不仅能在 HTML5 Canvas 2D 上运行,未来还可无缝平替接入 WebGL / WebGPU 批处理管线或原生桌面 SDL/Raylib。
6. 分层 ID 哈希系统与作用域栈 #
即时模式控件虽然每帧重新计算,但诸如文本框焦点、视窗拖拽锚点、手风琴折叠状态等交互必然需要跨帧追踪。
Id = Hash64(Parent_Scope_Id + Salt + Widget_Name)
- 作用域栈 (Scope Stack):通过
ui.push_id("panel_a")隔离命名空间,确保不同视窗中相同文本的按钮 ID 绝不冲突; hot_id:当前指针悬停的控件 ID;active_id:当前被按下激活、持有连续拖拽捕捉的控件 ID;focused_id:当前独占键盘文本输入的聚焦控件 ID。
7. Wasm 与宿主桥接互操作 #
WebAssembly 运行时与宿主 JavaScript 环境之间通过高效线性内存与预分配缓冲进行数据交互:
Float64Array / Uint32Array)进行块读取,避免了昂贵的 JSON 字符串序列化损耗。
8. 输入归一化与鼠标光标状态机 #
moon-egui 具备全状态机级别的鼠标光标动态汇报机制。在 step 执行完毕后,宿主调用 moon_gallery_cursor() 获取内核建议的光标指针:
| 光标指示 | 触发场景 | 关联组件 |
|---|---|---|
"pointer" |
按钮悬停、复选框、下拉项、标签切换、折叠栏 | button, checkbox, toggle, tab_bar, combo_box |
"text" |
单行/多行文本编辑、代码编辑区域光标定位 | text_edit, code_editor |
"ew-resize" |
横向数值拖拽调节器、视窗左右边缘缩放 | slider, drag_value, window |
"ns-resize" |
纵向尺寸手柄、滚动条拖拽滑块 | scroll_area, window |
"crosshair" |
CAD 无限画布平移测量与点位选取 | Canvas Workbench |
9. 百万级 CAD 矩阵极限性能架构 #
在 benchmark.html 演示中,面对 1,000,000 个复杂逻辑单元格,moon-egui 实现了稳定跑满 60 FPS 满帧刷新。核心优化技术包括:
- 空间 AABB 视口快速求交剔除:仅对当前摄像机视口裁剪矩形内部的可见单元格生成绘制图元,时间复杂度由 $O(N)$ 降至 $O(\text{visible})$;
- 微秒级帧计算耗时:内核每一帧的计算总耗时控制在 0.1ms 至 0.3ms 之间,为宿主 Canvas 2D 硬件光栅化留出充足的 16ms 帧预算;
- 图元批量调度:同类图形绘制指令自动连续批处理,降低宿主渲染上下文切换损耗。
10. 零垃圾回收抖动 (Zero GC Jitter) #
传统 Web 前端框架随着界面元素增多,频繁的临时对象分配会导致 V8 引擎周期性触发 Full GC,造成肉眼可见的卡顿掉帧。
moon-egui 在内部广泛重用指令列表与临时缓冲区,基础几何计算完全由值类型和轻量结构体承载,实现了极度平稳的帧时间曲线。
11. 106+ 项全功能自动化无头单测体系 #
项目构建了完备的白盒与黑盒测试套件(位于 src/core/*_wbtest.mbt 和 src/core/*_test.mbt),覆盖所有数学运算、控件交互状态机与边界用例:
$ moon test
Running 106 test(s) in packages...
Total tests: 106, passed: 106, failed: 0.
12. 确定性快照测试机制 (Snapshot Testing) #
利用 MoonBit 的 inspect() 快照机制,当几何计算或排版规则发生变更时,自动生成结构化快照对比,任何意外的布局漂移均会在 CI 中被立即捕获。
moon test --update
13. 编码风格与开源贡献规范 #
- 块注释分隔符:代码模块均采用
///|进行物理分块,保持纯净; - 弃用代码隔离:已过时的 API 统一归入各包下的
deprecated.mbt文件,不污染主逻辑; - 接口更新与格式化:在提交 PR 前必须执行
moon info && moon fmt,并校验.mbti接口变动; - 规范化提交 (Conventional Commits):提交信息遵循
feat:、fix:、refactor:、docs:格式。