gpui-kit Notification 组件实战:在 GPUI 应用中构建可自动消失的 Toast 通知与系统通知中心投递
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
gpui-kit 的Notification组件是一个面向 GPUI 桌面应用的 toast 通知系统,用于向用户显示短暂消息:通知默认出现在窗口右上角,支持超时后自动消失,并提供多种类型、标题、自定义内容与操作按钮。本文将以website/zh-CN/component/notification.md为骨架,结合crates/component下的源码实现,完整讲解通知层的挂载、构建器 API、唯一 ID 管理、系统通知中心投递及平台差异,帮助你为状态反馈、确认信息和异步操作提示构建专业级通知体验。
目录
- 导入与模块结构
- 第一步:在根视图中渲染通知层
- 基础通知与快捷构造方法
- 四种通知类型与视觉语义
- 标题、图标与自定义样式
- 自动隐藏的计时规则
- 操作按钮与可点击通知
- 自定义内容:内嵌 Markdown
- 唯一通知 ID:手动管理长任务状态
- 系统通知中心投递与平台要求
- 通知外观与布局的全局设置
- 综合示例
导入与模块结构
在你的 Cargo 项目中添加 gpui-kit 依赖后,按如下方式导入:
use gpui_kit::component::{ notification::{Notification, NotificationType}, WindowExt };Notification与NotificationType定义在 crates/component/src/notification.rs,并通过 crates/component/src/lib.rs 的pub mod notification;对外暴露。整个通知功能由三层协作完成:
Notification:可链式构建的单条通知(消息、标题、类型、ID、动作等),实现Render、Styled,并分别EventEmitter<DismissEvent>/EventEmitter<DismissRequest>,见 crates/component/src/notification.rs;NotificationList:维护同一窗口内所有通知的实体列表,负责推送、分组、生命周期推进与关闭,见 crates/component/src/notification.rs;Root与WindowExt:把NotificationList挂到窗口根视图,并向Window提供push_notification等便捷方法。
第一步:在根视图中渲染通知层
要显示通知,必须先让应用根视图渲染 notification layer。Root::render_notification_layer会把当前激活的通知渲染在应用内容之上,实现位于 crates/component/src/root.rs:它读取窗口的Root根视图,并挂载root.notification实体;当存在激活的 Sheet 时,还会按 Sheet 的Placement(上/右/下/左)自动留出对应边距,避免通知层与 Sheet 重叠。
use gpui_kit::component::{TitleBar, Root}; struct Example {} impl Render for Example { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let notification_layer = Root::render_notification_layer(window, cx); div() .size_full() .child( v_flex() .size_full() .child(TitleBar::new()) .child(div().flex_1().child("Hello world!")), ) // 将通知层渲染在应用内容之上 .children(notification_layer) } }基础通知与快捷构造方法
推送一条最简单的通知,可以直接把字符串交给window.push_notification:
window.push_notification("This is a notification.", cx);也可以使用Notification构建器显式指定消息:
Notification::new() .message("Your changes have been saved.")push_notification接受任何实现了Into<Notification>的类型——源码中为String、SharedString、&str、Cow<str>以及(NotificationType, T)元组都提供了From实现,见 crates/component/src/notification.rs。(NotificationType::Info, "message")这种写法实际展开为Notification::new().message(content).with_type(type_)。
除new()之外,源码还提供了四个类型化的快捷构造方法(crates/component/src/notification.rs):
Notification::info(message)— Info 类型Notification::success(message)— Success 类型Notification::warning(message)— Warning 类型Notification::error(message)— Error 类型
四种通知类型与视觉语义
NotificationType是一个派生Default的枚举,默认值为Info,见 crates/component/src/notification.rs。每种类型通过icon()方法映射到不同的图标与主题色(crates/component/src/notification.rs):
| 类型 | 图标 | 主题色 |
|---|---|---|
Info | IconName::Info | theme().info |
Success | IconName::CircleCheck | theme().success |
Warning | IconName::TriangleAlert | theme().warning |
Error | IconName::CircleX | theme().danger |
window.push_notification( (NotificationType::Info, "File saved successfully."), cx, ); window.push_notification( (NotificationType::Success, "Payment processed successfully."), cx, ); window.push_notification( (NotificationType::Warning, "Network connection is unstable."), cx, ); window.push_notification( (NotificationType::Error, "Failed to save file. Please try again."), cx, );带类型的通知渲染时会用对应图标和颜色填充通知左侧的图标位;若没有设置类型,则使用.icon()自定义的图标。
标题、图标与自定义样式
带标题
Notification::new() .title("Update Available") .message("A new version of the application is ready to install.") .with_type(NotificationType::Info)标题以text_sm+font_semibold样式渲染在消息上方(crates/component/src/notification.rs)。标题和消息均为可选:没有标题时通知只显示消息;两者都缺失时,该通知将不会被投递到系统通知中心(见下文系统通知章节)。
自定义图标
Notification::new() .message("Custom icon notification") .icon(Icon::new(IconName::Bell)).icon()接受任何impl Into<Icon>;如果同时设置了类型,类型自带图标优先,见 crates/component/src/notification.rs 的渲染逻辑。
自定义样式
Notification实现了Styled,因此可以直接链式调用样式方法(.bg()、.rounded()、.text_color()等)来覆盖默认外观。默认外观由BaseToast提供:边框色theme().border、背景theme().tokens.popover、圆角theme().radius_lg与toast_shadow阴影,见 crates/component/src/notification.rs。
自动隐藏的计时规则
// 关闭自动隐藏(只能手动关闭) Notification::new() .message("This notification stays until manually closed.") .autohide(false) // 开启自动隐藏(默认值) Notification::new() .message("This will disappear automatically.") .autohide(true) // 默认默认自动隐藏时长为 5 秒:在 crates/component/src/notification.rs 中,NotificationList::push会将autohide映射为ToastOptions { timeout: autohide.then_some(Duration::from_secs(5)) }。此外还有两个重要的计时行为:
- 悬停/聚焦暂停:指针悬停在通知上或某条通知获得键盘焦点时,倒计时暂停;指针移开或焦点离开后继续。
- 后台不暂停:窗口未激活时倒计时照常进行,因此不能错过的消息应关闭自动隐藏(
.autohide(false))或改用系统通知投递。这一行为有测试用例inactive_window_does_not_pause_autohide与focus_pauses_autohide_and_present_phase_is_projected佐证(crates/component/src/notification.rs)。
生命周期计时由NotificationList::start_advancing驱动(crates/component/src/notification.rs):以 50ms 为周期推进过渡相位(NOTIFICATION_ADVANCE_INTERVAL),当没有通知挂载时自动停止计时器,避免空闲窗口空转。进入动画时长 400ms、退出动画 200ms、位移偏移 96px(见文件顶部常量 crates/component/src/notification.rs)。
操作按钮与可点击通知
操作按钮
通过.action()可以在通知右侧添加一个按钮。注意:一旦设置了 action,通知将自动关闭自动隐藏(Notification::action内部强制self.autohide = false,见 crates/component/src/notification.rs),渲染时按钮会被缩放为small()尺寸:
Notification::new() .title("Connection Lost") .message("Unable to connect to server.") .with_type(NotificationType::Error) .autohide(false) .action(|_, cx| { Button::new("retry") .primary() .label("Retry") .on_click(cx.listener(|this, _, window, cx| { println!("Retrying connection..."); this.dismiss(window, cx); })) })按钮点击回调中通过this.dismiss(window, cx)关闭通知——dismiss会发出DismissRequest事件(crates/component/src/notification.rs),NotificationList订阅该事件后驱动退出动画并最终触发DismissEvent与on_close回调。
可点击通知
整条通知也可以响应点击。此时点击通知会先关闭它,再触发你的回调:
Notification::new() .message("Click to view details") .on_click(cx.listener(|_, _, _, cx| { println!("Notification clicked"); cx.notify(); }))on_click回调签名是Fn(&ClickEvent, &mut Window, &mut App)(crates/component/src/notification.rs)。此外,渲染层还注册了中键点击处理——中键点击也会关闭通知(on_aux_click+event.is_middle_click(),见 crates/component/src/notification.rs)。通知右上角的关闭按钮在悬停时显示(group_hover),点击后stop_propagation再 dismiss。
on_close回调(Fn(&mut Window, &mut App))在通知以任何方式关闭时触发:关闭按钮、中键点击、自动隐藏、点击处理器或程序化关闭,见 crates/component/src/notification.rs。
自定义内容:内嵌 Markdown
当需要比「标题 + 消息」更丰富的展示时,使用.content()提供任意 GPUI 元素。它接受一个返回AnyElement的闭包:
use gpui_kit::component::text::markdown; let markdown_content = r#" ## Custom Notification - **Feature**: New dashboard available - **Status**: Ready to use - [Learn more](https://example.com) "#; Notification::new() .content(|_, window, cx| { markdown(markdown_content).into_any_element() })markdown函数来自 crates/component/src/text 模块,用于把 Markdown 字符串渲染为元素。自定义内容会渲染在标题和消息下方;注意当只使用.content()而没有 title/message 时,系统通知投递会被跳过(系统通知需要文本)。
唯一通知 ID:手动管理长任务状态
默认情况下,每条通知使用随机 UUID 作为 ID,因此彼此独立、永不互相替换(见Notification::new中uuid::Uuid::new_v4(),crates/component/src/notification.rs)。当你需要手动管理通知——例如长任务状态或持久警告——可以为通知分配唯一 ID,用相同 ID 再次推送会替换前一条通知。
struct UpdateNotification; Notification::new() .id::<UpdateNotification>() .message("System update available") .autohide(false) struct TaskNotification; Notification::warning("Task failed to complete") .id1::<TaskNotification>("task-123") .title("Task Failed").id::<T>():以类型T作为唯一标识(TypeId),见 crates/component/src/notification.rs;.id1::<T>(key):以「类型 + 元素 ID」共同标识,可区分同一类型下的多条通知,见 crates/component/src/notification.rs。
后续通过WindowExt提供的方法移除:
// 移除所有 id 匹配 T 的通知(包括 .id 与 .id1 注册的) window.remove_notification::<UpdateNotification>(cx); // 仅移除 (T, key) 对应的单条通知 window.remove_notification1::<TaskNotification>("task-123", cx); // 清空当前窗口全部通知 window.clear_notifications(cx);这些方法定义在 crates/component/src/window_ext.rs,最终委托给Root的对应实现(crates/component/src/root.rs)。remove_notification::<T>对应NotificationList::close_by_type,会同时命中.id::<T>()与.id1::<T>(任意 key)的所有通知,该行为由测试close_by_type_removes_id_and_all_id1_of_same_type验证(crates/component/src/notification.rs);而remove_notification1只精确移除(T, key)匹配的单条(测试close_with_id_and_element_id_removes_only_matching_key,crates/component/src/notification.rs)。
系统通知中心投递与平台要求
通知不仅可以作为应用内 toast,还可以投递到操作系统的通知中心。使用NotificationDelivery选择去向:
| 枚举值 | 行为 |
|---|---|
InApp(默认) | 仅显示应用内 toast |
System | 仅投递系统通知中心,不显示 toast |
InAppAndSystem | 同时显示 toast 并投递系统通知中心 |
use gpui_kit::component::notification::{Notification, NotificationDelivery}; // 单条通知覆盖;`.system()` 和 `.in_app_and_system()` 是 // `.delivery(NotificationDelivery::...)` 的简写。 Notification::info("Your download is ready.") .title("Download complete") .system() // 或为所有通知设置全局默认值 Theme::global_mut(cx).notification.delivery = NotificationDelivery::InAppAndSystem;NotificationDelivery定义在 crates/component/src/notification.rs,并提供includes_in_app()/includes_system()两个判定方法。投递时的语义(均有源码与测试支撑):
- 标题/正文映射:title 和 message 分别成为系统通知的标题和正文;两者都缺失时不投递。只有 message 时,message 成为系统通知标题(见
push_system的匹配逻辑 crates/component/src/notification.rs,测试system_delivery_posts_to_center_without_in_app_toast验证)。 - 替换与撤回:用相同的
.id::<T>()再次推送会替换之前的系统通知(通过带gpui-component/notification/前缀的稳定 tag 实现,见system_tag(),crates/component/src/notification.rs);window.remove_notification::<T>(cx)/window.clear_notifications(cx)会撤回对应的系统通知。 - 自动隐藏与保留:toast 自动隐藏时,系统通知保留在通知中心(测试
explicit_close_retracts_system_notification_but_autohide_does_not验证,crates/component/src/notification.rs)。 - 点击行为:点击系统通知会激活应用及其窗口、关闭对应的应用内 toast(如有)、并以默认的
ClickEvent触发on_click(见SystemNotificationRegistry::handle_response,crates/component/src/notification.rs)。NotificationDelivery::System模式下没有 toast,因此on_close不会被调用。 - 响应处理器归属:
gpui_kit::component::init会注册应用级的on_system_notification_response处理器(crates/component/src/notification.rs),之后请勿再自行注册——gpui 只保留一个处理器。应用通过cx.show_system_notification直接发送的系统通知不受影响(响应处理器会忽略非本库前缀的 tag,测试response_for_a_foreign_tag_is_ignored验证,crates/component/src/notification.rs)。
平台要求
| 平台 | 要求 | 撤回 |
|---|---|---|
| macOS | 必须从可信位置(如/Applications)的打包.app运行;cargo run裸跑时静默禁用。首次投递会触发系统授权弹窗,拒绝后系统会记住该选择,后续投递静默失败 | 支持 |
| Windows | 启动早期调用cx.set_app_identity(identifier, name) | 支持 |
| Linux | 需要 XDG 通知守护进程 | 不支持(自然过期) |
通知外观与布局的全局设置
NotificationSettings定义在 crates/component/src/notification.rs,作为主题的一部分挂在theme().notification(见 crates/component/src/theme/mod.rs)。其默认值如下:
| 字段 | 默认值 | 说明 |
|---|---|---|
placement | Anchor::TopRight | 通知出现的位置;单条通知可用.placement()覆盖,每种位置各自独立堆叠 |
margins | 上下左右 16px,顶部额外加上TITLE_BAR_HEIGHT | 通知距窗口边缘的间距,顶部留白避免与标题栏重叠 |
max_items | 10 | 同时显示的最大通知数 |
width | 382px | 通知宽度 |
delivery | NotificationDelivery::InApp | 全局默认投递方式 |
全局修改方式:
let settings = &mut Theme::global_mut(cx).notification; settings.placement = Anchor::BottomRight; settings.max_items = 5; settings.width = px(420.); settings.delivery = NotificationDelivery::InAppAndSystem;NotificationList渲染时会按placement把可见通知分组到不同锚点的堆栈中(grouped,crates/component/src/notification.rs),并支持TopLeft / TopCenter / TopRight / BottomLeft / BottomCenter / BottomRight / LeftCenter / RightCenter共 8 个锚点。每个堆栈的 element id 以锚点本身为键,保证堆栈在其它位置通知消失时不会重放进入动画(测试stack_element_id_survives_other_placements_disappearing验证,crates/component/src/notification.rs)。单条通知覆盖全局位置:
Notification::info("bottom-left corner") .placement(Anchor::BottomLeft)综合示例
表单校验失败
Notification::error("Please correct the following errors before submitting.") .title("Validation Failed") .autohide(false) .action(|_, _, cx| { Button::new("review") .outline() .label("Review Form") .on_click(cx.listener(|this, _, window, cx| { // 跳转到表单并关闭通知 this.dismiss(window, cx); })) })文件上传进度
使用唯一 ID 让同一条通知在任务生命周期内被不断替换更新:
struct UploadNotification; // 开始上传 window.push_notification( Notification::info("Uploading file...") .id::<UploadNotification>() .title("File Upload") .autohide(false), cx, ); // 完成后替换为成功状态 window.push_notification( Notification::success("File uploaded successfully!") .id::<UploadNotification>() .title("Upload Complete"), cx, );系统状态更新
Notification::warning("System maintenance will begin in 30 minutes.") .title("Scheduled Maintenance") .autohide(false) .action(|_, cx| { Button::new("details") .link() .label("View Details") .on_click(cx.listener(|this, _, window, cx| { this.dismiss(window, cx); })) })批处理操作结果(富文本内容)
use gpui_kit::component::text::markdown; let results_content = r#" ## Batch Operation Complete **Processed**: 150 items **Success**: 147 items **Failed**: 3 items [View failed items](https://link.gitcode.com/i/955bf2f7c38d24071b2fa55f6f3ffe00) "#; Notification::success("Batch operation completed with some failures.") .title("Operation Results") .content(|window, cx| { markdown(results_content).into_any_element() }) .autohide(false)交互式确认(点击 + 操作按钮组合)
struct SaveConfirmation; Notification::new() .id::<SaveConfirmation>() .title("Unsaved Changes") .message("You have unsaved changes. Save before leaving?") .autohide(false) .action(|_, cx| { Button::new("save") .primary() .label("Save") .on_click(cx.listener(|this, _, window, cx| { println!("Saving changes..."); this.dismiss(window, cx); })) }) .on_click(cx.listener(|_, _, _, cx| { println!("Save reminder clicked"); cx.notify(); }))小结
gpui-kit 的Notification组件把「窗口右上角 toast + 操作系统通知中心」统一进一个构建器 API:Notification::new()链式设置消息、标题、类型、图标、唯一 ID、自动隐藏、动作按钮与自定义内容,WindowExt提供push_notification/remove_notification/remove_notification1/clear_notifications四个窗口级操作,Root::render_notification_layer负责把通知层盖在应用内容之上。其底层由ToastManager驱动 50ms 粒度、5 秒默认时长的生命周期计时,并正确处理悬停/聚焦暂停与后台继续的细节;系统通知投递则通过带命名空间前缀的稳定 tag 实现替换、撤回与点击回跳。对于长任务状态、持久告警等场景,建议关闭自动隐藏或使用系统投递,并配合唯一 ID 让通知随任务状态平滑演进。
【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考