1use crate::theme::get_global_color;
2use crate::material_symbol::material_symbol_text;
3use egui::{
4 ecolor::Color32, pos2, Area, FontId, Id, Order, Rect, Response, Sense, Stroke, Ui, Vec2, Widget,
5};
6use std::time::Duration;
7
8#[derive(Clone, Copy, PartialEq, Debug)]
10pub enum NotificationAlign {
11 Left,
13 Center,
15 Right,
17}
18
19#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
43pub struct MaterialNotification {
44 title: Option<String>,
46 subtitle: Option<String>,
48 text: Option<String>,
50 icon: Option<String>,
52 title_right_text: Option<String>,
54 closeable: bool,
56 opened: bool,
58 auto_dismiss: Option<Duration>,
60 bg_color: Option<Color32>,
62 width: Option<f32>,
64 align: NotificationAlign,
66}
67
68impl MaterialNotification {
69 pub fn new() -> Self {
71 Self {
72 title: None,
73 subtitle: None,
74 text: None,
75 icon: None,
76 title_right_text: None,
77 closeable: false,
78 opened: true,
79 auto_dismiss: None,
80 bg_color: None,
81 width: None,
82 align: NotificationAlign::Center,
83 }
84 }
85
86 pub fn title(mut self, title: impl Into<String>) -> Self {
88 self.title = Some(title.into());
89 self
90 }
91
92 pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
94 self.subtitle = Some(subtitle.into());
95 self
96 }
97
98 pub fn text(mut self, text: impl Into<String>) -> Self {
100 self.text = Some(text.into());
101 self
102 }
103
104 pub fn icon(mut self, icon: impl Into<String>) -> Self {
106 self.icon = Some(icon.into());
107 self
108 }
109
110 pub fn title_right_text(mut self, text: impl Into<String>) -> Self {
112 self.title_right_text = Some(text.into());
113 self
114 }
115
116 pub fn closeable(mut self, closeable: bool) -> Self {
118 self.closeable = closeable;
119 self
120 }
121
122 pub fn opened(mut self, opened: bool) -> Self {
124 self.opened = opened;
125 self
126 }
127
128 pub fn auto_dismiss(mut self, duration: Duration) -> Self {
130 self.auto_dismiss = Some(duration);
131 self
132 }
133
134 pub fn bg_color(mut self, color: Color32) -> Self {
136 self.bg_color = Some(color);
137 self
138 }
139
140 pub fn width(mut self, width: f32) -> Self {
142 self.width = Some(width);
143 self
144 }
145
146 pub fn align(mut self, align: NotificationAlign) -> Self {
148 self.align = align;
149 self
150 }
151
152 pub fn with_offset(self, offset: f32) -> MaterialNotificationWithOffset {
155 MaterialNotificationWithOffset {
156 notification: self,
157 vertical_offset: offset,
158 }
159 }
160}
161
162pub struct MaterialNotificationWithOffset {
164 notification: MaterialNotification,
165 vertical_offset: f32,
166}
167
168impl Default for MaterialNotification {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174impl Widget for MaterialNotification {
175 fn ui(self, ui: &mut Ui) -> Response {
176 self.ui_with_offset(ui, 0.0)
177 }
178}
179
180impl MaterialNotification {
181 fn ui_with_offset(self, ui: &mut Ui, vertical_offset: f32) -> Response {
182 if !self.opened {
183 return ui.allocate_response(Vec2::ZERO, Sense::hover());
184 }
185
186 let surface_container_highest = get_global_color("surfaceContainerHighest");
188 let on_surface = get_global_color("onSurface");
189 let on_surface_variant = get_global_color("onSurfaceVariant");
190 let outline = get_global_color("outline");
191
192 let bg_color = self.bg_color.unwrap_or(surface_container_highest);
193
194 let screen_rect = ui.ctx().screen_rect();
196 let max_width: f32 = 400.0;
197 let width = self.width.unwrap_or(max_width.min(screen_rect.width() - 48.0));
198
199 let padding = 12.0;
200 let content_width = width - padding * 2.0;
201
202 let icon_size = 24.0;
204 let icon_margin = 8.0;
205 let has_icon = self.icon.is_some();
206 let text_width = if has_icon {
207 content_width - icon_size - icon_margin
208 } else {
209 content_width
210 };
211
212 let close_button_space = if self.closeable { 40.0 } else { 0.0 };
214 let available_text_width = text_width - close_button_space;
215
216 let title_galley = self.title.as_ref().map(|title_text| {
218 ui.painter().layout(
219 title_text.clone(),
220 FontId::proportional(16.0),
221 on_surface,
222 available_text_width - if self.title_right_text.is_some() { 60.0 } else { 0.0 },
223 )
224 });
225
226 let subtitle_galley = self.subtitle.as_ref().map(|subtitle_text| {
227 ui.painter().layout(
228 subtitle_text.clone(),
229 FontId::proportional(14.0),
230 on_surface_variant,
231 available_text_width,
232 )
233 });
234
235 let text_galley = self.text.as_ref().map(|content_text| {
236 ui.painter().layout(
237 content_text.clone(),
238 FontId::proportional(14.0),
239 on_surface_variant,
240 available_text_width,
241 )
242 });
243
244 let right_text_galley = self.title_right_text.as_ref().map(|right_text| {
245 ui.painter().layout_no_wrap(
246 right_text.clone(),
247 FontId::proportional(12.0),
248 on_surface_variant,
249 )
250 });
251
252 let mut total_height = padding * 2.0;
254 if let Some(ref galley) = title_galley {
255 total_height += galley.size().y + 4.0;
256 }
257 if let Some(ref galley) = subtitle_galley {
258 total_height += galley.size().y + 4.0;
259 }
260 if let Some(ref galley) = text_galley {
261 total_height += galley.size().y;
262 }
263
264 let screen_rect = ui.ctx().screen_rect();
268 let notification_x = match self.align {
269 NotificationAlign::Left => screen_rect.min.x + 16.0,
270 NotificationAlign::Center => screen_rect.min.x + (screen_rect.width() - width) / 2.0,
271 NotificationAlign::Right => screen_rect.max.x - width - 16.0,
272 };
273 let notification_y = screen_rect.min.y + 16.0 + 50.0 + vertical_offset; let notification_pos = pos2(notification_x, notification_y);
275
276 let notification_id = Id::new("notification").with(self.title.as_deref().unwrap_or(""))
278 .with(self.text.as_deref().unwrap_or(""))
279 .with(vertical_offset as i32); let area_response = Area::new(notification_id)
283 .fixed_pos(notification_pos)
284 .order(Order::Foreground) .interactable(true)
286 .show(ui.ctx(), |ui| {
287 let (rect, mut response) = ui.allocate_exact_size(Vec2::new(width, total_height), Sense::click());
289 let notification_rect = rect;
290
291 ui.painter().rect_filled(notification_rect, 12.0, bg_color);
293
294 ui.painter().rect_stroke(
296 notification_rect,
297 12.0,
298 Stroke::new(1.0, outline),
299 egui::epaint::StrokeKind::Outside,
300 );
301
302 let mut current_y = notification_rect.min.y + padding;
304 let left_margin = notification_rect.min.x + padding;
305 let text_start_x = if has_icon {
306 left_margin + icon_size + icon_margin
307 } else {
308 left_margin
309 };
310
311 if let Some(icon_name) = &self.icon {
313 let icon_text = material_symbol_text(icon_name);
314 let icon_galley = ui.painter().layout_no_wrap(
315 icon_text.to_string(),
316 FontId::proportional(icon_size),
317 on_surface,
318 );
319 let icon_pos = pos2(left_margin, current_y);
320 ui.painter().galley(icon_pos, icon_galley, on_surface);
321 }
322
323 if let Some(galley) = title_galley {
325 let title_pos = pos2(text_start_x, current_y);
326 ui.painter().galley(title_pos, galley.clone(), on_surface);
327
328 if let Some(right_galley) = right_text_galley {
330 let right_pos = pos2(
331 notification_rect.max.x - padding - close_button_space - right_galley.size().x,
332 current_y,
333 );
334 ui.painter().galley(right_pos, right_galley, on_surface_variant);
335 }
336
337 current_y += galley.size().y + 4.0;
338 }
339
340 if let Some(galley) = subtitle_galley {
342 let subtitle_pos = pos2(text_start_x, current_y);
343 ui.painter().galley(subtitle_pos, galley.clone(), on_surface_variant);
344 current_y += galley.size().y + 4.0;
345 }
346
347 if let Some(galley) = text_galley {
349 let text_pos = pos2(text_start_x, current_y);
350 ui.painter().galley(text_pos, galley, on_surface_variant);
351 }
352
353 let mut close_clicked = false;
355 if self.closeable {
356 let close_button_pos = pos2(
357 notification_rect.max.x - padding - 24.0,
358 notification_rect.min.y + padding,
359 );
360 let close_icon = material_symbol_text("close");
361 let close_galley = ui.painter().layout_no_wrap(
362 close_icon.to_string(),
363 FontId::proportional(20.0),
364 on_surface_variant,
365 );
366
367 let close_rect = Rect::from_center_size(
368 pos2(close_button_pos.x + 12.0, close_button_pos.y + 12.0),
369 Vec2::new(24.0, 24.0),
370 );
371
372 let close_response = ui.interact(close_rect, response.id.with("close"), Sense::click());
373
374 if close_response.hovered() {
375 ui.painter().circle_filled(close_rect.center(), 12.0, on_surface_variant.linear_multiply(0.1));
376 }
377
378 ui.painter().galley(close_button_pos, close_galley, on_surface_variant);
379
380 if close_response.clicked() {
381 close_clicked = true;
382 response.mark_changed();
383 }
384 }
385
386 if response.clicked() && !close_clicked {
388 } else if close_clicked {
390 }
392
393 response
394 });
395
396 area_response.inner
397 }
398}
399
400impl Widget for MaterialNotificationWithOffset {
401 fn ui(self, ui: &mut Ui) -> Response {
402 self.notification.ui_with_offset(ui, self.vertical_offset)
403 }
404}
405
406pub fn notification() -> MaterialNotification {
419 MaterialNotification::new()
420}