gpui_ui_kit/
toast.rs

1//! Toast notification component
2//!
3//! Provides non-blocking notifications that appear temporarily.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Toast visual variant
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ToastVariant {
11    /// Informational message (default)
12    #[default]
13    Info,
14    /// Success message
15    Success,
16    /// Warning message
17    Warning,
18    /// Error message
19    Error,
20}
21
22impl ToastVariant {
23    fn icon(&self) -> &'static str {
24        match self {
25            ToastVariant::Info => "ℹ",
26            ToastVariant::Success => "✓",
27            ToastVariant::Warning => "⚠",
28            ToastVariant::Error => "✕",
29        }
30    }
31}
32
33/// Toast position on screen
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum ToastPosition {
36    /// Top right corner
37    TopRight,
38    /// Top left corner
39    TopLeft,
40    /// Bottom right corner (default)
41    #[default]
42    BottomRight,
43    /// Bottom left corner
44    BottomLeft,
45    /// Top center
46    TopCenter,
47    /// Bottom center
48    BottomCenter,
49}
50
51/// A single toast notification
52pub struct Toast {
53    id: ElementId,
54    title: Option<SharedString>,
55    message: SharedString,
56    variant: ToastVariant,
57    closeable: bool,
58    on_close: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
59}
60
61impl Toast {
62    /// Create a new toast with a message
63    pub fn new(id: impl Into<ElementId>, message: impl Into<SharedString>) -> Self {
64        Self {
65            id: id.into(),
66            title: None,
67            message: message.into(),
68            variant: ToastVariant::default(),
69            closeable: true,
70            on_close: None,
71        }
72    }
73
74    /// Set the toast title
75    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
76        self.title = Some(title.into());
77        self
78    }
79
80    /// Set the toast variant
81    pub fn variant(mut self, variant: ToastVariant) -> Self {
82        self.variant = variant;
83        self
84    }
85
86    /// Set whether the toast is closeable
87    pub fn closeable(mut self, closeable: bool) -> Self {
88        self.closeable = closeable;
89        self
90    }
91
92    /// Set the close handler
93    pub fn on_close(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
94        self.on_close = Some(Box::new(handler));
95        self
96    }
97
98    /// Build the toast into an element
99    pub fn build(self) -> Stateful<Div> {
100        // Get colors based on variant
101        let (bg, border, icon_color) = match self.variant {
102            ToastVariant::Info => (rgb(0x2a2a2a), rgb(0x007acc), rgb(0x007acc)),
103            ToastVariant::Success => (rgb(0x1a3a1a), rgb(0x2da44e), rgb(0x2da44e)),
104            ToastVariant::Warning => (rgb(0x3a3a1a), rgb(0xd29922), rgb(0xd29922)),
105            ToastVariant::Error => (rgb(0x3a1a1a), rgb(0xcc3333), rgb(0xcc3333)),
106        };
107        let icon = self.variant.icon();
108
109        let mut toast = div()
110            .id(self.id)
111            .w(px(320.0))
112            .flex()
113            .items_start()
114            .gap_3()
115            .px_4()
116            .py_3()
117            .bg(bg)
118            .border_1()
119            .border_color(border)
120            .rounded_lg()
121            .shadow_lg();
122
123        // Icon
124        toast = toast.child(
125            div()
126                .text_lg()
127                .text_color(icon_color)
128                .mt(px(2.0))
129                .child(icon),
130        );
131
132        // Content area
133        let mut content = div().flex_1().flex().flex_col().gap_1();
134
135        if let Some(title) = self.title {
136            content = content.child(
137                div()
138                    .text_sm()
139                    .font_weight(FontWeight::SEMIBOLD)
140                    .text_color(rgb(0xffffff))
141                    .child(title),
142            );
143        }
144
145        content = content.child(
146            div()
147                .text_sm()
148                .text_color(rgb(0xcccccc))
149                .child(self.message),
150        );
151
152        toast = toast.child(content);
153
154        // Close button
155        if self.closeable {
156            if let Some(handler) = self.on_close {
157                let handler_ptr: *const dyn Fn(&mut Window, &mut App) = handler.as_ref();
158                toast = toast.child(
159                    div()
160                        .id("toast-close")
161                        .text_sm()
162                        .text_color(rgb(0x888888))
163                        .cursor_pointer()
164                        .hover(|s| s.text_color(rgb(0xffffff)))
165                        .on_mouse_up(MouseButton::Left, move |_event, window, cx| unsafe {
166                            (*handler_ptr)(window, cx);
167                        })
168                        .child("×"),
169                );
170                // Keep handler alive - it will be dropped with the toast
171                std::mem::forget(handler);
172            }
173        }
174
175        toast
176    }
177}
178
179impl IntoElement for Toast {
180    type Element = Stateful<Div>;
181
182    fn into_element(self) -> Self::Element {
183        self.build()
184    }
185}
186
187/// A container for positioning toasts on screen
188pub struct ToastContainer {
189    position: ToastPosition,
190    toasts: Vec<Toast>,
191}
192
193impl ToastContainer {
194    /// Create a new toast container
195    pub fn new(position: ToastPosition) -> Self {
196        Self {
197            position,
198            toasts: Vec::new(),
199        }
200    }
201
202    /// Add a toast to the container
203    pub fn toast(mut self, toast: Toast) -> Self {
204        self.toasts.push(toast);
205        self
206    }
207
208    /// Add multiple toasts
209    pub fn toasts(mut self, toasts: impl IntoIterator<Item = Toast>) -> Self {
210        self.toasts.extend(toasts);
211        self
212    }
213
214    /// Build the container into an element
215    pub fn build(self) -> Div {
216        let mut container = div().absolute().flex().flex_col().gap_2().p_4();
217
218        // Position the container
219        match self.position {
220            ToastPosition::TopRight => {
221                container = container.top_0().right_0();
222            }
223            ToastPosition::TopLeft => {
224                container = container.top_0().left_0();
225            }
226            ToastPosition::BottomRight => {
227                container = container.bottom_0().right_0();
228            }
229            ToastPosition::BottomLeft => {
230                container = container.bottom_0().left_0();
231            }
232            ToastPosition::TopCenter => {
233                container = container.top_0().left_0().right_0().items_center();
234            }
235            ToastPosition::BottomCenter => {
236                container = container.bottom_0().left_0().right_0().items_center();
237            }
238        }
239
240        for toast in self.toasts {
241            container = container.child(toast);
242        }
243
244        container
245    }
246}
247
248impl IntoElement for ToastContainer {
249    type Element = Div;
250
251    fn into_element(self) -> Self::Element {
252        self.build()
253    }
254}