应用内通知
用 Notifier 在窗口内弹出不打断当前任务的通知:成功、错误、进度、撤销、人员消息,堆叠、悬停展开、八个停靠位置都由它管理。
Notifier 是什么
ui.widgets.Notifier 是一个窗口级服务:初始化一次,之后在任何地方调用 show。通知卡挂在窗口浮层 portal 的 toast 层(z 2000,高于对话框、低于 tooltip),不参与页面布局;每张卡有自己固定的节点,删除其中一张不会让其他卡片重建或重放入场动画。它取代了早期版本的 ToastManager。
每种组件的逐项 API(Notification 字段、NotifierOptions、全部方法签名)见组件站的 Notification 页面。
创建 Notifier
在根挂载函数里创建一次,把指针交给需要发通知的代码(例如放进 cx.bindState 的应用状态)。Notifier 由传入的 Scope 持有,Scope 释放时一并移除。
const notifier = try ui.widgets.Notifier.init(scope, cx, .{
.position = .bottom_center,
// Keep the stack clear of a 56px toolbar.
.content_insets = .{ .top = 56 },
.max_visible = 4,
});position八个停靠位置之一,默认 .bottom_center;运行时可用 setPosition 切换max_visible展开时最多显示的条数(2–6,默认 4)expand_on_hoverfalse 时悬停只暂停计时、不展开content_insets相对窗口的内缩(标题栏、工具栏、状态栏),22px 停靠边距从这里起算width卡片宽度,默认 392strings界面文案("刚刚"、"撤销"、"回复…"、折叠角标等),做本地化时替换listener用户操作与到期事件的回调,也可用 setListener 设置弹出一条通知
show 接收一个 ui.widgets.Notification 值并返回它的 id;字符串会被 Notifier 复制持有,调用方的缓冲区可以立即复用。同屏超过 6 条时,最旧的一条会走正常的退场动画。
// Success: auto-dismisses after 3.6 s.
_ = try notifier.show(.{
.tone = .success,
.title = "Saved to iCloud",
.body = "3 files synced · 2.1 MB",
});
// Error with two actions: errors stay until the user acts.
const failed = try notifier.show(.{
.tone = .@"error",
.title = "Upload failed",
.body = "network timeout · dist/app.zip",
.actions = &.{
.{ .label = "Retry", .primary = true, .tag = "retry" },
.{ .label = "View log", .tag = "log" },
},
});
// Undo: a 7 s countdown ring and a default "Undo" button.
_ = try notifier.show(.{
.kind = .undo,
.title = "Moved 4 items to the Trash",
.body = "You can undo this for 7 seconds.",
});
// A person's message with an inline reply box.
_ = try notifier.show(.{
.tone = .violet,
.avatar = "L",
.title = "Lin",
.body = "I finished the table widths in section three.",
.reply = true,
});类型、色调与停留时长
外观由 kind(版式)和 tone(语义色)两个维度决定;行首图标默认从两者推导,也可以用 lead 或 glyph 覆盖。
.default图标 + 标题 + 正文,可选按钮、回复框、头像.progress旋转指示器 + 确定进度条 + n / N 计数,不显示生命条.undo倒计时环(中心显示剩余秒数)+ 默认的「撤销」按钮.success操作完成3.6 s.@"error"失败,需要用户处理常驻.warning需要注意但不阻塞5.6 s.info一般信息(默认)4.2 s.violet人员、邀请等社交类消息4.2 s;带按钮时常驻.quiet低打扰的状态提示(小圆点图标)2.8 s- ✓
自动常驻:progress 卡、带回复框的卡、error 色调、带按钮的 violet 卡默认不会自动消失。
- ✓
显式覆盖:
sticky = true强制常驻;duration_ms覆盖默认时长;life_bar控制底部生命条是否显示。 - ✓
undo 的时长默认 7 秒,倒计时环与计时同步。
原地转换与进度
update(id, …) 改写同一张卡的类型、色调、标题与正文,并重置生命周期;卡片不销毁、不重建,也不重放入场。进度卡用 setProgress 逐帧推进,只更新进度层。用同一个 id 再次调用 show 也会走原地转换。
const id = try notifier.show(.{
.kind = .progress,
.title = "Uploading attachment",
.body = "assets/hero-shot.png",
.progress = .{ .value = 0, .label = "Uploading · about 5 s", .count = "0 / 5" },
});
// Each tick: only the progress layer is touched, the card isn't rebuilt.
notifier.setProgress(id, 0.6, "Uploading · about 2 s", "3 / 5");
// Done: the same card turns into a success card in place —
// no new entrance, and it now dismisses itself after 3.4 s.
try notifier.update(id, .{
.tone = .success,
.title = "Upload complete",
.body = "assets/hero-shot.png",
.duration_ms = 3400,
});处理用户操作
Notifier 通过一个 ui.widgets.NotificationListener 回报事件。NotificationEvent 带有卡片 id、事件类型 kind,以及按钮的 tag / index 或回复内容 text。
.action点击了按钮(用 tag 区分).undo点击了撤销;倒计时环已原地换成 ✓.reply回复框提交(Enter 或「发送」),text 为内容.dismissed用户关闭(✕ 或拖拽甩出).expired到期自动收起const Uploads = struct {
fn onEvent(ctx: ?*anyopaque, n: *ui.widgets.Notifier, e: ui.widgets.NotificationEvent) void {
const self: *Uploads = @ptrCast(@alignCast(ctx orelse return));
switch (e.kind) {
.action => if (std.mem.eql(u8, e.tag, "retry")) {
// Turn the sticky error card into a progress card in place.
n.update(e.id, .{
.kind = .progress,
.title = "Retrying upload",
.progress = .{ .value = 0, .label = "Uploading", .count = "0 / 5" },
}) catch return;
self.restart(e.id);
} else n.dismiss(e.id),
.reply => self.send(e.text),
.undo => self.restoreTrash(),
.dismissed, .expired => {},
}
}
// …restart / send / restoreTrash
};
const uploads = try cx.bindState(Uploads, .{});
notifier.setListener(.{ .context = @ptrCast(uploads), .callback = Uploads.onEvent });堆叠与悬停
多条通知折叠成一叠:最新的一条离锚点最近、z 序最高,后面的每层露边 9px 并缩小 3.8%,最多露出三层,另有一个「还有 N 条 · 悬停展开」角标。指针进入整组外接矩形(外扩 18px)时展开,间距 9px,同时暂停所有计时;移开后折叠并继续计时。
角标旁有一个「全部清除」圆钮:第一下展开出文字,第二下才真正清除,移开即收回。代码里用 dismissAll()。
单张卡可以被拖出去关闭,会产生 dismissed 事件。
窗口尺寸变化时堆叠刚性跟随锚点,不会出现补间拖尾。
停靠位置
setPosition 只改锚点、增长方向和入场位移方向,卡片不重建;正在退场的卡片沿新方向继续完成退场。
通知中心属于应用
历史记录、勿扰模式、铃铛入口这些「通知中心」能力是业务逻辑,不在 zenit 里:在你调用 show 的地方记录历史、决定是否弹出,再通过 Listener 得知关闭、到期与按钮操作。Storybook 里的「汇总」卡片就是这种模式——勿扰结束后用一条 quiet 通知概括期间收到的内容。