use crate::backend::{Backend, Notification};
use crate::primitives::toast::{Toast, ToastButton, ToastSeverity};
use crate::types::WidgetId;
pub fn notify_or_toast(backend: &dyn Backend, n: Notification) -> Option<Toast> {
if backend.backend_caps().notifications {
backend.services().send_notification(n);
return None;
}
let id = match n.tag() {
Some(tag) => WidgetId::new(tag),
None => WidgetId::new(n.title.as_str()),
};
let severity = if n.urgent {
ToastSeverity::Warning
} else {
ToastSeverity::Info
};
let actions = n
.actions()
.iter()
.map(|(action_id, label)| ToastButton {
id: *action_id,
label: label.clone(),
primary: false,
})
.collect();
Some(Toast {
id,
title: n.title,
body: n.body,
severity,
actions,
accent: None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::toast::ToastHit;
use crate::testing::RecordingBackend;
#[test]
fn no_native_notifications_degrades_to_some() {
let backend = RecordingBackend::new();
assert!(!backend.backend_caps().notifications);
let result = notify_or_toast(&backend, Notification::new("t", "b"));
assert!(result.is_some());
}
#[test]
fn degrades_to_toast_with_title_body_and_severity() {
let backend = RecordingBackend::new();
let item = notify_or_toast(
&backend,
Notification::new("Build failed", "3 errors")
.with_action(WidgetId::new("open-problems"), "Show"),
)
.expect("no native notifications on RecordingBackend");
assert_eq!(item.title, "Build failed");
assert_eq!(item.body, "3 errors");
assert_eq!(item.severity, ToastSeverity::Info);
assert_eq!(
item.actions.first().map(|a| a.id.clone()),
Some(WidgetId::new("open-problems"))
);
}
#[test]
fn urgent_maps_to_warning_severity() {
let backend = RecordingBackend::new();
let mut n = Notification::new("t", "b");
n.urgent = true;
let item = notify_or_toast(&backend, n).unwrap();
assert_eq!(item.severity, ToastSeverity::Warning);
}
#[test]
fn tag_becomes_toast_id_falling_back_to_title() {
let backend = RecordingBackend::new();
let tagged = notify_or_toast(
&backend,
Notification::new("t", "b").with_tag("build-status"),
)
.unwrap();
assert_eq!(tagged.id, WidgetId::new("build-status"));
let untagged = notify_or_toast(&backend, Notification::new("untagged title", "b")).unwrap();
assert_eq!(untagged.id, WidgetId::new("untagged title"));
}
#[test]
fn every_action_survives_degrade() {
let backend = RecordingBackend::new();
let n = Notification::new("t", "b")
.with_action(WidgetId::new("first"), "First")
.with_action(WidgetId::new("second"), "Second");
let item = notify_or_toast(&backend, n).unwrap();
let ids: Vec<WidgetId> = item.actions.iter().map(|a| a.id.clone()).collect();
assert_eq!(ids, vec![WidgetId::new("first"), WidgetId::new("second")]);
}
#[test]
fn degraded_toast_action_is_hit_testable() {
use crate::primitives::toast::{ToastCorner, ToastMeasure, ToastOverlay};
let backend = RecordingBackend::new();
let item = notify_or_toast(
&backend,
Notification::new("t", "b").with_action(WidgetId::new("retry"), "Retry"),
)
.unwrap();
let stack = ToastOverlay {
id: WidgetId::new("toasts"),
corner: ToastCorner::BottomRight,
toasts: vec![item],
focus: None,
};
let layout = stack.layout(0.0, 0.0, 400.0, 300.0, 8.0, 8.0, |_| {
let mut m = ToastMeasure::new(220.0, 48.0);
m.action_rects = vec![crate::event::Rect::new(140.0, 28.0, 60.0, 20.0)];
m.dismiss_rect = Some(crate::event::Rect::new(200.0, 0.0, 20.0, 20.0));
m
});
let action_bounds = *layout.visible_toasts[0]
.action_rects
.first()
.expect("toast with an action must lay out an action rect");
let cx = action_bounds.x + action_bounds.width / 2.0;
let cy = action_bounds.y + action_bounds.height / 2.0;
assert_eq!(
layout.hit_test(cx, cy),
ToastHit::Action(WidgetId::new("retry"))
);
}
}