///|
/// 嵌入式运行时 SDK:其他项目加载 .moonviz → 动态渲染 + 事件处理。
///
/// 宿主应用(编辑器 / Web 应用 / 桌面工具 / Agent 工作台)通过这个 SDK:
/// 1. `MoonVizRT::create(project)` → 创建运行时实例
/// 2. `rt.handle_event(event)` → 处理交互事件,返回状态变更列表
/// 3. `rt.render_plan()` → 获取后端无关的渲染指令(RenderPlan)
/// 4. `rt.hot_reload_doc(doc)` → 替换画板文档,保留导航栈与输入值
///
/// RenderPlan 是一个 **显示列表**(display list):
/// 任何渲染后端(Canvas / Skia / SVG / 终端 / OpenGL)都能消费。
// ---------------------------------------------------------------------------
// 事件
// ---------------------------------------------------------------------------
pub(all) enum VizEvent {
TapEvent(Double, Double) // (x, y)
LongPressEvent(Double, Double)
SwipeEvent(String) // "left" | "right" | "up" | "down"
KeyEvent(String) // "enter" | "escape" | ...
FocusEvent(String) // node_id
BlurEvent(String) // node_id
}
///|
pub fn VizEvent::describe(self : VizEvent) -> String {
match self {
TapEvent(x, y) => "tap(\{x.to_int()}, \{y.to_int()})"
LongPressEvent(x, y) => "long_press(\{x.to_int()}, \{y.to_int()})"
SwipeEvent(dir) => "swipe_\{dir}"
KeyEvent(key) => "key_\{key}"
FocusEvent(id) => "focus(\{id})"
BlurEvent(id) => "blur(\{id})"
}
}
// ---------------------------------------------------------------------------
// 状态变更(事件处理后返回)
// ---------------------------------------------------------------------------
///|
pub(all) enum StateChange {
ArtboardChanged(String) // 当前画板变了
NodeUpdated(String) // 某个节点更新了
NavigationPushed(String) // 导航栈压入
NavigationPopped(String) // 导航栈弹出
ToastShown(String) // 显示 toast
FormSubmitted(Map[String, String]) // 表单提交 (node_id → text)
}
///|
pub fn StateChange::describe(self : StateChange) -> String {
match self {
ArtboardChanged(ab) => "artboard_changed:\{ab}"
NodeUpdated(id) => "node_updated:\{id}"
NavigationPushed(ab) => "nav_push:\{ab}"
NavigationPopped(ab) => "nav_pop:\{ab}"
ToastShown(msg) => "toast:\"\{msg}\""
FormSubmitted(data) => "form_submitted(\{data.size()} fields)"
}
}
// ---------------------------------------------------------------------------
// 渲染计划(后端无关的显示列表)
// ---------------------------------------------------------------------------
///|
pub(all) enum RenderCommand {
CmdRect(Double, Double, Double, Double, String, Double) // x,y,w,h,fill,radius
CmdText(Double, Double, String, Double, String) // x,y,content,font_size,color
CmdLine(Double, Double, Double, Double, String) // x1,y1,x2,y2,color
CmdClipPush(Double, Double, Double, Double) // clip rect
CmdClipPop
}
///|
pub(all) struct RenderPlan {
artboard : String
width : Double
height : Double
commands : Array[RenderCommand]
}
// ---------------------------------------------------------------------------
// 运行时
// ---------------------------------------------------------------------------
///|
pub(all) struct MoonVizRT {
project : Project
nav : NavigationState
mut overlay_toast : String? // 当前显示的 toast
mut input_values : Map[String, String] // 输入框值 (node_id → text)
listeners : Array[(StateChange) -> Unit] // 状态变更回调
}
///|
/// 创建运行时实例
pub fn MoonVizRT::create(
project : Project,
initial_artboard? : String = "",
) -> Result[MoonVizRT, String] {
let first : String = if initial_artboard == "" {
// 取第一个画板
let mut first_id = ""
for id in project.artboards.keys() {
if first_id == "" {
first_id = id
}
}
if first_id == "" {
return Err("no_artboards")
}
first_id
} else {
if !project.artboards.contains(initial_artboard) {
return Err("unknown_artboard: \{initial_artboard}")
}
initial_artboard
}
Ok({
project,
nav: NavigationState::new(first),
overlay_toast: None,
input_values: Map::new(),
listeners: [],
})
}
// ---------------------------------------------------------------------------
// 事件处理
// ---------------------------------------------------------------------------
///|
/// 处理一个交互事件,返回状态变更列表
pub fn MoonVizRT::handle_event(
self : MoonVizRT,
event : VizEvent,
) -> Array[StateChange] {
let changes : Array[StateChange] = []
match event {
TapEvent(x, y) =>
// 命中检测:找到当前画板中被点击的节点
match self.project.artboards.get(self.nav.current) {
Some(ab) => {
let layout = solve(ab.doc)
let hit = pick_node(ab.doc, layout, x, y)
match hit {
Some(node_id) => {
// 查找该节点是否有交互定义(通过 flow)
let flow_target = self.find_flow_target(node_id)
match flow_target {
Some(target) => {
// 导航
let _ = self.nav.navigate(target)
changes.push(NavigationPushed(target))
changes.push(ArtboardChanged(target))
}
None =>
// 无导航流:尝试节点交互标记(⚡trigger→action);再退到内置
// 返回语义——tap 命中形似返回的节点(back/prev/返回/< 等)
// 时强制返回上一层(返回交互是必须的:原型绝不因漏接 flow
// 而无路可退;与宿主端 isBackish 同语义,2026-10-11 下沉引擎)。
// 栈底再返回 = 原地不动;都不是才报状态更新
if !self.run_marker(node_id, changes) {
let node = ab.doc.nodes.get(node_id)
let back_like = match node {
Some(n) => is_back_like_node(n)
None => false
}
if back_like {
match self.nav.go_back() {
Some(prev) => {
changes.push(NavigationPopped(prev))
changes.push(ArtboardChanged(prev))
}
None => changes.push(NodeUpdated(node_id))
}
} else {
changes.push(NodeUpdated(node_id))
}
}
}
}
None => ()
}
}
None => ()
}
KeyEvent(key) =>
if key == "escape" || key == "back" {
// 返回上一页
match self.nav.go_back() {
Some(prev) => {
changes.push(NavigationPopped(prev))
changes.push(ArtboardChanged(prev))
}
None => ()
}
} else {
// 键盘导航流:flow - key_
let trigger = "key_\{key}"
for e in self.project.flows.iter() {
if e.from == self.nav.current && e.trigger == trigger {
let _ = self.nav.navigate(e.to)
changes.push(NavigationPushed(e.to))
changes.push(ArtboardChanged(e.to))
break
}
}
}
SwipeEvent(dir) =>
// 滑动也可能触发导航。与 KeyEvent 臂同语义:命中首条即停——
// 同画板多条同 trigger 的 swipe flow 只执行第一条,否则导航栈
// 连推多个画板、成串状态变更(此前漏 break,与 tap/key 不一致)
match self.project.artboards.get(self.nav.current) {
Some(ab) => {
let trigger = "swipe_\{dir}"
for e in self.project.flows.iter() {
if e.from == self.nav.current && e.trigger == trigger {
let _ = self.nav.navigate(e.to)
changes.push(NavigationPushed(e.to))
changes.push(ArtboardChanged(e.to))
break
}
}
}
None => ()
}
_ => ()
}
// 通知监听器
for c in changes.iter() {
for l in self.listeners.iter() {
l(c)
}
}
changes
}
///|
/// 查找节点的导航流目标。
/// 必须同时匹配 from == 当前画板:node id 在不同画板间会重名,
/// 仅按 trigger 匹配会让 A 画板的点击触发 B 画板的跳转。
fn MoonVizRT::find_flow_target(self : MoonVizRT, node_id : String) -> String? {
let trigger = "tap:\{node_id}"
for e in self.project.flows.iter() {
if e.from == self.nav.current && e.trigger == trigger {
return Some(e.to)
}
}
None
}
///|
/// 把节点当前状态的补丁应用到文档(渲染/命中前调用)。
/// 状态补丁是渲染期变换:不改写 canonical MBT,只在当前会话视图上生效。
fn MoonVizRT::apply_active_state(self : MoonVizRT, node_id : String) -> Unit {
match self.project.active_state_patch(node_id) {
None => ()
Some(p) =>
match self.project.artboards.get(self.nav.current) {
Some(ab) => {
let _ = ab.doc.update_node(node_id, fn(n) { apply_node_patch(n, p) })
}
None => ()
}
}
}
///|
/// 执行节点上的交互标记(无 flow 时的回落路径),返回是否已处理。
/// 标记由 add_interaction 写入节点名;此前只写不读(write-only API),
/// 本函数让 Trigger/Action 词汇表在运行时真正生效。
fn MoonVizRT::run_marker(
self : MoonVizRT,
node_id : String,
changes : Array[StateChange],
) -> Bool {
let node_name = match self.project.artboards.get(self.nav.current) {
Some(ab) =>
match ab.doc.nodes.get(node_id) {
Some(n) => n.name
None => return false
}
None => return false
}
let (trigger, spec) = match parse_interaction_marker(node_name) {
Some(t) => t
None => return false
}
// 标记仅在 tap 命中时执行;其他触发器由对应事件路径处理
if trigger != "tap" {
return false
}
match action_from_spec(spec) {
None => false
Some(act) =>
match act {
NavigateTo(target) =>
if self.project.artboards.contains(target) {
let _ = self.nav.navigate(target)
changes.push(NavigationPushed(target))
changes.push(ArtboardChanged(target))
true
} else {
false
}
Back =>
match self.nav.go_back() {
Some(prev) => {
changes.push(NavigationPopped(prev))
changes.push(ArtboardChanged(prev))
true
}
None => false
}
ShowToast(msg) => {
self.overlay_toast = Some(msg)
changes.push(ToastShown(msg))
true
}
SetText(id, text) =>
match self.project.artboards.get(self.nav.current) {
Some(ab) =>
if ab.doc.update_node(id, fn(n) { { ..n, text, } }) {
changes.push(NodeUpdated(id))
true
} else {
false
}
None => false
}
SetState(id, state) =>
match self.project.set_node_state(node_id=id, state_name=state) {
Ok(_) => {
self.apply_active_state(id)
changes.push(NodeUpdated(id))
true
}
Err(_) => false
}
ToggleState(id, _) =>
// 在 default 与最近定义的非 default 状态间切换
match self.project.node_states.get(id) {
None => false
Some(def) => {
let mut target = ""
for s in def.states.keys() {
if s != "default" {
target = s
break
}
}
if target == "" {
return false
}
match
self.project.set_node_state(
node_id=id,
state_name=target,
toggle=true,
) {
Ok(_) => {
self.apply_active_state(id)
changes.push(NodeUpdated(id))
true
}
Err(_) => false
}
}
}
_ => false
}
}
}
// ---------------------------------------------------------------------------
// 渲染计划生成
// ---------------------------------------------------------------------------
///|
/// 当前状态 → 后端无关的渲染指令列表
pub fn MoonVizRT::render_plan(self : MoonVizRT) -> Result[RenderPlan, String] {
match self.project.artboards.get(self.nav.current) {
None => Err("unknown_artboard")
Some(ab) => {
let layout = solve(ab.doc)
let plan : RenderPlan = {
artboard: self.nav.current,
width: ab.width,
height: ab.height,
commands: [],
}
// 树序遍历生成渲染指令
rt_walk(plan, ab.doc, ab.doc.root_id, layout)
// toast 覆盖层
match self.overlay_toast {
Some(msg) => {
plan.commands.push(
CmdRect(
ab.width * 0.1,
ab.height * 0.85,
ab.width * 0.8,
44.0,
"#333333",
22.0,
),
)
plan.commands.push(
CmdText(
ab.width * 0.5 - msg.length().to_double() * 4.0,
ab.height * 0.85 + 28.0,
msg,
14.0,
"#FFFFFF",
),
)
}
None => ()
}
Ok(plan)
}
}
}
///|
fn rt_walk(
plan : RenderPlan,
doc : Document,
id : String,
layout : SolvedLayout,
) -> Unit {
match (doc.nodes.get(id), layout.rect_of(id)) {
(Some(n), Some(r)) if !n.style.visible => ()
(Some(n), Some(r)) => {
match n.kind {
Text =>
plan.commands.push(
CmdText(
r.x,
r.y + r.height * 0.7,
n.text,
n.style.font_size,
n.style.text_color,
),
)
_ => {
let fill = if n.style.fill == "none" {
"transparent"
} else {
n.style.fill
}
plan.commands.push(
CmdRect(r.x, r.y, r.width, r.height, fill, n.style.corner_radius),
)
if n.text != "" {
plan.commands.push(
CmdText(
r.x + 8.0,
r.y + r.height * 0.6,
n.text,
n.style.font_size,
n.style.text_color,
),
)
}
}
}
for cid in n.children.iter() {
rt_walk(plan, doc, cid, layout)
}
}
_ => ()
}
}
// ---------------------------------------------------------------------------
// 热重载(Agent 修改声明后,运行时无刷新更新)
// ---------------------------------------------------------------------------
///|
/// 热重载:用新的文档替换当前画板内容,保留导航栈与仍存在的输入值。
///
/// 声明文本解析属于 decl 包,core 不依赖 decl——故本原语接收已解析好的 Document。
/// 旧版接收字符串却仅校验非空就返回 hot_reloaded:true,是结构性假成功(core 永远
/// 无法真正解析声明文本),已移除,避免调用方误信变更已生效。
pub fn MoonVizRT::hot_reload_doc(
self : MoonVizRT,
doc : Document,
) -> Result[String, String] {
let ab = match self.project.artboards.get(self.nav.current) {
Some(a) => a
None => return Err("unknown_artboard: \{self.nav.current}")
}
if doc.nodes.length() == 0 {
return Err("empty_document")
}
if !doc.nodes.contains(doc.root_id) {
return Err("document_missing_root")
}
ab.doc.nodes = doc.nodes
ab.doc.root_id = doc.root_id
ab.doc.revision = ab.doc.revision + 1
// 输入值只保留仍然存在的节点,避免残留指向已删除控件的值
let kept : Map[String, String] = Map::new()
for k in self.input_values.keys() {
match self.input_values.get(k) {
Some(v) => if ab.doc.nodes.contains(k) { kept.set(k, v) }
None => ()
}
}
self.input_values = kept
let n = (ab.doc.nodes.length() - 1).to_string()
Ok(
"{\"ok\":true,\"artboard\":\"\{self.nav.current}\",\"hot_reloaded\":true,\"nodes\":\{n}}",
)
}
// ---------------------------------------------------------------------------
// 输入值管理
// ---------------------------------------------------------------------------
///|
pub fn MoonVizRT::set_input(
self : MoonVizRT,
node_id : String,
value : String,
) -> Unit {
self.input_values.set(node_id, value)
}
///|
pub fn MoonVizRT::get_input(self : MoonVizRT, node_id : String) -> String {
match self.input_values.get(node_id) {
Some(v) => v
None => ""
}
}
///|
pub fn MoonVizRT::get_all_inputs(self : MoonVizRT) -> Map[String, String] {
self.input_values
}
// ---------------------------------------------------------------------------
// 状态监听
// ---------------------------------------------------------------------------
///|
pub fn MoonVizRT::on_change(
self : MoonVizRT,
listener : (StateChange) -> Unit,
) -> Unit {
self.listeners.push(listener)
}
// ---------------------------------------------------------------------------
// 导航查询
// ---------------------------------------------------------------------------
///|
pub fn MoonVizRT::current_artboard(self : MoonVizRT) -> String {
self.nav.current
}
///|
pub fn MoonVizRT::nav_stack(self : MoonVizRT) -> Array[String] {
self.nav.stack.copy()
}
///|
pub fn MoonVizRT::can_go_back(self : MoonVizRT) -> Bool {
self.nav.stack.length() > 1
}
// ---------------------------------------------------------------------------
// 辅助
// ---------------------------------------------------------------------------
///|
fn pick_node(
doc : Document,
layout : SolvedLayout,
x : Double,
y : Double,
) -> String? {
let mut best : String? = None
let mut best_area : Double = 1000000000000000000.0
for id in layout.rects.keys() {
match layout.rect_of(id) {
Some(r) =>
match doc.nodes.get(id) {
Some(n) if n.style.visible &&
x >= r.x &&
y >= r.y &&
x < r.x + r.width &&
y < r.y + r.height => {
let area = r.width * r.height
if area < best_area && id != "root" {
best = Some(id)
best_area = area
}
}
_ => ()
}
None => ()
}
}
best
}
// ---------------------------------------------------------------------------
// 输入值管理
// ---------------------------------------------------------------------------