-
Notifications
You must be signed in to change notification settings - Fork 9
Guide Notification
NotificationCenter is the global toast / notification manager.
It owns the queue, schedules auto-dismiss timers, and exposes
items() for the renderer to paint. The headless layer lives in
crates/yororen-ui-core/src/notification/center.rs; the visual
host is a caller-rendered widget that you wire into the render
tree.
This page covers the global + the builder, the deferred placement above modals, and the sticky / manual-dismiss semantics.
- You need to show toasts from anywhere in your app.
- You want automatic queue management (max 5 by default).
- You want one of:
- Auto-dismiss after a configurable duration (default 4 s).
- Manual-dismiss (sticky) for important alerts.
- Persistence across process restarts for sticky items.
- You want click / dismiss callbacks attached to a specific notification.
If you only need a single in-page message (e.g. an inline error
under a form field), use Toast instead —
that's a local visual component, not a global queue.
Three pieces:
-
NotificationCenter—gpui::Globalthin state machine. Owns the queue, schedules auto-dismiss timers, exposesitems()for the renderer, and persists sticky items. -
Toast host — caller-rendered scaffold that reads
center.items()and paints the stack. Thegallery_demo'snotifications_host.rsis the canonical implementation; copy it. -
Notification— the payload: message, optional title,ToastKind, dismiss strategy, optional sticky flag, optional payload JSON, optional action label.
Call once at boot, after the renderer is installed but before opening any window that may show toasts:
use yororen_ui::notification::center::NotificationCenter;
cx.set_global(NotificationCenter::new());The center is a gpui::Global (cheap to clone — it's an
Arc<Mutex<…>> underneath), so you can grab it from any event
handler:
let center = cx.global::<NotificationCenter>().clone();use yororen_ui::notification::center::{Notification, ToastKind};
center.notify(
Notification::new("Saved!")
.title("Done")
.kind(ToastKind::Success),
cx,
);ToastKind is one of Neutral | Success | Warning | Error | Info. The renderer maps each kind to the
status.<kind>.bg / .fg colors in your theme.
notify returns the NotificationId if you need to dismiss a
specific toast later:
let id = center.notify(Notification::new("Hello"), cx);
center.dismiss(id, cx); // remove it from the queue| Method | What it does |
|---|---|
Notification::new(message) |
constructor |
.title("Done") |
optional bold title |
.kind(ToastKind::Success) |
Neutral (default) / Success / Warning / Error / Info
|
.dismiss(DismissStrategy::After { duration_ms: 4000 }) |
override the default 4-second timer |
.dismiss(DismissStrategy::Manual) |
never auto-dismiss |
.sticky(true) |
upgrade dismiss to Manual and mark for cross-restart persistence |
.action_label("View") |
label for a click action |
.payload(json!({…})) |
arbitrary JSON for the click callback |
pub enum DismissStrategy {
/// Never auto-dismiss. User must explicitly close.
Manual,
/// Auto-dismiss after `duration_ms`.
After { duration_ms: u64 },
}
impl Default for DismissStrategy {
fn default() -> Self { Self::After { duration_ms: 4000 } }
}The bundled default-renderer ships a notification_host.rs
file, but it depends on a renderer-internal module that does
not exist in the current API — it is dead code. Implementing
the host in your app (or copying the one from gallery_demo)
is the intended path.
The host reads center.items() and renders a stack of cards
floating to the top-right of the window. The shape
(paraphrasing gallery_demo/src/notifications_host.rs):
use gpui::{
Context, Hsla, IntoElement, ParentElement, Render, Styled, Window,
deferred, div, hsla, px,
};
use yororen_ui::notification::center::{
Notification, NotificationCenter, NotificationId, ToastKind,
};
use yororen_ui::theme::ActiveTheme;
/// Paint priority — 3 sits above modal scrim (2) and popovers (1).
const HOST_PRIORITY: usize = 3;
pub fn render_host(cx: &mut Context<MyApp>) -> impl IntoElement {
let mut stack = div()
.id("ui:notification-host")
.absolute()
.top_0()
.right_0()
.mt(px(16.))
.mr(px(16.))
.flex()
.flex_col()
.gap(px(8.))
.items_end();
let Some(center) = cx.try_global::<NotificationCenter>() else {
return stack;
};
for n in center.items() {
stack = stack.child(toast_card(cx, n));
}
stack
}
fn toast_card(cx: &mut Context<MyApp>, n: Notification) -> gpui::Stateful<gpui::Div> {
let kind_path = match n.kind {
ToastKind::Success => "status.success",
ToastKind::Warning => "status.warning",
ToastKind::Error => "status.error",
ToastKind::Info => "status.info",
ToastKind::Neutral => "status.neutral",
};
let bg = cx.theme().get_color(&format!("{kind_path}.bg"))
.unwrap_or_else(|| cx.theme().get_color("surface.raised").unwrap_or_else(default_surface));
let fg = cx.theme().get_color(&format!("{kind_path}.fg"))
.unwrap_or_else(|| cx.theme().get_color("content.primary").unwrap_or_else(default_text));
let id = n.id;
let center = cx.try_global::<NotificationCenter>().cloned()
.unwrap_or_else(NotificationCenter::new);
let close = div()
.id(("ui:notification:dismiss", id.raw()))
.w(px(20.)).h(px(20.))
.flex().items_center().justify_center()
.cursor_pointer()
.text_color(fg).opacity(0.55)
.on_click(move |_ev, _w, cx| center.dismiss(id, cx))
.child("×");
let title_row = div().flex().flex_row().items_start().justify_between().gap(px(8.))
.child(
div().flex_1().flex().flex_col().gap(px(2.))
.when_some(n.title.clone(), |d, t|
d.child(div().text_sm().font_weight(gpui::FontWeight::BOLD).text_color(fg).child(t)))
.child(div().text_sm().text_color(fg).child(n.message.clone()))
)
.child(close);
div()
.id(("ui:notification", id.raw()))
.w(px(320.)).p(px(12.)).rounded(px(6.))
.border_1().border_color(bg)
.bg(bg)
.child(title_row)
}
fn default_surface() -> Hsla { hsla(0.0, 0.0, 0.95, 1.0) }
fn default_text() -> Hsla { hsla(0.0, 0.0, 0.10, 1.0) }Then add it as a deferred child of your root:
impl Render for MyApp {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.child(/* … page content … */)
// Modals are siblings, painted at priority 2.
// .child(deferred(modal_el).with_priority(2))
// Toast host is the topmost overlay — priority 3.
.child(deferred(render_host(cx)).with_priority(HOST_PRIORITY))
}
}Priority 3 keeps toasts above the modal scrim (priority 2) and popover / dropdown panels (priority 1). Bump the priority if a future overlay needs to sit above notifications.
root
├── page content
├── modal scrim + panel (deferred priority 2)
├── popover / dropdown / menu (deferred priority 1, owned by renderer)
└── notification host (deferred priority 3)
The center refuses to schedule the auto-dismiss timer unless
register_host_window has been called. Your host's
Render::render should call
center.register_host_window(window.window_handle()) on every
paint (the gallery_demo host does this). Without that,
non-sticky toasts never auto-dismiss.
impl Render for MyApp {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if let Some(center) = cx.try_global::<NotificationCenter>() {
center.register_host_window(window.window_handle());
}
// … render …
}
}Notification::sticky(true) upgrades dismiss to Manual
automatically (both in the builder and in the scheduler,
dual-guarded). Use it for:
- Error alerts that need acknowledgment.
- Long-running-task notifications ("Indexing 1.2M files…") that should stay until the user dismisses them.
- Notifications you want to persist across process restarts — sticky items are written to the persisted snapshot and reloaded on next launch.
// Sticky error — must be dismissed manually, persists across restart.
center.notify(
Notification::new("Build complete — 12 errors")
.title("Build failed")
.kind(ToastKind::Error)
.sticky(true), // also sets dismiss to Manual
cx,
);
// Manual-dismiss without persistence (rare).
center.notify(
Notification::new("Welcome!")
.kind(ToastKind::Info)
.dismiss(DismissStrategy::Manual),
cx,
);The scheduler is defensive: a hand-built Notification with
sticky: true and dismiss: After { … } will still not
auto-dismiss — sticky always wins.
For click / dismiss callbacks, use notify_with_callbacks:
use std::sync::Arc;
use yororen_ui::notification::center::Notification;
center.notify_with_callbacks(
Notification::new("Click to view details")
.kind(ToastKind::Info)
.action_label("View")
.payload(serde_json::json!({ "id": 42 })),
Some(Arc::new(|n, _ev, _w, cx| {
// n.payload contains the JSON
println!("clicked: {:?}", n.payload);
})),
Some(Arc::new(|_n, _w, cx| {
// fired on explicit dismiss (auto or manual)
})),
cx,
);Callbacks are not persisted across restarts. They're
re-attached each launch by re-emitting
notify_with_callbacks for each loaded sticky notification.
let center = cx.global::<NotificationCenter>().clone();
// Queue length (default 5; oldest is dropped when over capacity).
center.set_max_queue_len(10);
// Persistence (default on, key "yororen_ui:notifications").
center.set_persistence(false, "my-app-key");| Method | What it does |
|---|---|
NotificationCenter::new() |
constructor |
.notify(notification, cx) |
push a notification, returns NotificationId
|
.notify_with_callbacks(n, on_click, on_dismiss, cx) |
push with callbacks |
.dismiss(id, cx) |
dismiss a specific notification |
.clear(cx) |
dismiss all |
.items() |
snapshot the queue (used by the host) |
.set_max_queue_len(n) |
set the queue cap |
.set_persistence(enabled, key) |
toggle persistence |
.register_host_window(window) |
required for auto-dismiss to fire |
pub struct NotificationId(u64);
impl NotificationId {
pub fn allocate() -> Self; // process-local monotonic counter
pub fn raw(self) -> u64; // useful when building a gpui::ElementId
}Stable, opaque, monotonically-increasing. Used to disambiguate
stacked toasts in the gpui element tree and as the key in the
center's HashMap<NotificationId, ClickCb> / DismissCb.
- Guide-Quick-Start §5 — installing the global at boot
- Guide-Composing-UI §5 — the popover-with-menu pattern that often pairs with toasts
-
Demos —
gallery_demo/src/notifications_host.rsis the canonical host implementation -
codex-skills/yororen-ui-app-core§6 — install + notify + sticky
Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.