1use std::time::Duration;
21
22use egui::Color32; use egui_notify::{Anchor, Toasts};
24
25pub const SCREEN_CAP: usize = 3;
27
28pub const AUTO_CLOSE: Duration = Duration::from_secs(8);
30
31pub const MARGIN: (f32, f32) = (20.0, 20.0);
34
35pub const SPACING: f32 = 10.0;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum AlertLevel {
41 #[default]
43 Info,
44 Warning,
46 Error,
48}
49
50impl AlertLevel {
51 pub const fn color(self) -> Color32 {
53 match self {
54 Self::Info => crate::tokens::alert::INFO,
55 Self::Warning => crate::tokens::alert::WARNING,
56 Self::Error => crate::tokens::alert::ERROR,
57 }
58 }
59
60 pub const fn label(self) -> &'static str {
62 match self {
63 Self::Info => "预警",
64 Self::Warning => "注意",
65 Self::Error => "故障",
66 }
67 }
68}
69
70pub fn configured() -> Toasts {
72 Toasts::new()
73 .with_anchor(Anchor::BottomRight)
74 .reverse(true)
77 .with_margin(egui::vec2(MARGIN.0, MARGIN.1))
78 .with_spacing(SPACING)
79}
80
81pub fn push(toasts: &mut Toasts, level: AlertLevel, text: impl Into<String>) {
83 if toasts.len() >= SCREEN_CAP {
84 toasts.dismiss_oldest_toast();
85 }
86 toasts
87 .custom(
88 format!("{}:{}", level.label(), text.into()),
89 "▎".to_string(),
90 level.color(),
91 )
92 .duration(AUTO_CLOSE)
93 .closable(false)
95 .show_progress_bar(false);
96}
97
98#[derive(Default)]
103pub struct ToastsCell {
104 inner: std::sync::Mutex<Option<Toasts>>,
105}
106
107impl ToastsCell {
108 pub fn lock(&self) -> std::sync::MutexGuard<'_, Option<Toasts>> {
110 self.inner
111 .lock()
112 .unwrap_or_else(std::sync::PoisonError::into_inner)
113 }
114}
115
116impl std::fmt::Debug for ToastsCell {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 let n = self.lock().as_ref().map(|t| t.len()).unwrap_or(0);
119 f.debug_struct("ToastsCell")
120 .field("queued", &n)
121 .field("configured", &self.lock().is_some())
122 .finish()
123 }
124}
125
126pub fn drain_and_show(ctx: &egui::Context, shared: &crate::state::UiShared) {
131 let pending = shared.take_alerts();
132 let mut cell = shared.toasts();
133 let toasts = cell.get_or_insert_with(configured);
134 for (level, text) in pending {
135 push(toasts, level, text);
136 }
137 toasts.show(ctx);
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
146 fn alert_colors_are_the_python_triplet() {
147 assert_eq!(AlertLevel::Info.color(), crate::tokens::alert::INFO);
148 assert_eq!(AlertLevel::Warning.color(), crate::tokens::alert::WARNING);
149 assert_eq!(AlertLevel::Error.color(), crate::tokens::alert::ERROR);
150 }
151
152 #[test]
154 fn cap_and_auto_close_match_the_python_numbers() {
155 assert_eq!(SCREEN_CAP, 3);
156 assert_eq!(AUTO_CLOSE, Duration::from_secs(8));
157 assert_eq!(SPACING, 10.0);
158 }
159
160 #[test]
162 fn pushing_a_fourth_dismisses_the_oldest_first() {
163 let mut t = configured();
164 for i in 0..3 {
165 push(&mut t, AlertLevel::Info, format!("甲{i}"));
166 }
167 assert_eq!(t.len(), 3, "三条应当都在(未退场)");
168 push(&mut t, AlertLevel::Error, "第四条");
169 assert_eq!(
170 t.len(),
171 4,
172 "dismiss 只是置退场状态,真正出列要 show() 跑过动画帧 —— 这里钉的是「关过最旧那条」这个动作"
173 );
174 }
175}