1#![deny(clippy::all)]
67
68mod toast;
69pub use toast::*;
70
71use std::collections::HashMap;
72use std::sync::Arc;
73use std::time::Duration;
74
75use egui::epaint::RectShape;
76use egui::{Align2, Area, Context, Direction, Frame, Id, Order, Pos2, Response, CornerRadius, Shape, Stroke, Ui, StrokeKind};
77
78pub type ToastContents = dyn Fn(&mut Ui, &mut Toast) -> Response + Send + Sync;
79
80pub struct Toasts {
81 id: Id,
82 align: Align2,
83 offset: Pos2,
84 direction: Direction,
85 custom_toast_contents: HashMap<ToastKind, Arc<ToastContents>>,
86 added_toasts: Vec<Toast>,
89}
90
91impl Default for Toasts {
92 fn default() -> Self {
93 Self {
94 id: Id::new("__toasts"),
95 align: Align2::LEFT_TOP,
96 offset: Pos2::new(10.0, 10.0),
97 direction: Direction::TopDown,
98 custom_toast_contents: HashMap::new(),
99 added_toasts: Vec::new(),
100 }
101 }
102}
103
104impl Toasts {
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn with_id(id: Id) -> Self {
114 Self {
115 id,
116 ..Default::default()
117 }
118 }
119
120 pub fn position(mut self, position: impl Into<Pos2>) -> Self {
125 self.offset = position.into();
126 self
127 }
128
129 pub fn anchor(mut self, anchor: Align2, offset: impl Into<Pos2>) -> Self {
134 self.align = anchor;
135 self.offset = offset.into();
136 self
137 }
138
139 pub fn direction(mut self, direction: impl Into<Direction>) -> Self {
141 self.direction = direction.into();
142 self
143 }
144
145 pub fn custom_contents(
147 mut self,
148 kind: impl Into<ToastKind>,
149 add_contents: impl Fn(&mut Ui, &mut Toast) -> Response + Send + Sync + 'static,
150 ) -> Self {
151 self.custom_toast_contents
152 .insert(kind.into(), Arc::new(add_contents));
153 self
154 }
155
156 pub fn add(&mut self, toast: Toast) -> &mut Self {
158 self.added_toasts.push(toast);
159 self
160 }
161
162 pub fn show(&mut self, ctx: &Context) {
164 let Self {
165 id,
166 align,
167 mut offset,
168 direction,
169 ..
170 } = *self;
171
172 let dt = ctx.input(|i| i.unstable_dt) as f64;
173
174 let mut toasts: Vec<Toast> = ctx.data_mut(|d| d.get_temp(id).unwrap_or_default());
175 toasts.extend(std::mem::take(&mut self.added_toasts));
176 toasts.retain(|toast| toast.options.ttl_sec > 0.0);
177
178 for (i, toast) in toasts.iter_mut().enumerate() {
179 let response = Area::new(id.with("toast").with(i))
180 .anchor(align, offset.to_vec2())
181 .order(Order::Foreground)
182 .interactable(true)
183 .show(ctx, |ui| {
184 if let Some(add_contents) = self.custom_toast_contents.get_mut(&toast.kind) {
185 add_contents(ui, toast)
186 } else {
187 default_toast_contents(ui, toast)
188 };
189 })
190 .response;
191
192 if !response.hovered() {
193 toast.options.ttl_sec -= dt;
194 if toast.options.ttl_sec.is_finite() {
195 ctx.request_repaint_after(Duration::from_secs_f64(
196 toast.options.ttl_sec.max(0.0),
197 ));
198 }
199 }
200
201 if toast.options.show_progress {
202 ctx.request_repaint();
203 }
204
205 match direction {
206 Direction::LeftToRight => {
207 offset.x += response.rect.width() + 10.0;
208 }
209 Direction::RightToLeft => {
210 offset.x -= response.rect.width() + 10.0;
211 }
212 Direction::TopDown => {
213 offset.y += response.rect.height() + 10.0;
214 }
215 Direction::BottomUp => {
216 offset.y -= response.rect.height() + 10.0;
217 }
218 }
219 }
220
221 ctx.data_mut(|d| d.insert_temp(id, toasts));
222 }
223}
224
225fn default_toast_contents(ui: &mut Ui, toast: &mut Toast) -> Response {
226 let inner_margin = 10.0;
227 let frame = Frame::window(ui.style());
228 let response = frame
229 .inner_margin(inner_margin)
230 .stroke(Stroke::NONE)
231 .show(ui, |ui| {
232 ui.horizontal(|ui| {
233 let a = |ui: &mut Ui, toast: &mut Toast| {
234 if toast.options.show_icon {
235 ui.label(match toast.kind {
236 ToastKind::Warning => toast.style.warning_icon.clone(),
237 ToastKind::Error => toast.style.error_icon.clone(),
238 ToastKind::Success => toast.style.success_icon.clone(),
239 _ => toast.style.info_icon.clone(),
240 });
241 }
242 };
243 let b = |ui: &mut Ui, toast: &mut Toast| ui.label(toast.text.clone());
244 let c = |ui: &mut Ui, toast: &mut Toast| {
245 if ui.button(toast.style.close_button_text.clone()).clicked() {
246 toast.close();
247 }
248 };
249
250 if ui.layout().prefer_right_to_left() {
253 c(ui, toast);
254 b(ui, toast);
255 a(ui, toast);
256 } else {
257 a(ui, toast);
258 b(ui, toast);
259 c(ui, toast);
260 }
261 })
262 })
263 .response;
264
265 if toast.options.show_progress {
266 progress_bar(ui, &response, toast);
267 }
268
269 let frame_shape = Shape::Rect(RectShape::stroke(
271 response.rect,
272 frame.corner_radius,
273 ui.visuals().window_stroke,
274 StrokeKind::Inside
275 ));
276 ui.painter().add(frame_shape);
277
278 response
279}
280
281fn progress_bar(ui: &mut Ui, response: &Response, toast: &Toast) {
282 let rounding = CornerRadius {
283 nw: 0,
284 ne: 0,
285 ..ui.visuals().window_corner_radius
286 };
287 let mut clip_rect = response.rect;
288 clip_rect.set_top(clip_rect.bottom() - 2.0);
289 clip_rect.set_right(clip_rect.left() + clip_rect.width() * toast.options.progress() as f32);
290
291 ui.painter().with_clip_rect(clip_rect).rect_filled(
292 response.rect,
293 rounding,
294 ui.visuals().text_color(),
295 );
296}
297
298pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui, &Context)) {
299 let ctx = Context::default();
300 let _ = ctx.run(Default::default(), |ctx| {
301 egui::CentralPanel::default().show(ctx, |ui| {
302 add_contents(ui, ctx);
303 });
304 });
305}
306
307pub fn __run_test_ui_with_toasts(mut add_contents: impl FnMut(&mut Ui, &mut Toasts)) {
308 let ctx = Context::default();
309 let _ = ctx.run(Default::default(), |ctx| {
310 egui::CentralPanel::default().show(ctx, |ui| {
311 let mut toasts = Toasts::new();
312 add_contents(ui, &mut toasts);
313 });
314 });
315}