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) -> Color32 {
match self {
Self::Info => crate::tokens::alert::INFO,
Self::Warning => crate::tokens::alert::WARNING,
Self::Error => crate::tokens::alert::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, text: impl Into<String>) {
if toasts.len() >= SCREEN_CAP {
toasts.dismiss_oldest_toast();
}
toasts
.custom(
format!("{}:{}", level.label(), text.into()),
"▎".to_string(),
level.color(),
)
.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 mut cell = shared.toasts();
let toasts = cell.get_or_insert_with(configured);
for (level, text) in pending {
push(toasts, level, text);
}
toasts.show(ctx);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alert_colors_are_the_python_triplet() {
assert_eq!(AlertLevel::Info.color(), crate::tokens::alert::INFO);
assert_eq!(AlertLevel::Warning.color(), crate::tokens::alert::WARNING);
assert_eq!(AlertLevel::Error.color(), crate::tokens::alert::ERROR);
}
#[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, format!("甲{i}"));
}
assert_eq!(t.len(), 3, "三条应当都在(未退场)");
push(&mut t, AlertLevel::Error, "第四条");
assert_eq!(
t.len(),
4,
"dismiss 只是置退场状态,真正出列要 show() 跑过动画帧 —— 这里钉的是「关过最旧那条」这个动作"
);
}
}