1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::collections::VecDeque;
4use std::rc::{Rc, Weak};
5
6use repose_core::{Modifier, View, ViewKind, request_frame};
7use web_time::{Duration, Instant};
8
9thread_local! {
13 static SNACKBAR_REGISTRY: RefCell<Vec<Weak<SnackbarControllerInner>>> =
14 const { RefCell::new(Vec::new()) };
15 static AMBIENT_OVERLAY: RefCell<Option<OverlayHandle>> = const { RefCell::new(None) };
16}
17
18pub fn with_ambient_overlay<R>(handle: OverlayHandle, f: impl FnOnce() -> R) -> R {
25 struct Guard;
26 impl Drop for Guard {
27 fn drop(&mut self) {
28 AMBIENT_OVERLAY.with(|slot| *slot.borrow_mut() = None);
29 }
30 }
31 AMBIENT_OVERLAY.with(|slot| *slot.borrow_mut() = Some(handle));
32 let _guard = Guard;
33 f()
34}
35
36pub fn ambient_overlay() -> Option<OverlayHandle> {
38 AMBIENT_OVERLAY.with(|slot| slot.borrow().clone())
39}
40
41const SNACKBAR_EXIT_ANIM_MS: u64 = 200;
43
44#[derive(Clone)]
45pub struct OverlayHandle {
46 inner: Rc<RefCell<OverlayState>>,
47}
48
49pub type BackHandler = Rc<dyn Fn() -> bool>;
50
51struct OverlayState {
52 next_id: u64,
53 entries: Vec<OverlayEntry>,
54 back_handlers: HashMap<u64, BackHandler>,
55}
56
57#[derive(Clone)]
58pub struct OverlayEntry {
59 pub id: u64,
60 pub builder: Rc<dyn Fn() -> View>,
61 pub z_index: f32,
62 pub pass_through: bool,
63}
64
65impl Default for OverlayHandle {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl OverlayHandle {
72 pub fn new() -> Self {
73 Self {
74 inner: Rc::new(RefCell::new(OverlayState {
75 next_id: 1,
76 entries: Vec::new(),
77 back_handlers: HashMap::new(),
78 })),
79 }
80 }
81
82 pub fn show(&self, view: View) -> u64 {
83 self.show_with(view, 0.0, false)
84 }
85
86 pub fn show_with(&self, view: View, z_index: f32, pass_through: bool) -> u64 {
87 let builder = Rc::new(move || view.clone());
88 self.show_entry(builder, z_index, pass_through)
89 }
90
91 pub fn show_with_back(
92 &self,
93 view: View,
94 z_index: f32,
95 pass_through: bool,
96 back_handler: BackHandler,
97 ) -> u64 {
98 let builder = Rc::new(move || view.clone());
99 self.show_entry_with_back(builder, z_index, pass_through, Some(back_handler))
100 }
101
102 pub fn show_entry(
103 &self,
104 builder: Rc<dyn Fn() -> View>,
105 z_index: f32,
106 pass_through: bool,
107 ) -> u64 {
108 self.show_entry_with_back(builder, z_index, pass_through, None)
109 }
110
111 pub fn show_entry_with_back(
112 &self,
113 builder: Rc<dyn Fn() -> View>,
114 z_index: f32,
115 pass_through: bool,
116 back_handler: Option<BackHandler>,
117 ) -> u64 {
118 let mut inner = self.inner.borrow_mut();
119 let id = inner.next_id;
120 inner.next_id += 1;
121 inner.entries.push(OverlayEntry {
122 id,
123 builder,
124 z_index,
125 pass_through,
126 });
127 if let Some(back_handler) = back_handler {
128 inner.back_handlers.insert(id, back_handler);
129 }
130 request_frame();
131 id
132 }
133
134 pub fn show_guard(
137 &self,
138 builder: Rc<dyn Fn() -> View>,
139 z_index: f32,
140 pass_through: bool,
141 ) -> OverlayGuard {
142 OverlayGuard::show(self, builder, z_index, pass_through)
143 }
144
145 pub fn show_guard_with_back(
146 &self,
147 builder: Rc<dyn Fn() -> View>,
148 z_index: f32,
149 pass_through: bool,
150 back_handler: BackHandler,
151 ) -> OverlayGuard {
152 OverlayGuard::show_with_back(self, builder, z_index, pass_through, back_handler)
153 }
154
155 pub fn handle_back(&self) -> bool {
156 let handlers = {
157 let inner = self.inner.borrow();
158 let mut handlers = inner
159 .entries
160 .iter()
161 .enumerate()
162 .filter_map(|(order, entry)| {
163 inner
164 .back_handlers
165 .get(&entry.id)
166 .map(|handler| (order, entry.z_index, (*handler).clone()))
167 })
168 .collect::<Vec<_>>();
169 handlers.sort_by(|left, right| {
170 right
171 .1
172 .partial_cmp(&left.1)
173 .unwrap_or(std::cmp::Ordering::Equal)
174 .then_with(|| right.0.cmp(&left.0))
175 });
176 handlers
177 .into_iter()
178 .map(|(_, _, handler)| handler)
179 .collect::<Vec<_>>()
180 };
181 handlers.into_iter().any(|handler| handler())
182 }
183
184 pub fn entry_count(&self) -> usize {
188 self.inner.borrow().entries.len()
189 }
190
191 pub fn dismiss(&self, id: u64) -> bool {
192 let mut inner = self.inner.borrow_mut();
193 let before = inner.entries.len();
194 inner.entries.retain(|entry| entry.id != id);
195 let removed = inner.entries.len() != before;
196 inner.back_handlers.remove(&id);
197 if removed {
198 request_frame();
199 }
200 removed
201 }
202
203 pub fn clear(&self) {
204 let mut inner = self.inner.borrow_mut();
205 let had_entries = !inner.entries.is_empty();
206 inner.entries.clear();
207 inner.back_handlers.clear();
208 if had_entries {
209 request_frame();
210 }
211 }
212
213 pub fn host(&self, modifier: Modifier, content: View) -> View {
219 let mut root = View::new(0, ViewKind::OverlayHost).modifier(modifier);
220 root.children.push(content);
221 let mut overlays = self.inner.borrow().entries.clone();
222 overlays.sort_by(|a, b| {
224 a.z_index
225 .partial_cmp(&b.z_index)
226 .unwrap_or(std::cmp::Ordering::Equal)
227 });
228
229 for entry in overlays {
230 let view = (entry.builder)();
231 let mut modifier = view
232 .modifier
233 .clone()
234 .z_index(entry.z_index)
235 .render_z_index(entry.z_index + 1000.0);
236 if entry.pass_through {
237 modifier = modifier.hit_passthrough();
238 }
239 root.children.push(view.modifier(modifier));
240 }
241 root
242 }
243}
244
245#[must_use = "dropping the guard dismisses the overlay entry"]
255pub struct OverlayGuard {
256 handle: OverlayHandle,
257 id: Option<u64>,
258}
259
260impl OverlayGuard {
261 pub fn show(
263 handle: &OverlayHandle,
264 builder: Rc<dyn Fn() -> View>,
265 z_index: f32,
266 pass_through: bool,
267 ) -> Self {
268 let id = handle.show_entry(builder, z_index, pass_through);
269 Self {
270 handle: handle.clone(),
271 id: Some(id),
272 }
273 }
274
275 pub fn show_with_back(
276 handle: &OverlayHandle,
277 builder: Rc<dyn Fn() -> View>,
278 z_index: f32,
279 pass_through: bool,
280 back_handler: BackHandler,
281 ) -> Self {
282 let id = handle.show_entry_with_back(builder, z_index, pass_through, Some(back_handler));
283 Self {
284 handle: handle.clone(),
285 id: Some(id),
286 }
287 }
288
289 pub fn id(&self) -> Option<u64> {
291 self.id
292 }
293
294 pub fn dismiss(mut self) {
297 self.dismiss_now();
298 }
299
300 fn dismiss_now(&mut self) {
301 if let Some(id) = self.id.take() {
302 self.handle.dismiss(id);
303 }
304 }
305}
306
307impl Drop for OverlayGuard {
308 fn drop(&mut self) {
309 self.dismiss_now();
310 }
311}
312
313#[derive(Clone)]
314pub struct SnackbarController {
315 inner: Rc<SnackbarControllerInner>,
316}
317
318struct SnackbarControllerInner {
319 state: RefCell<SnackbarState>,
320 overlay: OverlayHandle,
321}
322
323#[derive(Clone)]
324pub struct SnackbarAction {
325 pub label: String,
326 pub on_click: Rc<dyn Fn()>,
327}
328
329#[derive(Clone)]
330pub struct SnackbarRequest {
331 pub message: String,
332 pub action: Option<SnackbarAction>,
333 pub duration_ms: u32,
334 pub builder: Rc<dyn Fn(bool) -> View>,
337}
338
339struct SnackbarState {
340 queue: VecDeque<SnackbarRequest>,
341 active: Option<ActiveSnackbar>,
342}
343
344enum SnackbarPhase {
347 Showing { deadline: Instant },
348 Dismissing { deadline: Instant },
349}
350
351struct ActiveSnackbar {
352 #[allow(dead_code)]
355 guard: OverlayGuard,
356 message: String,
357 action: Option<SnackbarAction>,
358 phase: SnackbarPhase,
359}
360
361impl ActiveSnackbar {
362 fn deadline(&self) -> Instant {
363 match self.phase {
364 SnackbarPhase::Showing { deadline } | SnackbarPhase::Dismissing { deadline } => {
365 deadline
366 }
367 }
368 }
369
370 fn is_dismissing(&self) -> bool {
371 matches!(self.phase, SnackbarPhase::Dismissing { .. })
372 }
373
374 fn start_dismiss(&mut self, now: Instant) {
376 if !self.is_dismissing() {
377 self.phase = SnackbarPhase::Dismissing {
378 deadline: now + Duration::from_millis(SNACKBAR_EXIT_ANIM_MS),
379 };
380 request_frame();
381 }
382 }
383}
384
385impl SnackbarController {
386 pub fn new(overlay: OverlayHandle) -> Self {
387 Self::with_handle(overlay)
388 }
389
390 pub fn ambient() -> Self {
394 let overlay = ambient_overlay().unwrap_or_default();
395 Self::with_handle(overlay)
396 }
397
398 fn with_handle(overlay: OverlayHandle) -> Self {
399 let controller = Self {
400 inner: Rc::new(SnackbarControllerInner {
401 state: RefCell::new(SnackbarState {
402 queue: VecDeque::new(),
403 active: None,
404 }),
405 overlay,
406 }),
407 };
408
409 SNACKBAR_REGISTRY.with(|reg| reg.borrow_mut().push(Rc::downgrade(&controller.inner)));
410 controller
411 }
412
413 pub fn handle(&self) -> OverlayHandle {
415 self.inner.overlay.clone()
416 }
417
418 fn live_controllers() -> Vec<Rc<SnackbarControllerInner>> {
419 SNACKBAR_REGISTRY.with(|reg| {
420 let mut reg = reg.borrow_mut();
421 reg.retain(|w| w.upgrade().is_some());
422 reg.iter().filter_map(Weak::upgrade).collect()
423 })
424 }
425
426 pub fn tick_all() {
428 for inner in Self::live_controllers() {
429 Self { inner }.tick();
430 }
431 }
432
433 pub fn next_deadline() -> Option<Instant> {
436 Self::live_controllers()
437 .iter()
438 .filter_map(|inner| {
439 inner
440 .state
441 .borrow()
442 .active
443 .as_ref()
444 .map(ActiveSnackbar::deadline)
445 })
446 .min()
447 }
448
449 pub fn show(&self, request: SnackbarRequest) {
450 if self.inner.state.borrow().active.is_some() {
451 self.inner.state.borrow_mut().queue.push_back(request);
452 } else {
453 self.activate_next(request);
454 }
455 }
456
457 pub fn tick(&self) {
458 let now = Instant::now();
459 let mut finished: Option<ActiveSnackbar> = None;
460 {
461 let mut state = self.inner.state.borrow_mut();
462 if let Some(active) = state.active.as_mut() {
463 match active.phase {
464 SnackbarPhase::Showing { deadline } if now >= deadline => {
465 active.start_dismiss(now);
466 }
467 SnackbarPhase::Dismissing { deadline } if now >= deadline => {
468 finished = state.active.take();
469 }
470 _ => {}
471 }
472 }
473 }
474 drop(finished);
475 self.activate_next_if_needed();
476 }
477
478 pub fn dismiss(&self) {
479 let now = Instant::now();
480 if let Some(active) = self.inner.state.borrow_mut().active.as_mut() {
481 active.start_dismiss(now);
482 }
483 }
484
485 pub fn current(&self) -> Option<(String, Option<SnackbarAction>)> {
486 let inner = self.inner.state.borrow();
487 inner
488 .active
489 .as_ref()
490 .map(|active| (active.message.clone(), active.action.clone()))
491 }
492
493 fn activate_next_if_needed(&self) {
494 if self.inner.state.borrow().active.is_some() {
495 return;
496 }
497 let req = self.inner.state.borrow_mut().queue.pop_front();
498 if let Some(req) = req {
499 self.activate_next(req);
500 }
501 }
502
503 fn activate_next(&self, req: SnackbarRequest) {
504 if self.inner.state.borrow().active.is_some() {
505 return;
506 }
507 let weak = Rc::downgrade(&self.inner);
508 let original_builder = req.builder.clone();
509 let wrapped_builder: Rc<dyn Fn() -> View> = Rc::new(move || {
510 let dismissing = weak
511 .upgrade()
512 .and_then(|inner| {
513 inner
514 .state
515 .borrow()
516 .active
517 .as_ref()
518 .map(ActiveSnackbar::is_dismissing)
519 })
520 .unwrap_or(false);
521 (original_builder)(dismissing)
522 });
523 let action = req.action.map(|a| SnackbarAction {
524 on_click: {
525 let original = a.on_click;
526 let controller = self.clone();
527 Rc::new(move || {
528 (original)();
529 controller.dismiss();
530 })
531 },
532 label: a.label,
533 });
534 let guard = OverlayGuard::show(&self.inner.overlay, wrapped_builder, 900.0, true);
535 let deadline = Instant::now() + Duration::from_millis(u64::from(req.duration_ms.max(1)));
536 self.inner.state.borrow_mut().active = Some(ActiveSnackbar {
537 guard,
538 message: req.message,
539 action,
540 phase: SnackbarPhase::Showing { deadline },
541 });
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548 use std::cell::Cell;
549
550 fn test_request() -> SnackbarRequest {
551 SnackbarRequest {
552 message: "hi".into(),
553 action: None,
554 duration_ms: 60_000,
555 builder: Rc::new(|_| View::new(0, ViewKind::OverlayHost)),
556 }
557 }
558
559 #[test]
560 fn dropped_controller_is_pruned_from_registry() {
561 {
562 let overlay = OverlayHandle::new();
563 let controller = SnackbarController::new(overlay);
564 controller.show(test_request());
565 SnackbarController::tick_all();
566 assert!(SnackbarController::next_deadline().is_some());
567 }
568 SnackbarController::tick_all();
569 assert!(SnackbarController::next_deadline().is_none());
570 }
571
572 #[test]
573 fn dismiss_schedules_exit_deadline() {
574 let overlay = OverlayHandle::new();
575 let controller = SnackbarController::new(overlay);
576 controller.show(test_request());
577 let before = Instant::now();
578 controller.dismiss();
579 let deadline = SnackbarController::next_deadline().expect("deadline while dismissing");
580 assert!(deadline >= before);
581 assert!(deadline.saturating_duration_since(before) <= Duration::from_millis(500));
582 controller.dismiss();
583 let again = SnackbarController::next_deadline().expect("deadline while dismissing");
584 assert_eq!(deadline, again);
585 drop(controller);
586 SnackbarController::tick_all();
587 }
588
589 #[test]
590 fn back_uses_topmost_handler_and_falls_back_after_dismiss() {
591 let overlay = OverlayHandle::new();
592 let lower = Rc::new(Cell::new(0u32));
593 let upper = Rc::new(Cell::new(0u32));
594 let lower_handler = lower.clone();
595 let upper_handler = upper.clone();
596 let builder: Rc<dyn Fn() -> View> = Rc::new(|| View::new(0, ViewKind::OverlayHost));
597 let lower_id = overlay.show_entry_with_back(
598 builder.clone(),
599 1.0,
600 false,
601 Some(Rc::new(move || {
602 lower_handler.set(lower_handler.get() + 1);
603 true
604 })),
605 );
606 overlay.show_entry_with_back(
607 builder,
608 2.0,
609 false,
610 Some(Rc::new(move || {
611 upper_handler.set(upper_handler.get() + 1);
612 true
613 })),
614 );
615 assert!(overlay.handle_back());
616 assert_eq!(upper.get(), 1);
617 assert_eq!(lower.get(), 0);
618 overlay.dismiss(lower_id);
619 assert!(overlay.handle_back());
620 assert_eq!(upper.get(), 2);
621 }
622
623 #[test]
624 fn guard_drop_dismisses_entry() {
625 let overlay = OverlayHandle::new();
626 let builder: Rc<dyn Fn() -> View> = Rc::new(|| View::new(0, ViewKind::OverlayHost));
627 {
628 let guard = overlay.show_guard(builder.clone(), 5.0, false);
629 assert!(guard.id().is_some());
630 assert_eq!(overlay.inner.borrow().entries.len(), 1);
631 guard.dismiss();
632 assert!(overlay.inner.borrow().entries.is_empty());
633 }
634 let guard = overlay.show_guard(builder, 5.0, false);
635 assert_eq!(overlay.inner.borrow().entries.len(), 1);
636 drop(guard);
637 assert!(overlay.inner.borrow().entries.is_empty());
638 }
639}