use std::time::Duration;
use egui::Color32; use egui_notify::{Anchor, Toasts};
pub const SCREEN_CAP: usize = 3;
pub const AUTO_CLOSE: Duration = Duration::from_secs(8);
pub const MARGIN: (f32, f32) = (20.0, 20.0);
pub const SPACING: f32 = 10.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlertLevel {
#[default]
Info,
Warning,
Error,
}
impl AlertLevel {
pub const fn color(self, theme: crate::tokens::Theme) -> Color32 {
let skin = crate::tokens::alert_of_theme(theme);
match self {
Self::Info => skin.info,
Self::Warning => skin.warning,
Self::Error => skin.error,
}
}
pub const fn label(self) -> &'static str {
match self {
Self::Info => "预警",
Self::Warning => "注意",
Self::Error => "故障",
}
}
}
pub fn configured() -> Toasts {
Toasts::new()
.with_anchor(Anchor::BottomRight)
.reverse(true)
.with_margin(egui::vec2(MARGIN.0, MARGIN.1))
.with_spacing(SPACING)
}
pub fn push(
toasts: &mut Toasts,
level: AlertLevel,
theme: crate::tokens::Theme,
text: impl Into<String>,
) {
if toasts.len() >= SCREEN_CAP {
toasts.dismiss_oldest_toast();
}
toasts
.custom(
format!("{}:{}", level.label(), text.into()),
"▎".to_string(),
level.color(theme),
)
.duration(AUTO_CLOSE)
.closable(false)
.show_progress_bar(false);
}
#[derive(Default)]
pub struct ToastsCell {
inner: std::sync::Mutex<Option<Toasts>>,
}
impl ToastsCell {
pub fn lock(&self) -> std::sync::MutexGuard<'_, Option<Toasts>> {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl std::fmt::Debug for ToastsCell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let n = self.lock().as_ref().map(|t| t.len()).unwrap_or(0);
f.debug_struct("ToastsCell")
.field("queued", &n)
.field("configured", &self.lock().is_some())
.finish()
}
}
pub fn drain_and_show(ctx: &egui::Context, shared: &crate::state::UiShared) {
let pending = shared.take_alerts();
let theme = crate::tokens::theme_of(ctx);
let mut cell = shared.toasts();
let toasts = cell.get_or_insert_with(configured);
for (level, text) in pending {
push(toasts, level, theme, text);
}
toasts.show(ctx);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alert_colors_are_the_python_triplet() {
let dark = crate::tokens::Theme::Dark;
assert_eq!(AlertLevel::Info.color(dark), crate::tokens::alert::INFO);
assert_eq!(
AlertLevel::Warning.color(dark),
crate::tokens::alert::WARNING
);
assert_eq!(AlertLevel::Error.color(dark), crate::tokens::alert::ERROR);
let light = crate::tokens::Theme::Light;
assert_ne!(
AlertLevel::Warning.color(light),
crate::tokens::alert::WARNING,
"浅档沿用深档橙 = 浅底上糊字"
);
}
#[test]
fn cap_and_auto_close_match_the_python_numbers() {
assert_eq!(SCREEN_CAP, 3);
assert_eq!(AUTO_CLOSE, Duration::from_secs(8));
assert_eq!(SPACING, 10.0);
}
#[test]
fn pushing_a_fourth_dismisses_the_oldest_first() {
let mut t = configured();
for i in 0..3 {
push(
&mut t,
AlertLevel::Info,
crate::tokens::Theme::Dark,
format!("甲{i}"),
);
}
assert_eq!(t.len(), 3, "三条应当都在(未退场)");
push(
&mut t,
AlertLevel::Error,
crate::tokens::Theme::Dark,
"第四条",
);
assert_eq!(
t.len(),
4,
"dismiss 只是置退场状态,真正出列要 show() 跑过动画帧 —— 这里钉的是「关过最旧那条」这个动作"
);
}
}