diff --git a/assets/icons/small_close.svg b/assets/icons/small_close.svg
new file mode 100644
index 0000000..77843b6
--- /dev/null
+++ b/assets/icons/small_close.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/icon.rs b/src/components/icon.rs
index 3960839..28a926a 100644
--- a/src/components/icon.rs
+++ b/src/components/icon.rs
@@ -13,6 +13,7 @@ pub enum SvgIcon {
Copy,
Plus,
Qr,
+ SmallClose,
}
pub fn map_icon(icon: SvgIcon) -> Svg<'static, Theme> {
@@ -29,5 +30,6 @@ pub fn map_icon(icon: SvgIcon) -> Svg<'static, Theme> {
SvgIcon::Copy => Svg::from_path("assets/icons/copy.svg"),
SvgIcon::Plus => Svg::from_path("assets/icons/plus.svg"),
SvgIcon::Qr => Svg::from_path("assets/icons/qr.svg"),
+ SvgIcon::SmallClose => Svg::from_path("assets/icons/small_close.svg"),
}
}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 167aa16..b527664 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -39,3 +39,6 @@ pub use rules::*;
mod layout;
pub use layout::*;
+
+mod toast;
+pub use toast::*;
diff --git a/src/components/toast.rs b/src/components/toast.rs
new file mode 100644
index 0000000..0314b92
--- /dev/null
+++ b/src/components/toast.rs
@@ -0,0 +1,540 @@
+// Mostly stolen from https://github.com/iced-rs/iced/blob/master/examples/toast/src/main.rs but I made it pretty
+use std::fmt;
+use std::time::{Duration, Instant};
+
+use iced::advanced::layout::{self, Layout};
+use iced::advanced::overlay;
+use iced::advanced::renderer;
+use iced::advanced::widget::{self, Operation, Tree};
+use iced::advanced::{Clipboard, Shell, Widget};
+use iced::event::{self, Event};
+use iced::widget::button::Status;
+use iced::widget::{button, column, container, horizontal_space, row, text};
+use iced::Border;
+use iced::{mouse, Color, Font};
+use iced::{window, Shadow};
+use iced::{Alignment, Element, Length, Point, Rectangle, Renderer, Size, Theme, Vector};
+
+use super::{darken, lighten, map_icon, SvgIcon};
+
+pub const DEFAULT_TIMEOUT: u64 = 5;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub enum ToastStatus {
+ #[default]
+ Neutral,
+ Good,
+ Bad,
+}
+
+impl ToastStatus {
+ pub const ALL: &'static [Self] = &[Self::Neutral, Self::Good, Self::Bad];
+}
+
+impl fmt::Display for ToastStatus {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ ToastStatus::Neutral => "Neutral",
+ ToastStatus::Good => "Good",
+ ToastStatus::Bad => "Bad",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct Toast {
+ pub title: String,
+ pub body: String,
+ pub status: ToastStatus,
+}
+
+pub struct ToastManager<'a, Message> {
+ content: Element<'a, Message>,
+ toasts: Vec>,
+ timeout_secs: u64,
+ on_close: Box Message + 'a>,
+}
+
+impl<'a, Message> ToastManager<'a, Message>
+where
+ Message: 'a + Clone + 'static,
+{
+ pub fn new(
+ content: impl Into>,
+ toasts: &'a [Toast],
+ on_close: impl Fn(usize) -> Message + 'a,
+ ) -> Self {
+ let toasts = toasts
+ .iter()
+ .enumerate()
+ .map(|(index, toast)| {
+ let close_icon = map_icon(SvgIcon::SmallClose).height(12).width(12);
+
+ let close_button = button(close_icon)
+ .style(|theme: &Theme, status| {
+ let border = Border {
+ color: Color::WHITE,
+ width: 0.,
+ radius: (4.).into(),
+ };
+
+ let background = match status {
+ Status::Hovered => darken(theme.palette().background, 0.1),
+ Status::Pressed => darken(Color::BLACK, 0.1),
+ _ => theme.palette().background,
+ };
+ button::Style {
+ background: Some(background.into()),
+ text_color: Color::WHITE,
+ border,
+ shadow: Shadow::default(),
+ }
+ })
+ .padding(6)
+ .width(Length::Fixed(24.))
+ .height(Length::Fixed(24.));
+
+ container(column![container(column![
+ row![
+ text(toast.title.as_str()).font(Font {
+ family: iced::font::Family::default(),
+ weight: iced::font::Weight::Bold,
+ stretch: iced::font::Stretch::Normal,
+ style: iced::font::Style::Normal,
+ }),
+ horizontal_space(),
+ close_button.on_press((on_close)(index))
+ ]
+ .align_items(Alignment::Center),
+ text(toast.body.as_str())
+ ])
+ .width(Length::Fill)
+ .padding(16)
+ .style(match toast.status {
+ ToastStatus::Neutral => neutral,
+ ToastStatus::Good => good,
+ ToastStatus::Bad => bad,
+ }),])
+ .max_width(256)
+ .into()
+ })
+ .collect();
+
+ Self {
+ content: content.into(),
+ toasts,
+ timeout_secs: DEFAULT_TIMEOUT,
+ on_close: Box::new(on_close),
+ }
+ }
+
+ pub fn timeout(self, seconds: u64) -> Self {
+ Self {
+ timeout_secs: seconds,
+ ..self
+ }
+ }
+}
+
+impl<'a, Message> Widget for ToastManager<'a, Message> {
+ fn size(&self) -> Size {
+ self.content.as_widget().size()
+ }
+
+ fn layout(
+ &self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ self.content
+ .as_widget()
+ .layout(&mut tree.children[0], renderer, limits)
+ }
+
+ fn tag(&self) -> widget::tree::Tag {
+ struct Marker;
+ widget::tree::Tag::of::()
+ }
+
+ fn state(&self) -> widget::tree::State {
+ widget::tree::State::new(Vec::