patternfly_yew/components/toast/
mod.rs1use crate::prelude::{Action, Alert, AlertGroup, AlertType};
3use chrono::{DateTime, Utc};
4use core::cmp::Reverse;
5use gloo_timers::callback::Timeout;
6use std::fmt::Display;
7use std::{collections::BinaryHeap, time::Duration};
8use yew::{prelude::*, virtual_dom::VChild};
9
10#[derive(Clone, Debug, Default)]
45pub struct Toast {
46 pub title: String,
47 pub r#type: AlertType,
48 pub timeout: Option<Duration>,
52 pub body: Html,
53 pub actions: Vec<Action>,
54}
55
56impl From<&str> for Toast {
57 fn from(value: &str) -> Self {
58 Self::from(value.to_string())
59 }
60}
61
62impl From<String> for Toast {
63 fn from(value: String) -> Self {
64 Self {
65 title: value,
66 timeout: None,
67 body: Default::default(),
68 r#type: Default::default(),
69 actions: Vec::new(),
70 }
71 }
72}
73
74impl From<&String> for Toast {
75 fn from(value: &String) -> Self {
76 Self::from(value.clone())
77 }
78}
79
80pub trait ToToast {
82 fn to_toast(&self) -> Toast;
83}
84
85impl<T> ToToast for T
86where
87 T: Display,
88{
89 fn to_toast(&self) -> Toast {
90 Toast::from(self.to_string())
91 }
92}
93
94#[doc(hidden)]
95#[derive(Debug)]
96pub enum ToasterRequest {
97 Toast(Toast),
98}
99
100#[doc(hidden)]
101pub enum ToastAction {
102 ShowToast(Toast),
103}
104
105#[derive(Clone, PartialEq)]
107pub struct Toaster {
108 callback: Callback<ToastAction>,
109}
110
111impl Toaster {
112 pub fn toast(&self, toast: impl Into<Toast>) {
114 self.callback.emit(ToastAction::ShowToast(toast.into()))
115 }
116}
117
118#[derive(Clone, PartialEq, Properties)]
119pub struct Props {
120 pub children: Html,
121}
122
123pub struct ToastEntry {
124 id: usize,
125 alert: VChild<Alert>,
126 timeout: Option<DateTime<Utc>>,
127}
128
129pub struct ToastViewer {
134 context: Toaster,
135 alerts: Vec<ToastEntry>,
136 counter: usize,
137
138 task: Option<Timeout>,
139 timeouts: BinaryHeap<Reverse<DateTime<Utc>>>,
140}
141
142pub enum ToastViewerMsg {
143 Perform(ToastAction),
144 Cleanup,
145 Close(usize),
146}
147
148impl Component for ToastViewer {
149 type Message = ToastViewerMsg;
150 type Properties = Props;
151
152 fn create(ctx: &Context<Self>) -> Self {
153 let context = Toaster {
154 callback: ctx.link().callback(ToastViewerMsg::Perform),
155 };
156 Self {
157 context,
158 alerts: Vec::new(),
159 counter: 0,
160 task: None,
161 timeouts: BinaryHeap::new(),
162 }
163 }
164
165 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
166 match msg {
167 ToastViewerMsg::Perform(action) => self.perform(ctx, action),
168 ToastViewerMsg::Cleanup => self.cleanup(ctx),
169 ToastViewerMsg::Close(id) => self.remove_toast(id),
170 }
171 }
172
173 fn view(&self, ctx: &Context<Self>) -> Html {
174 let context = self.context.clone();
175
176 html! {
177 <ContextProvider<Toaster> {context}>
178 <AlertGroup toast=true>
179 { for self.alerts.iter().map(|entry|entry.alert.clone()) }
180 </AlertGroup>
181 { ctx.props().children.clone() }
182 </ContextProvider<Toaster>>
183 }
184 }
185}
186
187impl ToastViewer {
188 fn now() -> DateTime<Utc> {
189 Utc::now()
190 }
191
192 fn perform(&mut self, ctx: &Context<Self>, action: ToastAction) -> bool {
193 match action {
194 ToastAction::ShowToast(toast) => self.add_toast(ctx, toast),
195 }
196 true
197 }
198
199 fn add_toast(&mut self, ctx: &Context<Self>, toast: Toast) {
200 let now = Self::now();
201 let timeout = toast
202 .timeout
203 .and_then(|timeout| chrono::Duration::from_std(timeout).ok())
204 .map(|timeout| now + timeout);
205
206 let id = self.counter;
207 self.counter += 1;
208
209 let onclose = match toast.timeout {
210 None => Some(ctx.link().callback(move |_| ToastViewerMsg::Close(id))),
211 Some(_) => None,
212 };
213
214 self.alerts.push(ToastEntry {
215 id,
216 alert: html_nested! {
217 <Alert r#type={toast.r#type} title={toast.title} onclose={onclose} actions={toast.actions}>
218 { toast.body }
219 </Alert>
220 },
221 timeout,
222 });
223
224 if let Some(timeout) = timeout {
225 self.schedule_cleanup(ctx, timeout);
226 }
227 }
228
229 fn schedule_cleanup(&mut self, ctx: &Context<Self>, timeout: DateTime<Utc>) {
230 log::debug!("Schedule cleanup: {:?}", timeout);
231
232 self.timeouts.push(Reverse(timeout));
233 self.trigger_next_cleanup(ctx);
234 }
235
236 fn trigger_next_cleanup(&mut self, ctx: &Context<Self>) {
237 if self.task.is_some() {
238 log::debug!("Already have a task");
239 return;
240 }
241
242 while let Some(next) = self.timeouts.pop() {
245 let timeout = next.0;
246 log::debug!("Next timeout: {:?}", timeout);
247 let duration = timeout - Self::now();
248 let duration = duration.to_std();
249 log::debug!("Duration: {:?}", duration);
250 if let Ok(duration) = duration {
251 let link = ctx.link().clone();
252 self.task = Some(Timeout::new(duration.as_millis() as u32, move || {
253 link.send_message(ToastViewerMsg::Cleanup);
254 }));
255 log::debug!("Scheduled cleanup: {:?}", duration);
256 break;
257 }
258 }
259 }
260
261 fn remove_toast(&mut self, id: usize) -> bool {
262 self.retain_alert(|entry| entry.id != id)
263 }
264
265 fn cleanup(&mut self, ctx: &Context<Self>) -> bool {
266 let now = Self::now();
267
268 self.task = None;
269 self.trigger_next_cleanup(ctx);
270
271 self.retain_alert(|alert| {
272 if let Some(timeout) = alert.timeout {
273 timeout > now
274 } else {
275 true
276 }
277 })
278 }
279
280 fn retain_alert<F>(&mut self, f: F) -> bool
281 where
282 F: Fn(&ToastEntry) -> bool,
283 {
284 let before = self.alerts.len();
285 self.alerts.retain(f);
286 before != self.alerts.len()
287 }
288}
289
290#[hook]
292pub fn use_toaster() -> Option<Toaster> {
293 use_context()
294}