Add `Toast` component

Marshall Bowers created

Change summary

crates/storybook2/src/stories/components.rs       |  1 
crates/storybook2/src/stories/components/toast.rs | 31 +++++++
crates/storybook2/src/story_selector.rs           |  2 
crates/ui2/src/components.rs                      |  2 
crates/ui2/src/components/toast.rs                | 66 +++++++++++++++++
crates/ui2/src/prelude.rs                         | 20 +++++
6 files changed, 122 insertions(+)

Detailed changes

crates/storybook2/src/stories/components/toast.rs 🔗

@@ -0,0 +1,31 @@
+use std::marker::PhantomData;
+use std::sync::Arc;
+
+use ui::prelude::*;
+use ui::{Label, Toast, ToastOrigin};
+
+use crate::story::Story;
+
+#[derive(Element)]
+pub struct ToastStory<S: 'static + Send + Sync + Clone> {
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> ToastStory<S> {
+    pub fn new() -> Self {
+        Self {
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        Story::container(cx)
+            .child(Story::title_for::<_, Toast<S>>(cx))
+            .child(Story::label(cx, "Default"))
+            .child(Toast::new(
+                ToastOrigin::Bottom,
+                |_, _| vec![Label::new("label").into_any()],
+                Box::new(()),
+            ))
+    }
+}

crates/storybook2/src/story_selector.rs 🔗

@@ -52,6 +52,7 @@ pub enum ComponentStory {
     TabBar,
     Terminal,
     TitleBar,
+    Toast,
     Toolbar,
     TrafficLights,
     Workspace,
@@ -82,6 +83,7 @@ impl ComponentStory {
             Self::TabBar => components::tab_bar::TabBarStory::new().into_any(),
             Self::Terminal => components::terminal::TerminalStory::new().into_any(),
             Self::TitleBar => components::title_bar::TitleBarStory::new().into_any(),
+            Self::Toast => components::toast::ToastStory::new().into_any(),
             Self::Toolbar => components::toolbar::ToolbarStory::new().into_any(),
             Self::TrafficLights => components::traffic_lights::TrafficLightsStory::new().into_any(),
             Self::Workspace => components::workspace::WorkspaceStory::new().into_any(),

crates/ui2/src/components.rs 🔗

@@ -20,6 +20,7 @@ mod tab;
 mod tab_bar;
 mod terminal;
 mod title_bar;
+mod toast;
 mod toolbar;
 mod traffic_lights;
 mod workspace;
@@ -46,6 +47,7 @@ pub use tab::*;
 pub use tab_bar::*;
 pub use terminal::*;
 pub use title_bar::*;
+pub use toast::*;
 pub use toolbar::*;
 pub use traffic_lights::*;
 pub use workspace::*;

crates/ui2/src/components/toast.rs 🔗

@@ -0,0 +1,66 @@
+use crate::prelude::*;
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastOrigin {
+    #[default]
+    Bottom,
+    BottomRight,
+}
+
+#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ToastVariant {
+    #[default]
+    Toast,
+    Status,
+}
+
+/// A toast is a small, temporary window that appears to show a message to the user
+/// or indicate a required action.
+///
+/// Toasts should not persist on the screen for more than a few seconds unless
+/// they are actively showing the a process in progress.
+///
+/// Only one toast may be visible at a time.
+#[derive(Element)]
+pub struct Toast<S: 'static + Send + Sync> {
+    origin: ToastOrigin,
+    children: HackyChildren<S>,
+    payload: HackyChildrenPayload,
+}
+
+impl<S: 'static + Send + Sync> Toast<S> {
+    pub fn new(
+        origin: ToastOrigin,
+        children: HackyChildren<S>,
+        payload: HackyChildrenPayload,
+    ) -> Self {
+        Self {
+            origin,
+            children,
+            payload,
+        }
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<S>) -> impl Element<State = S> {
+        let color = ThemeColor::new(cx);
+
+        let mut div = div();
+
+        if self.origin == ToastOrigin::Bottom {
+            div = div.right_1_2();
+        } else {
+            div = div.right_4();
+        }
+
+        div.absolute()
+            .bottom_4()
+            .flex()
+            .py_2()
+            .px_1p5()
+            .min_w_40()
+            .rounded_md()
+            .fill(color.elevated_surface)
+            .max_w_64()
+            .children_any((self.children)(cx, self.payload.as_ref()))
+    }
+}

crates/ui2/src/prelude.rs 🔗

@@ -30,6 +30,26 @@ impl SystemColor {
     }
 }
 
+#[derive(Clone, Copy)]
+pub struct ThemeColor {
+    pub border: Hsla,
+    pub border_variant: Hsla,
+    /// The background color of an elevated surface, like a modal, tooltip or toast.
+    pub elevated_surface: Hsla,
+}
+
+impl ThemeColor {
+    pub fn new(cx: &WindowContext) -> Self {
+        let theme = theme(cx);
+
+        Self {
+            border: theme.lowest.base.default.border,
+            border_variant: theme.lowest.variant.default.border,
+            elevated_surface: theme.middle.base.default.background,
+        }
+    }
+}
+
 #[derive(Default, PartialEq, EnumIter, Clone, Copy)]
 pub enum HighlightColor {
     #[default]