1use crate::atoms::icons;
4use crate::Theme;
5use egui::{Color32, FontFamily, RichText, Ui};
6use egui_cha::{Severity, ViewCtx};
7use std::collections::VecDeque;
8use std::time::{Duration, Instant};
9
10#[derive(Clone, Debug)]
12pub struct ErrorEntry {
13 pub message: String,
14 pub timestamp: Instant,
15 pub level: ErrorLevel,
16}
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub enum ErrorLevel {
23 Debug,
25 Info,
27 Warning,
29 #[default]
31 Error,
32 Critical,
34}
35
36impl From<Severity> for ErrorLevel {
37 fn from(severity: Severity) -> Self {
38 match severity {
39 Severity::Debug => ErrorLevel::Debug,
40 Severity::Info => ErrorLevel::Info,
41 Severity::Warn => ErrorLevel::Warning,
42 Severity::Error => ErrorLevel::Error,
43 Severity::Critical => ErrorLevel::Critical,
44 }
45 }
46}
47
48impl ErrorLevel {
49 pub fn is_production_visible(self) -> bool {
51 self >= ErrorLevel::Info
52 }
53}
54
55pub struct ErrorConsoleState {
57 errors: VecDeque<ErrorEntry>,
58 max_entries: usize,
59 auto_dismiss: Option<Duration>,
60}
61
62impl Default for ErrorConsoleState {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl ErrorConsoleState {
69 pub fn new() -> Self {
70 Self {
71 errors: VecDeque::new(),
72 max_entries: 10,
73 auto_dismiss: Some(Duration::from_secs(10)),
74 }
75 }
76
77 pub fn with_max_entries(mut self, max: usize) -> Self {
79 self.max_entries = max;
80 self
81 }
82
83 pub fn with_auto_dismiss(mut self, duration: Option<Duration>) -> Self {
85 self.auto_dismiss = duration;
86 self
87 }
88
89 pub fn push(&mut self, message: impl Into<String>) {
91 self.push_with_level(message, ErrorLevel::Error);
92 }
93
94 pub fn push_warning(&mut self, message: impl Into<String>) {
96 self.push_with_level(message, ErrorLevel::Warning);
97 }
98
99 pub fn push_info(&mut self, message: impl Into<String>) {
101 self.push_with_level(message, ErrorLevel::Info);
102 }
103
104 pub fn push_with_level(&mut self, message: impl Into<String>, level: ErrorLevel) {
106 self.errors.push_back(ErrorEntry {
107 message: message.into(),
108 timestamp: Instant::now(),
109 level,
110 });
111
112 while self.errors.len() > self.max_entries {
114 self.errors.pop_front();
115 }
116 }
117
118 pub fn cleanup(&mut self) {
120 if let Some(duration) = self.auto_dismiss {
121 let now = Instant::now();
122 self.errors
123 .retain(|e| now.duration_since(e.timestamp) < duration);
124 }
125 }
126
127 pub fn clear(&mut self) {
129 self.errors.clear();
130 }
131
132 pub fn dismiss(&mut self, index: usize) {
134 if index < self.errors.len() {
135 self.errors.remove(index);
136 }
137 }
138
139 pub fn is_empty(&self) -> bool {
141 self.errors.is_empty()
142 }
143
144 pub fn len(&self) -> usize {
146 self.errors.len()
147 }
148
149 pub fn iter(&self) -> impl Iterator<Item = &ErrorEntry> {
151 self.errors.iter()
152 }
153
154 pub fn drain(&mut self) -> impl Iterator<Item = ErrorEntry> + '_ {
156 self.errors.drain(..)
157 }
158}
159
160#[derive(Clone, Debug)]
162pub enum ErrorConsoleMsg {
163 Dismiss(usize),
164 DismissAll,
165}
166
167pub struct ErrorConsole;
169
170impl ErrorConsole {
171 fn level_colors(level: ErrorLevel, theme: &Theme) -> (Color32, Color32, &'static str) {
174 let (log_color, icon) = match level {
175 ErrorLevel::Debug => (theme.log_debug, icons::WRENCH),
176 ErrorLevel::Info => (theme.log_info, icons::INFO),
177 ErrorLevel::Warning => (theme.log_warn, icons::WARNING),
178 ErrorLevel::Error => (theme.log_error, icons::X_CIRCLE),
179 ErrorLevel::Critical => (theme.log_critical, icons::FIRE),
180 };
181
182 let bg_color = Color32::from_rgba_unmultiplied(
184 log_color.r(),
185 log_color.g(),
186 log_color.b(),
187 30, );
189
190 (bg_color, log_color, icon)
191 }
192
193 fn header_color(theme: &Theme) -> Color32 {
195 theme.log_error
196 }
197
198 pub fn show<Msg>(
200 ctx: &mut ViewCtx<'_, Msg>,
201 state: &ErrorConsoleState,
202 map_msg: impl Fn(ErrorConsoleMsg) -> Msg + Clone,
203 ) {
204 if state.is_empty() {
205 return;
206 }
207
208 let theme = Theme::current(ctx.ui.ctx());
209
210 let mut dismiss_index: Option<usize> = None;
212 let mut clear_all = false;
213
214 ctx.ui.vertical(|ui| {
215 ui.horizontal(|ui| {
217 ui.label(
218 RichText::new(format!("Errors ({})", state.len()))
219 .strong()
220 .color(Self::header_color(&theme)),
221 );
222 ui.add_space(8.0);
223 if ui.small_button("Clear All").clicked() {
224 clear_all = true;
225 }
226 });
227
228 ui.add_space(4.0);
229
230 for (index, entry) in state.iter().enumerate() {
232 let (bg_color, text_color, icon) = Self::level_colors(entry.level, &theme);
233
234 egui::Frame::new()
235 .fill(bg_color)
236 .corner_radius(4.0)
237 .inner_margin(egui::Margin::symmetric(8, 4))
238 .show(ui, |ui| {
239 ui.horizontal(|ui| {
240 ui.label(
241 RichText::new(icon)
242 .family(FontFamily::Name("icons".into()))
243 .color(text_color),
244 );
245 ui.label(RichText::new(&entry.message).color(text_color));
246 ui.with_layout(
247 egui::Layout::right_to_left(egui::Align::Center),
248 |ui| {
249 if ui.small_button("×").clicked() {
250 dismiss_index = Some(index);
251 }
252 },
253 );
254 });
255 });
256
257 ui.add_space(2.0);
258 }
259 });
260
261 if clear_all {
263 ctx.emit(map_msg(ErrorConsoleMsg::DismissAll));
264 } else if let Some(index) = dismiss_index {
265 ctx.emit(map_msg(ErrorConsoleMsg::Dismiss(index)));
266 }
267 }
268
269 pub fn show_ui(ui: &mut Ui, state: &ErrorConsoleState) -> Option<ErrorConsoleMsg> {
271 if state.is_empty() {
272 return None;
273 }
274
275 let theme = Theme::current(ui.ctx());
276 let mut result = None;
277
278 ui.vertical(|ui| {
279 ui.horizontal(|ui| {
280 ui.label(
281 RichText::new(format!("Errors ({})", state.len()))
282 .strong()
283 .color(Self::header_color(&theme)),
284 );
285 ui.add_space(8.0);
286 if ui.small_button("Clear All").clicked() {
287 result = Some(ErrorConsoleMsg::DismissAll);
288 }
289 });
290
291 ui.add_space(4.0);
292
293 for (index, entry) in state.iter().enumerate() {
294 let (bg_color, text_color, icon) = Self::level_colors(entry.level, &theme);
295
296 egui::Frame::new()
297 .fill(bg_color)
298 .corner_radius(4.0)
299 .inner_margin(egui::Margin::symmetric(8, 4))
300 .show(ui, |ui| {
301 ui.horizontal(|ui| {
302 ui.label(
303 RichText::new(icon)
304 .family(FontFamily::Name("icons".into()))
305 .color(text_color),
306 );
307 ui.label(RichText::new(&entry.message).color(text_color));
308 ui.with_layout(
309 egui::Layout::right_to_left(egui::Align::Center),
310 |ui| {
311 if ui.small_button("×").clicked() && result.is_none() {
312 result = Some(ErrorConsoleMsg::Dismiss(index));
313 }
314 },
315 );
316 });
317 });
318
319 ui.add_space(2.0);
320 }
321 });
322
323 result
324 }
325}