// Copyright 2024 wiiznokes // SPDX-License-Identifier: MPL-2.0 //! A widget that displays toasts. use std::collections::VecDeque; use crate::widget::container; use crate::widget::Column; use crate::Command; use iced_core::Element; use widget::Toaster; use crate::ext::CollectionWidget; use super::column; use super::{button, icon, row, text}; mod widget; /// Create a new Toaster widget. pub fn toaster<'a, Message: Clone + 'static>( toasts: &'a Toasts, content: impl Into>, ) -> Element<'a, Message, crate::Theme, iced::Renderer> { let theme = crate::theme::active(); let cosmic_theme::Spacing { space_xxxs, space_xxs, space_s, space_m, .. } = theme.cosmic().spacing; let make_toast = move |(id, toast): (usize, &'a Toast)| { let row = row() .push(text(&toast.message)) .push( row() .push_maybe(toast.action.as_ref().map(|action| { button::text(&action.description).on_press((action.message)(id)) })) .push( button::icon(icon::from_name("window-close-symbolic")) .on_press((toasts.on_close)(id)), ) .align_items(iced::Alignment::Center) .spacing(space_xxs), ) .align_items(iced::Alignment::Center) .spacing(space_s); container(row) .padding([space_xxs, space_s, space_xxs, space_m]) .style(crate::style::Container::Tooltip) }; let col = toasts .toasts .iter() .enumerate() .rev() .map(make_toast) .fold(column::with_capacity(toasts.toasts.len()), Column::push) .spacing(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 Duration { #[default] Short, Long, Custom(std::time::Duration), } impl Duration { fn duration(&self) -> std::time::Duration { match self { Duration::Short => std::time::Duration::from_millis(5000), Duration::Long => std::time::Duration::from_millis(15000), Duration::Custom(duration) => *duration, } } } impl From for Duration { fn from(value: std::time::Duration) -> Self { Self::Custom(value) } } /// Action that can be triggered by the user. /// /// Example: `undo` #[derive(Debug, Clone)] pub struct Action { pub description: String, pub message: fn(usize) -> Message, } /// Represent the data used to display a [`Toast`] #[derive(Debug, Clone)] pub struct Toast { message: String, action: Option>, duration: Duration, } impl Toast { /// Construct a new [`Toast`] with the provided message. pub fn new(message: impl Into) -> Self { Self { message: message.into(), action: None, duration: Duration::default(), } } /// Set the [`Action`] of this [`Toast`] #[must_use] pub fn action(mut self, action: Action) -> Self { self.action.replace(action); self } /// Set the [`Duration`] 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 { toasts: VecDeque>, on_close: fn(usize) -> Message, limit: usize, } impl Toasts { pub fn new(on_close: fn(usize) -> Message) -> Self { Self { toasts: VecDeque::new(), on_close, limit: 5, } } /// Add a new [`Toast`] pub fn push(&mut self, toast: Toast) -> Command { while self.toasts.len() >= self.limit { self.toasts.pop_front(); } let duration = toast.duration.duration(); let id = self.toasts.len(); self.toasts.push_back(toast); let on_close = self.on_close; crate::command::future(async move { #[cfg(feature = "tokio")] tokio::time::sleep(duration).await; on_close(id) }) } /// Remove a [`Toast`] pub fn remove(&mut self, id: usize) { self.toasts.remove(id); } }