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
218 r#type={toast.r#type}
219 title={toast.title}
220 onclose={onclose}
221 actions={toast.actions}
222 >
223 { toast.body }
224 </Alert>
225 },
226 timeout,
227 });
228
229 if let Some(timeout) = timeout {
230 self.schedule_cleanup(ctx, timeout);
231 }
232 }
233
234 fn schedule_cleanup(&mut self, ctx: &Context<Self>, timeout: DateTime<Utc>) {
235 log::debug!("Schedule cleanup: {:?}", timeout);
236
237 self.timeouts.push(Reverse(timeout));
238 self.trigger_next_cleanup(ctx);
239 }
240
241 fn trigger_next_cleanup(&mut self, ctx: &Context<Self>) {
242 if self.task.is_some() {
243 log::debug!("Already have a task");
244 return;
245 }
246
247 while let Some(next) = self.timeouts.pop() {
250 let timeout = next.0;
251 log::debug!("Next timeout: {:?}", timeout);
252 let duration = timeout - Self::now();
253 let duration = duration.to_std();
254 log::debug!("Duration: {:?}", duration);
255 if let Ok(duration) = duration {
256 let link = ctx.link().clone();
257 self.task = Some(Timeout::new(duration.as_millis() as u32, move || {
258 link.send_message(ToastViewerMsg::Cleanup);
259 }));
260 log::debug!("Scheduled cleanup: {:?}", duration);
261 break;
262 }
263 }
264 }
265
266 fn remove_toast(&mut self, id: usize) -> bool {
267 self.retain_alert(|entry| entry.id != id)
268 }
269
270 fn cleanup(&mut self, ctx: &Context<Self>) -> bool {
271 let now = Self::now();
272
273 self.task = None;
274 self.trigger_next_cleanup(ctx);
275
276 self.retain_alert(|alert| {
277 if let Some(timeout) = alert.timeout {
278 timeout > now
279 } else {
280 true
281 }
282 })
283 }
284
285 fn retain_alert<F>(&mut self, f: F) -> bool
286 where
287 F: Fn(&ToastEntry) -> bool,
288 {
289 let before = self.alerts.len();
290 self.alerts.retain(f);
291 before != self.alerts.len()
292 }
293}
294
295#[hook]
297pub fn use_toaster() -> Option<Toaster> {
298 use_context()
299}