Skip to main content

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}