Skip to main content

ui/components/notification/
toast_stack.rs

1use crate::AnnouncementToast;
2use crate::prelude::*;
3use gpui::AnyElement;
4use smallvec::SmallVec;
5
6/// Default cap on simultaneously visible toasts (Tailwind reference spec: max 3 visible).
7pub const TOAST_STACK_MAX_VISIBLE: usize = 3;
8
9/// A bottom-right stacking container for [`crate::AnnouncementToast`] instances.
10///
11/// The caller owns the toast list (and its lifetime/state) and passes already-built
12/// `AnnouncementToast` elements as children on every render; [`ToastStack`] only
13/// handles layout (vertical stack, `gap-3`, bottom-right corner) and caps how many
14/// are shown at once via [`Self::max_visible`] — any elements beyond the cap are
15/// simply not rendered (the caller's own list still holds them, e.g. for a "N more"
16/// indicator).
17///
18/// Auto-dismiss timing is intentionally **caller-driven**: this component has no
19/// timer of its own. Callers wanting a timed dismissal should schedule it themselves
20/// (e.g. via `cx.spawn` + `cx.background_executor().timer(..)`, the same pattern used
21/// elsewhere in this crate) and remove the toast from their owned list when it fires.
22#[derive(IntoElement, RegisterComponent)]
23pub struct ToastStack {
24    children: SmallVec<[AnyElement; 4]>,
25    max_visible: usize,
26}
27
28impl ToastStack {
29    pub fn new() -> Self {
30        Self {
31            children: SmallVec::new(),
32            max_visible: TOAST_STACK_MAX_VISIBLE,
33        }
34    }
35
36    /// Caps how many toasts are rendered at once. Values below `1` are clamped to `1`.
37    pub fn max_visible(mut self, max: usize) -> Self {
38        self.max_visible = max.max(1);
39        self
40    }
41}
42
43impl Default for ToastStack {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl ParentElement for ToastStack {
50    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
51        self.children.extend(elements)
52    }
53}
54
55impl RenderOnce for ToastStack {
56    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
57        let visible = self.children.into_iter().take(self.max_visible);
58
59        div().absolute().bottom_0().right_0().p_4().child(
60            v_flex()
61                .id("toast-stack")
62                .gap(DynamicSpacing::Base12.rems(cx))
63                .items_end()
64                .children(visible),
65        )
66    }
67}
68
69impl Component for ToastStack {
70    fn scope() -> ComponentScope {
71        ComponentScope::Notification
72    }
73
74    fn description() -> Option<&'static str> {
75        Some(
76            "Stacks AnnouncementToast instances bottom-right, capping visible count; auto-dismiss and list ownership stay with the caller.",
77        )
78    }
79
80    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
81        Some(
82            example_group(vec![single_example(
83                "Basic",
84                div()
85                    .relative()
86                    .w(px(480.))
87                    .h(px(320.))
88                    .overflow_hidden()
89                    .child(
90                        ToastStack::new()
91                            .child(
92                                div().w_80().child(
93                                    AnnouncementToast::new()
94                                        .severity(Severity::Success)
95                                        .heading("Saved")
96                                        .description("Your changes were saved."),
97                                ),
98                            )
99                            .child(
100                                div().w_80().child(
101                                    AnnouncementToast::new()
102                                        .severity(Severity::Info)
103                                        .heading("Sync complete")
104                                        .description("All projects are up to date."),
105                                ),
106                            ),
107                    )
108                    .into_any_element(),
109            )])
110            .into_any_element(),
111        )
112    }
113}