Skip to main content

gpui_core/elements/
toast.rs

1#![allow(deprecated)] // All public items in this module are deprecated; internal self-references are fine.
2
3use std::time::Duration;
4
5use crate::{
6    AnyElement, Context, IntoElement, ParentElement, Render, SharedString, Styled, Timer,
7    WeakEntity, Window, WindowAppearance, div, hsla, px,
8};
9
10/// Position where toasts appear on screen.
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
12#[deprecated(
13    since = "0.11.0",
14    note = "toast is a component-level concern; use the toast component from fc-ui (https://github.com/freefcw/fc-ui) instead"
15)]
16pub enum ToastPosition {
17    /// Top-right corner of the window.
18    #[default]
19    TopRight,
20    /// Bottom-right corner of the window.
21    BottomRight,
22    /// Top-center of the window.
23    TopCenter,
24}
25
26/// Configuration for a single toast notification.
27#[derive(Clone)]
28#[deprecated(
29    since = "0.11.0",
30    note = "toast is a component-level concern; use the toast component from fc-ui (https://github.com/freefcw/fc-ui) instead"
31)]
32pub struct Toast {
33    title: SharedString,
34    body: Option<SharedString>,
35    duration: Duration,
36    position: ToastPosition,
37}
38
39impl Toast {
40    /// Create a new toast with the given title.
41    pub fn new(title: impl Into<SharedString>) -> Self {
42        Self {
43            title: title.into(),
44            body: None,
45            duration: Duration::from_secs(3),
46            position: ToastPosition::default(),
47        }
48    }
49
50    /// Set the body text of the toast.
51    pub fn body(mut self, body: impl Into<SharedString>) -> Self {
52        self.body = Some(body.into());
53        self
54    }
55
56    /// Set how long the toast should be displayed before auto-dismissing.
57    pub fn duration(mut self, duration: Duration) -> Self {
58        self.duration = duration;
59        self
60    }
61
62    /// Set the screen position where the toast appears.
63    pub fn position(mut self, position: ToastPosition) -> Self {
64        self.position = position;
65        self
66    }
67}
68
69struct ToastEntry {
70    id: u64,
71    toast: Toast,
72}
73
74/// A stack of toast notifications that manages display and auto-dismissal.
75///
76/// Create a `ToastStack` as a GPUI entity and render it as part of your
77/// window's view tree. Use [`ToastStack::push`] to add new toasts.
78#[deprecated(
79    since = "0.11.0",
80    note = "toast is a component-level concern; use the toast component from fc-ui (https://github.com/freefcw/fc-ui) instead"
81)]
82pub struct ToastStack {
83    toasts: Vec<ToastEntry>,
84    next_toast_id: u64,
85    position: ToastPosition,
86}
87
88impl ToastStack {
89    /// Create a new empty toast stack with the default position.
90    pub fn new() -> Self {
91        Self {
92            toasts: Vec::new(),
93            next_toast_id: 0,
94            position: ToastPosition::default(),
95        }
96    }
97
98    /// Set the default position for toasts in this stack.
99    pub fn with_position(mut self, position: ToastPosition) -> Self {
100        self.position = position;
101        self
102    }
103
104    /// Push a new toast onto the stack and schedule its auto-dismissal.
105    pub fn push(&mut self, toast: Toast, window: &Window, cx: &mut Context<Self>) {
106        let duration = toast.duration;
107        let id = self.next_toast_id;
108        self.next_toast_id += 1;
109        self.toasts.push(ToastEntry { id, toast });
110        cx.notify();
111
112        cx.spawn_in(window, async move |this: WeakEntity<Self>, cx| {
113            Timer::after(duration).await;
114            this.update(cx, |stack, cx| {
115                let len = stack.toasts.len();
116                stack.toasts.retain(|entry| entry.id != id);
117                if stack.toasts.len() != len {
118                    cx.notify();
119                }
120            })
121            .ok();
122        })
123        .detach();
124    }
125
126    /// Remove all toasts from the stack.
127    pub fn clear(&mut self, cx: &mut Context<Self>) {
128        self.toasts.clear();
129        cx.notify();
130    }
131
132    fn is_dark_appearance(window: &Window) -> bool {
133        matches!(
134            window.appearance(),
135            WindowAppearance::Dark | WindowAppearance::VibrantDark
136        )
137    }
138}
139
140impl Render for ToastStack {
141    fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
142        let is_dark = Self::is_dark_appearance(window);
143        let position = self.position;
144
145        let mut container = div().flex().flex_col().gap_2().p_4().max_w(px(360.0));
146
147        match position {
148            ToastPosition::TopRight => {
149                container = container.absolute().top_0().right_0();
150            }
151            ToastPosition::BottomRight => {
152                container = container.absolute().bottom_0().right_0();
153            }
154            ToastPosition::TopCenter => {
155                container = container.absolute().top_0().left_auto().right_auto();
156            }
157        }
158
159        let children: Vec<AnyElement> = self
160            .toasts
161            .iter()
162            .map(|entry| render_toast_item(&entry.toast, is_dark))
163            .collect();
164
165        for child in children {
166            container = container.child(child);
167        }
168
169        container
170    }
171}
172
173fn render_toast_item(toast: &Toast, is_dark: bool) -> AnyElement {
174    let bg_color = if is_dark {
175        hsla(0.0, 0.0, 0.1, 0.92)
176    } else {
177        hsla(0.0, 0.0, 0.0, 0.85)
178    };
179
180    let text_color = if is_dark {
181        hsla(0.0, 0.0, 0.95, 1.0)
182    } else {
183        hsla(0.0, 0.0, 1.0, 1.0)
184    };
185
186    let secondary_text_color = if is_dark {
187        hsla(0.0, 0.0, 0.7, 1.0)
188    } else {
189        hsla(0.0, 0.0, 0.85, 1.0)
190    };
191
192    let title = toast.title.clone();
193    let body = toast.body.clone();
194
195    let mut toast_div = div()
196        .flex()
197        .flex_col()
198        .gap_1()
199        .py(px(12.0))
200        .px(px(16.0))
201        .rounded(px(8.0))
202        .bg(bg_color)
203        .shadow_lg()
204        .max_w(px(320.0))
205        .min_w(px(200.0))
206        .text_color(text_color)
207        .text_sm()
208        .child(div().font_weight(crate::FontWeight::SEMIBOLD).child(title));
209
210    if let Some(body_text) = body {
211        toast_div = toast_div.child(
212            div()
213                .text_xs()
214                .text_color(secondary_text_color)
215                .child(body_text),
216        );
217    }
218
219    toast_div.into_any_element()
220}