UI 树与布局UI tree & layout
用节点构造器和 Flex 风格布局,建立 zenit 界面的空间模型。Build zenit’s spatial model with node constructors and Flex-style layout.
心智模型Mental model
挂载函数创建一棵真实、常驻的 *ui.Node 树,只运行一次。容器决定子节点如何排列,叶子节点承载文本、图片或图标。之后每一帧,框架只从「脏」的节点重新进入管线:布局、生成绘制项、交给 Metal。Your mount function builds a real, retained tree of *ui.Nodes — once. Containers decide how children are arranged; leaves carry text, images or icons. On every later frame the framework re-enters the pipeline only from dirty nodes: layout, paint into display items, hand off to Metal.
常用构造器Constructors
ui.box通用容器,完整控制方向、尺寸、间距、背景。默认 direction 为 column,宽高为 fitGeneral container with full control of direction, size, spacing and background. Defaults to column, fit width and heightui.hstack / ui.vstack固定 direction 为 row / column 的 boxA box with direction fixed to row / columnui.text / ui.textFmt静态文本 / 订阅 Signal 的格式化文本:textFmt(cx, scope, fmt, .{signals}, props)Static text / formatted text that subscribes to Signals: textFmt(cx, scope, fmt, .{signals}, props)ui.icon / ui.iconTint / ui.svg / ui.image矢量图标、着色图标、SVG 与位图纹理Vector icons, tinted icons, SVG and bitmap texturesui.grid按列 / 行轨道排布的网格容器A grid container laid out on column / row tracksui.spacer宽高都为 grow 的弹性空白Flexible blank space, grow in both axesui.clickable给任意节点挂上 on_click 回调,返回同一个节点Attaches an on_click handler to any node and returns itui.boxStyled / hstackStyled / vstackStyled / textStyled接收具名样式函数,换主题时自动重放,见样式与主题Take a named style function and replay it on theme change — see Styling & themes布局示例A layout example
容器先创建,再用 appendChild 挂上子节点。样式值优先来自 cx.tokens,这样换主题和统一调整间距时不用逐个改字面量。Create the container, then attach children with appendChild. Style values come from cx.tokens first, so theme switches and spacing changes don’t mean hunting down literals.
const card = try ui.vstack(cx, .{
.width = .fixed(360),
.gap = cx.tokens.space._4,
.padding = ui.Padding.all(cx.tokens.space._6),
.background = cx.tokens.color.bg_secondary,
.corner_radius = cx.tokens.radius.xl,
.align_items = .stretch,
}, .{});
const header = try ui.hstack(cx, .{
.gap = cx.tokens.space._2,
.align_items = .center,
}, .{});
try header.appendChild(cx.allocator, try ui.iconTint(
cx,
ui.icons.star,
cx.tokens.color.accent,
.{ .width = .fixed(18), .height = .fixed(18) },
));
try header.appendChild(cx.allocator, try ui.text(cx, "Overview", .{}));
try header.appendChild(cx.allocator, try ui.spacer(cx));
try card.appendChild(cx.allocator, header);子节点也可以在构造时以 tuple 传入;ui.Padding.symmetric(v, h) 分别设置上下与左右内边距。Children can also be passed as a tuple at construction; ui.Padding.symmetric(v, h) sets vertical and horizontal padding separately.
// Children can also be passed as a tuple at construction time.
const row = try ui.hstack(cx, .{
.width = .fill(),
.padding = ui.Padding.symmetric(cx.tokens.space._2, cx.tokens.space._4),
.gap = cx.tokens.space._3,
}, .{
try ui.text(cx, "Name", .{}),
try ui.spacer(cx),
try ui.text(cx, "42", .{ .color = cx.tokens.color.fg_secondary }),
});
// Any node can take a click handler.
// (model: *Model from cx.bindState; Model.select is fn (*Model) void)
_ = ui.clickable(row, cx.on(Model, model, Model.select));尺寸与坐标Sizing and coordinates
宽高是一个 ui.Sizing 联合体,四个成员各有一个简写:Width and height are a ui.Sizing union; each of its four members has a shorthand:
.fixed(120).{ .px = 120 }固定像素Fixed pixels.fill().{ .grow = .{} }分配剩余空间;grow 带 min / max 载荷,例如 .{ .grow = .{ .min = 200 } }Takes the remaining space; grow carries a min / max payload, e.g. .{ .grow = .{ .min = 200 } }.pct(50).{ .percent = 50 }父节点 content box(已扣除 padding)的百分比,取值 0–100Percent (0–100) of the parent’s content box, padding excluded.{ .fit = .{} }由内容决定,box 的默认值,同样可带 min / maxSized by content; the box default, also with min / max布局结果不存在节点字段里。node.rectFromWorldOrFallback() 返回上一次布局得到的、相对父节点左上角的矩形;需要窗口坐标时用 node.globalRect(),它会沿祖先链累加位置、translate 与 sticky 偏移。Layout results aren’t stored as a node field. node.rectFromWorldOrFallback() returns the rect from the last layout pass, relative to the parent’s top-left; for window coordinates use node.globalRect(), which accumulates positions, translate and sticky offsets up the ancestor chain.
// Parent-relative rect from the last layout pass.
const local = node.rectFromWorldOrFallback();
// Window coordinates: accumulates ancestors, translate and sticky offsets.
const screen = node.globalRect();
std.log.info("{d}x{d} at ({d}, {d})", .{ screen.w, screen.h, screen.x, screen.y });
_ = local;正确更新节点Updating nodes
直接修改节点时,要让框架知道哪一段管线需要重跑。优先用 setStyle:它在编译期根据字段选出脏级别,不需要手动 markLayoutDirty / markRenderDirty。When you change a node directly, the framework has to know which part of the pipeline to re-run. Prefer setStyle: it picks the dirty level from the field at compile time, so there’s no manual markLayoutDirty / markRenderDirty.
.sizingwidth, height自身与父节点重新布局Relayout self and parent.layoutpadding, margin, gap, direction, justify, align_items, min/max_*重新布局此节点Relayout this node.interactionopacity, translate_*, corner_radius, border, z_index, cursor更新命中索引并重绘Update hit index and repaint.renderbackground, shadow, gradient, outline, text_color只重新生成绘制项Regenerate paint only.nonetab_index, layout_isolation不触发帧工作No frame work// setStyle picks the dirty level from the field at compile time.
node.setStyle(cx.allocator, .width, .fixed(240)); // sizing
node.setStyle(cx.allocator, .gap, 12); // layout
node.setStyle(cx.allocator, .background, next); // render
// Low-frequency fields live in StyleExt and need a real allocator.
node.setStyle(cx.allocator, .z_index, 10);
// Replace text: dupes the content, frees the previous owned copy,
// and re-measures only if the content actually changed.
try node.setTextContent(cx.allocator, "Updated");
// Hide without unmounting: display:none leaves layout, paint,
// hit-testing, Tab order and the a11y tree; state stays alive.
panel.setDisplay(.none);
panel.setDisplay(.flex); // back, same nodes
// Recolor an icon node whether it is icon-table or image (svgTint) backed.
_ = icon.setTint(cx.tokens.color.accent); // false: node has neithersetText 会比较新旧文本签名:影响测量时标记 sizing,只影响外观时标记 render,完全相同则什么也不做。所以更新文本后不需要再调用 markRenderDirty。setText compares the old and new text signatures: sizing-dirty if measurement changes, render-dirty if only appearance does, nothing at all if identical. No markRenderDirty needed after updating text.
要临时隐藏一段子树,用 node.setDisplay(.none)(或在 BoxStyle 里写 .display = .none):节点连同子树不占空间、不计 gap、不绘制,也退出命中测试、Tab 遍历与无障碍树;节点和状态保持存活,.flex 即可恢复,不必卸载重建。setDisplay 会自己标脏本节点与父节点的布局。只把 opacity 设为 0 虽然也不再命中,但仍然占位、仍在 Tab 顺序里。To hide a subtree for a while, call node.setDisplay(.none) (or set .display = .none in a BoxStyle): the node and its subtree take no space, count no gap and don’t paint, and they leave hit-testing, Tab traversal and the accessibility tree. Nodes and state stay alive, and .flex brings them back — no unmount and rebuild. setDisplay marks the node’s and its parent’s layout dirty itself. Opacity 0 alone also stops hit-testing, but the node still takes up space and stays in the Tab order.