// Copyright 2024 wiiznokes // SPDX-License-Identifier: MPL-2.0 //! A widget that displays toasts. use std::collections::VecDeque; use std::time::Duration; use crate::app::Command; use crate::widget::container; use crate::widget::Column; use iced_core::Element; use widget::Toaster; use crate::ext::CollectionWidget; use super::column; use super::{button, row, text}; mod widget; /// Create a new Toaster widget. pub fn toaster<'a, Message>( toasts: &'a Toasts, content: impl Into>, ) -> Element<'a, Message, crate::Theme, iced::Renderer> where Message: From + Clone + 'static, { let make_toast = |toast: &'a Toast| { let row = row() .push(text(&toast.message)) .push_maybe(toast.action.as_ref().map(|action| { button::standard(&action.description).on_press(action.message.clone()) })) .push(button::standard("close").on_press(ToastMessage(toast.id).into())) .align_items(iced::Alignment::Center); container(row) .padding(crate::theme::active().cosmic().space_xs()) .style(crate::style::Container::Card) }; let col = toasts .toasts .iter() .rev() .map(make_toast) .fold(column::with_capacity(toasts.toasts.len()), Column::push) .spacing(crate::theme::active().cosmic().space_xxxs()); Toaster::new(col.into(), content.into(), toasts.toasts.is_empty()).into() } /// Duration for the [`Toast`] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum ToastDuration { #[default] Short, Long, Custom(Duration), } impl ToastDuration { fn duration(&self) -> Duration { match self { ToastDuration::Short => Duration::from_millis(2000), ToastDuration::Long => Duration::from_millis(3500), ToastDuration::Custom(duration) => *duration, } } } impl From for ToastDuration { fn from(value: Duration) -> Self { Self::Custom(value) } } /// Action that can be triggered by the user. /// /// Example: `undo` #[derive(Debug, Clone)] pub struct ToastAction { pub description: String, pub message: Message, } /// Represent the data used to display a [`Toast`] #[derive(Debug, Clone)] pub struct Toast { message: String, action: Option>, duration: ToastDuration, id: u32, } impl Toast { /// Construct a new [`Toast`] with the provided message. pub fn new(message: impl Into) -> Self { Self { message: message.into(), action: None, duration: ToastDuration::default(), id: 0, } } /// Set the [`ToastAction`] of this [`Toast`] #[must_use] pub fn action(mut self, action: ToastAction) -> Self { self.action.replace(action); self } /// Set the [`ToastDuration`] of this [`Toast`] #[must_use] pub fn duration(mut self, duration: impl Into) -> Self { self.duration = duration.into(); self } } #[derive(Debug, Clone)] pub struct Toasts { id_count: u32, toasts: VecDeque>, limit: usize, } // need custom impl to not require Message: Clone impl Default for Toasts { fn default() -> Self { Self { id_count: 0, toasts: VecDeque::new(), limit: 5, } } } #[derive(Debug, Clone)] pub struct ToastMessage(u32); impl Toasts { /// Add a new [`Toast`] pub fn push(&mut self, mut toast: Toast) -> Command where Message: From, { while self.toasts.len() >= self.limit { self.toasts.pop_front(); } toast.id = self.id_count; self.id_count += 1; let message = ToastMessage(toast.id); let duration = toast.duration.duration(); self.toasts.push_back(toast); crate::command::future(async move { #[cfg(feature = "tokio")] tokio::time::sleep(duration).await; crate::app::Message::App(Message::from(message)) }) } /// Handle the [`ToastMessage`] pub fn handle_message(&mut self, message: &ToastMessage) { self.toasts .iter() .position(|e| e.id == message.0) .map(|index| self.toasts.remove(index)); } }