cosmic/widget/toaster/
mod.rs1use std::collections::VecDeque;
7use std::rc::Rc;
8
9use crate::widget::{Column, container};
10use iced::Task;
11use iced_core::Element;
12use slotmap::{SlotMap, new_key_type};
13use widget::Toaster;
14
15use super::{button, column, icon, row, text};
16
17mod widget;
18
19pub fn toaster<'a, Message: Clone + 'static>(
21 toasts: &'a Toasts<Message>,
22 content: impl Into<Element<'a, Message, crate::Theme, iced::Renderer>>,
23) -> Element<'a, Message, crate::Theme, iced::Renderer> {
24 let theme = crate::theme::active();
25 let cosmic_theme::Spacing {
26 space_xxxs,
27 space_xxs,
28 space_s,
29 space_m,
30 ..
31 } = theme.cosmic().spacing;
32
33 let make_toast = move |(id, toast): (ToastId, &'a Toast<Message>)| {
34 let row = row::with_capacity(2)
35 .push(text(&toast.message))
36 .push(
37 row::with_capacity(2)
38 .push_maybe(toast.action.as_ref().map(|action| {
39 button::text(&action.description).on_press((action.message)(id))
40 }))
41 .push(
42 button::icon(icon::from_name("window-close-symbolic"))
43 .on_press((toasts.on_close)(id)),
44 )
45 .align_y(iced::Alignment::Center)
46 .spacing(space_xxs),
47 )
48 .align_y(iced::Alignment::Center)
49 .spacing(space_s);
50
51 container(row)
52 .padding([space_xxs, space_s, space_xxs, space_m])
53 .class(crate::style::Container::Tooltip)
54 };
55
56 let col = toasts
57 .queue
58 .iter()
59 .filter_map(|id| Some((*id, toasts.toasts.get(*id)?)))
60 .rev()
61 .map(make_toast)
62 .fold(column::with_capacity(toasts.toasts.len()), Column::push)
63 .spacing(space_xxxs);
64
65 Toaster::new(col.into(), content.into(), toasts.toasts.is_empty()).into()
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
70pub enum Duration {
71 #[default]
72 Short,
73 Long,
74 Custom(std::time::Duration),
75}
76
77impl Duration {
78 #[cfg(feature = "tokio")]
79 fn duration(&self) -> std::time::Duration {
80 match self {
81 Duration::Short => std::time::Duration::from_millis(5000),
82 Duration::Long => std::time::Duration::from_millis(15000),
83 Duration::Custom(duration) => *duration,
84 }
85 }
86}
87
88impl From<std::time::Duration> for Duration {
89 fn from(value: std::time::Duration) -> Self {
90 Self::Custom(value)
91 }
92}
93
94#[derive(Clone)]
98pub struct Action<Message> {
99 pub description: String,
100 pub message: Rc<dyn Fn(ToastId) -> Message>,
101}
102
103impl<Message> std::fmt::Debug for Action<Message> {
104 #[cold]
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 f.debug_struct("Action")
107 .field("description", &self.description)
108 .finish()
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct Toast<Message> {
115 message: String,
116 action: Option<Action<Message>>,
117 duration: Duration,
118}
119
120impl<Message> Toast<Message> {
121 pub fn new(message: impl Into<String>) -> Self {
123 Self {
124 message: message.into(),
125 action: None,
126 duration: Duration::default(),
127 }
128 }
129
130 #[must_use]
132 pub fn action(
133 mut self,
134 description: String,
135 message: impl Fn(ToastId) -> Message + 'static,
136 ) -> Self {
137 self.action.replace(Action {
138 description,
139 message: Rc::new(message),
140 });
141 self
142 }
143
144 #[must_use]
146 pub fn duration(mut self, duration: impl Into<Duration>) -> Self {
147 self.duration = duration.into();
148 self
149 }
150}
151
152new_key_type! { pub struct ToastId; }
153
154#[derive(Debug, Clone)]
155pub struct Toasts<Message> {
156 toasts: SlotMap<ToastId, Toast<Message>>,
157 queue: VecDeque<ToastId>,
158 on_close: fn(ToastId) -> Message,
159 limit: usize,
160}
161
162impl<Message: Clone + Send + 'static> Toasts<Message> {
163 pub fn new(on_close: fn(ToastId) -> Message) -> Self {
164 let limit = 5;
165 Self {
166 toasts: SlotMap::with_capacity_and_key(limit),
167 queue: VecDeque::new(),
168 on_close,
169 limit,
170 }
171 }
172
173 pub fn push(&mut self, toast: Toast<Message>) -> Task<Message> {
175 while self.toasts.len() >= self.limit {
176 self.toasts.remove(
177 self.queue
178 .pop_front()
179 .expect("Queue must contain all toast ids"),
180 );
181 }
182
183 #[cfg(feature = "tokio")]
184 let duration = toast.duration.duration();
185
186 let id = self.toasts.insert(toast);
187 self.queue.push_back(id);
188
189 #[cfg(feature = "tokio")]
190 {
191 let on_close = self.on_close;
192 crate::task::future(async move {
193 tokio::time::sleep(duration).await;
194 on_close(id)
195 })
196 }
197 #[cfg(not(feature = "tokio"))]
198 {
199 Task::none()
200 }
201 }
202
203 pub fn remove(&mut self, id: ToastId) {
205 self.toasts.remove(id);
206 if let Some(pos) = self.queue.iter().position(|key| *key == id) {
207 self.queue.remove(pos);
208 }
209 }
210}