问题排查
从构建错误、状态生命周期到渲染异常,按最短路径定位原因。
构建错误
invalid fingerprint
模板的指纹不能直接复用。删除 build.zig.zon 里的 .fingerprint 行,运行一次 zig build,把 Zig 输出的唯一值填回去。
import of file outside module path
不要对框架内部文件直接执行 zig test。使用 zig build test-ui 等仓库定义的测试 step(见命令速查)。
@import("ui") / @import("zenit_app") not found
确认 build.zig.zon 声明了 .zenit 依赖,并且创建 executable 之后调用了 zenit.attach(zenit_dep, exe)——它负责添加 ui / zenit_app 模块、编译 macOS 桥接并链接系统框架。
build API field mismatch
先运行 zig version。zenit 要求 Zig 0.15.2(build.zig.zon 的 minimum_zig_version),其他版本的 build API 不在支持范围。
-Dtest-mode=true has no effect
Zig 的依赖选项是隔离的。在 b.dependency("zenit", ...) 中显式转发 .@"test-mode" 与 .@"e2e-port",参考模板 templates/minimal-app/build.zig。
运行时与状态
error.StateNotFound
显式 ID 的 cx.handler(State, id, method) 在对应 state 创建之前被调用。优先改用 cx.bindState + cx.on:直接传状态指针,不再管理 ID。
leaked ArrayList / HashMap at exit
bindState 的状态归 Cx 所有,Cx 释放时一起释放;如果 T 声明了 pub 的 deinit(self: *T),框架会自动调用它。泄漏通常意味着 deinit 不是 pub,或者资源本该跟随页面生命周期。写法见下一节。
crash after switching pages
检查是否在全局状态里保存了上一个页面的 *Node 或 *Scope。页面 Scope dispose 之后这些指针全部失效,必须同时清掉。
Invalid free after updating text
节点原有文本可能是节点自己持有的副本。不要把 getText() 拿到的 props 改了 content 再 setText 回去;用 try node.setTextContent(cx.allocator, "Updated"),它会复制字符串并正确标记所有权。
状态清理的正确写法
在状态里保存 allocator 字段,让 pub deinit 自己完成清理。初始值不能已经持有资源——在 bindState 之后再分配。
const Editor = struct {
allocator: std.mem.Allocator,
lines: std.ArrayList([]const u8) = .empty,
// pub: the Cx calls this automatically when it frees the state.
pub fn deinit(self: *Editor) void {
self.lines.deinit(self.allocator);
}
};
// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(Editor, .{ .allocator = cx.allocator });只有两种情况才用 scope.onCleanup:清理函数不是 pub deinit,或者资源必须跟随某个 Scope(页面)而不是整个 Cx 的生命周期。
// Resources that must follow a page (Scope), not the whole window (Cx).
const PageCache = struct {
allocator: std.mem.Allocator,
map: std.StringHashMapUnmanaged(u32) = .empty,
// Not named `pub fn deinit`: the Cx would call it again and double-free.
fn release(self: *PageCache) void {
self.map.deinit(self.allocator);
}
};
const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);布局与绘制
state changed but the text did not
setText 本身会比较新旧签名,并按需标记 sizing / render dirty,不需要手动 markRenderDirty。文本没刷新通常是改了一份 TextProps 副本却没有调用 setText,或者显示的值没有订阅对应的 Signal。更推荐用 ui.textFmt(cx, scope, fmt, .{ signals }, props) 直接订阅 Signal / Memo。
some colors ignore a theme switch
boxStyled / textStyled 等构造器会挂 on_theme hook,cx.setTheme 只在 cx.root 子树上重放它们;普通节点和组件库的样式是 mount 时的快照。主题切换时重建相应组件树,或在 Effect 里订阅 cx.themeSignal(scope) 并更新样式;没挂在 cx.root 下的节点也不会被更新。
clicks fall through an overlay’s empty area
没有交互行为的纯视觉容器默认是 pass_through。在浮层根节点的样式扩展上设置 hit_behavior = .@"opaque"——或者直接使用自带 barrier 的 Modal / Sheet。
a huge invisible hit area in the window
挂上 ui.devtools.overlay 检查命中的节点,并用 node.globalRect() 打印它的屏幕矩形。通常是父容器的 fill / grow 配置或绝对定位范围过大;浮层未显示时也要确认它已摘出命中测试——用 node.setDisplay(.none) 隐藏的子树会整棵退出布局、绘制、命中与 Tab 遍历。
// Plain visual containers are pass-through by default.
// Make an overlay root swallow clicks on its empty area:
(try panel.style.ensureExtFallible(cx.allocator)).hit_behavior = .@"opaque";仍然无法解决
提交问题时至少附上:
- ✓
版本:zenit revision、
zig version与 macOS 版本。 - ✓
复现:最小复现代码与执行的 build step。
- ✓
输出:完整的错误输出,不要只截最后一行。
- ✓
渲染问题:截图,以及 DevTools 中相关节点的尺寸。