use std::rc::Rc;
use teksilo_app::{DefaultPostRoot, TeksiloAppBuilder};
use teksilo_core::app_event::AppEvent;
use teksilo_widgets::notification::{NotificationArchive, NotificationArchiveModel};
use teksilo_widgets::primitives::{Expand, ZStack};
use teksilo_widgets::toast::{ToastHost, ToastInstallOptions, ToastRegistry};
pub trait TeksiloAppBuilderToastExt {
fn install_toast(self, options: ToastInstallOptions) -> Self;
fn install_toast_default(self) -> Self;
}
impl TeksiloAppBuilderToastExt for TeksiloAppBuilder {
fn install_toast(self, options: ToastInstallOptions) -> Self {
let archive: Option<Rc<NotificationArchiveModel>> = match &options.archive {
None => None,
Some(NotificationArchive::InMemory { .. }) => Some(Rc::new(
NotificationArchiveModel::open(
options.archive.as_ref().unwrap(),
&teksilo_settings::AppPaths::for_testing(std::path::Path::new("")),
std::time::Duration::from_millis(0),
)
.expect("in-memory archive open never fails"),
)),
Some(NotificationArchive::Persistent { .. }) => {
let paths = self.configured_app_paths().cloned().expect(
"install_toast(Persistent) requires app_paths() (or application(...)) to be \
set on the builder first. For tests / sandboxed builds, override to \
`NotificationArchive::in_memory()` or `None`.",
);
Some(Rc::new(
NotificationArchiveModel::open(
options.archive.as_ref().unwrap(),
&paths,
teksilo_settings::DEFAULT_DEBOUNCE,
)
.expect("notification archive: file open failed"),
))
}
};
let registry = match archive.clone() {
Some(a) => ToastRegistry::with_archive(options.clone(), a),
None => ToastRegistry::new(options.clone()),
};
let registry_for_hook = registry.clone();
let options_for_hook = options.clone();
let post_root = DefaultPostRoot::new(move |tree, root_id| {
let host = ToastHost::new(registry_for_hook.clone(), options_for_hook.clone());
let host_id = tree.add(host);
let filled_root = tree.add(Expand::new().respect_intrinsic().child_id(root_id));
let stack = ZStack::new().add_child(filled_root).add_child(host_id);
tree.add(stack)
});
let registry_for_write_failure = registry.clone();
let write_failure_observer = move |event: &AppEvent| {
if let AppEvent::SettingsWriteFailed {
path,
attempts,
dropped_patches,
message,
} = event
{
let file_name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string());
registry_for_write_failure.show_settings_write_failed(
&file_name,
*attempts,
*dropped_patches,
message,
);
}
};
let mut builder = self
.app_state(registry)
.register_app_event_observer(write_failure_observer);
if let Some(a) = archive {
builder = builder.app_state(a);
}
builder.register_post_root(post_root)
}
fn install_toast_default(self) -> Self {
self.install_toast(ToastInstallOptions::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_widgets::notification::NotificationArchive;
#[test]
#[should_panic(expected = "app_paths() (or application(...)) to be set")]
fn install_persistent_without_app_paths_panics_with_helpful_message() {
let opts = ToastInstallOptions {
archive: Some(NotificationArchive::persistent("notif_test_panic")),
..ToastInstallOptions::default()
};
let _builder = TeksiloAppBuilder::new().install_toast(opts);
}
#[test]
fn settings_write_failed_event_reaching_the_observer_enqueues_a_toast() {
use std::path::PathBuf;
use teksilo_app::AppEventObservers;
use teksilo_core::app_event::AppEvent;
let app = TeksiloAppBuilder::new()
.install_toast(ToastInstallOptions {
archive: None,
..ToastInstallOptions::default()
})
.build_headless();
let ctx = app.tree.app_context();
let registry = ctx
.app_state::<ToastRegistry>()
.expect("install_toast registers a ToastRegistry in app_state")
.clone();
let observers = ctx
.app_state::<AppEventObservers>()
.expect("install_toast registers an AppEvent observer in app_state")
.clone();
assert_eq!(registry.live_count(), 0, "no toast before the event fires");
let event = AppEvent::SettingsWriteFailed {
path: PathBuf::from("/tmp/does-not-exist/window_state.toml"),
attempts: 5,
dropped_patches: 2,
message: "disk full".to_string(),
};
(observers.0)(&event);
assert_eq!(
registry.live_count(),
1,
"the observer must enqueue exactly one toast for the failed write"
);
}
#[test]
fn unrelated_app_events_do_not_enqueue_a_toast() {
use teksilo_app::AppEventObservers;
use teksilo_core::app_event::AppEvent;
let app = TeksiloAppBuilder::new()
.install_toast(ToastInstallOptions {
archive: None,
..ToastInstallOptions::default()
})
.build_headless();
let ctx = app.tree.app_context();
let registry = ctx
.app_state::<ToastRegistry>()
.expect("install_toast registers a ToastRegistry in app_state")
.clone();
let observers = ctx
.app_state::<AppEventObservers>()
.expect("install_toast registers an AppEvent observer in app_state")
.clone();
(observers.0)(&AppEvent::BackgroundComplete {
operation_id: "unrelated".to_string(),
});
assert_eq!(
registry.live_count(),
0,
"an unrelated AppEvent must not enqueue a toast"
);
}
}