teksilo_widgets/toast/ext.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Extension methods on [`EventContext`] for showing and dismissing
5//! toasts. Mirrors the `EventContextFileDialogExt` pattern from
6//! `teksilo-platform`.
7//!
8//! Apps `use teksilo_widgets::toast::EventContextToastExt;` (or
9//! `use teksilo::prelude::*;` once the umbrella re-exports it) and
10//! then call `ctx.show_toast(toast)` / `ctx.dismiss_toast(handle)`
11//! from any handler.
12
13use teksilo_core::widget::EventContext;
14
15use crate::toast::registry::ToastRegistry;
16use crate::toast::{Toast, ToastHandle, ToastRoute};
17
18/// Convenience methods on [`EventContext`] for the toast system. The
19/// registry is looked up via [`EventContext::app_state`] — the
20/// `install_toast` extension trait registers `ToastRegistry` there
21/// at app boot.
22///
23/// All methods are no-ops (returning a dropped [`ToastHandle`] where
24/// applicable) when `install_toast` was not called — a one-shot
25/// `log::warn!` fires the first time the missing registration is
26/// detected, so missing installs surface in logs without crashing
27/// app code that defensively calls `show_toast`.
28pub trait EventContextToastExt {
29 /// Present a [`Toast`] through the installed
30 /// [`ToastHost`](crate::toast::host::ToastHost). Returns a
31 /// [`ToastHandle`] for programmatic control.
32 fn show_toast(&mut self, toast: Toast) -> ToastHandle;
33
34 /// Programmatically dismiss a toast by handle, with cause
35 /// `ToastDismissCause::Programmatic`. Equivalent to
36 /// `handle.dismiss(ctx)`. No-op if the toast has already been
37 /// dismissed.
38 fn dismiss_toast(&mut self, handle: &ToastHandle);
39}
40
41impl EventContextToastExt for EventContext<'_> {
42 fn show_toast(&mut self, mut toast: Toast) -> ToastHandle {
43 // Default routing: a toast presented with no explicit
44 // `.target()` / `.broadcast()` is tagged with the presenting
45 // window's id. This is the one join point that has both a
46 // `Toast` and a real `EventContext` (hence a real window) —
47 // it's what makes every pre-existing `Toast::info(...).present(ctx)`
48 // call site in every app correct with zero changes: a
49 // single-window app has exactly one window, so "origin
50 // window" behaves identically to the old "one shared queue".
51 if toast.target.is_none() {
52 toast.target = self.window().map(|w| ToastRoute::Window(w.id()));
53 }
54 let Some(registry) = self.app_state::<ToastRegistry>().cloned() else {
55 warn_missing_install();
56 // Return a dropped handle — `is_alive` returns false,
57 // `dismiss` is a no-op. The caller's `on_dismiss`
58 // callback (if any) is silently dropped along with the
59 // toast.
60 return ToastHandle::new(crate::toast::ToastHandleInner {
61 entry_id: 0,
62 dismissed: std::cell::Cell::new(true),
63 registry: ToastRegistry::new(crate::toast::host::ToastInstallOptions::default()),
64 });
65 };
66 let (handle, overflow_callback) = registry.enqueue(toast);
67 // Slot-pool overflow: fire on_dismiss synchronously with the
68 // handler context the caller is in.
69 if let Some((cause, cb)) = overflow_callback {
70 cb(cause, self);
71 }
72 handle
73 }
74
75 fn dismiss_toast(&mut self, handle: &ToastHandle) {
76 // Delegate to the handle's own dismiss path.
77 let handle = handle.clone();
78 handle.dismiss(self);
79 }
80}
81
82/// One-shot stderr warning when `show_toast` is called without
83/// `install_toast`. Uses a thread-local flag so noisy callers in a
84/// tight loop don't spam the output. (Stderr rather than `log::warn!`
85/// to avoid adding a `log` dependency to teksilo-widgets — the registry
86/// missing is a setup error rather than a runtime condition that
87/// needs structured logging.)
88fn warn_missing_install() {
89 thread_local! {
90 static WARNED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
91 }
92 WARNED.with(|w| {
93 if !w.get() {
94 eprintln!(
95 "[teksilo-widgets::toast] ctx.show_toast(...) called without install_toast(opts) \
96 on the TeksiloAppBuilder — the toast was dropped. See teksilo::install_toast \
97 or teksilo_widgets::toast docs."
98 );
99 w.set(true);
100 }
101 });
102}