use crate::atoms::icons;
use crate::Theme;
use egui::{Color32, FontFamily, RichText, Ui};
use egui_cha::{Severity, ViewCtx};
use std::collections::VecDeque;
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct ErrorEntry {
pub message: String,
pub timestamp: Instant,
pub level: ErrorLevel,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ErrorLevel {
Debug,
Info,
Warning,
#[default]
Error,
Critical,
}
impl From<Severity> for ErrorLevel {
fn from(severity: Severity) -> Self {
match severity {
Severity::Debug => ErrorLevel::Debug,
Severity::Info => ErrorLevel::Info,
Severity::Warn => ErrorLevel::Warning,
Severity::Error => ErrorLevel::Error,
Severity::Critical => ErrorLevel::Critical,
}
}
}
impl ErrorLevel {
pub fn is_production_visible(self) -> bool {
self >= ErrorLevel::Info
}
}
pub struct ErrorConsoleState {
errors: VecDeque<ErrorEntry>,
max_entries: usize,
auto_dismiss: Option<Duration>,
}
impl Default for ErrorConsoleState {
fn default() -> Self {
Self::new()
}
}
impl ErrorConsoleState {
pub fn new() -> Self {
Self {
errors: VecDeque::new(),
max_entries: 10,
auto_dismiss: Some(Duration::from_secs(10)),
}
}
pub fn with_max_entries(mut self, max: usize) -> Self {
self.max_entries = max;
self
}
pub fn with_auto_dismiss(mut self, duration: Option<Duration>) -> Self {
self.auto_dismiss = duration;
self
}
pub fn push(&mut self, message: impl Into<String>) {
self.push_with_level(message, ErrorLevel::Error);
}
pub fn push_warning(&mut self, message: impl Into<String>) {
self.push_with_level(message, ErrorLevel::Warning);
}
pub fn push_info(&mut self, message: impl Into<String>) {
self.push_with_level(message, ErrorLevel::Info);
}
pub fn push_with_level(&mut self, message: impl Into<String>, level: ErrorLevel) {
self.errors.push_back(ErrorEntry {
message: message.into(),
timestamp: Instant::now(),
level,
});
while self.errors.len() > self.max_entries {
self.errors.pop_front();
}
}
pub fn cleanup(&mut self) {
if let Some(duration) = self.auto_dismiss {
let now = Instant::now();
self.errors
.retain(|e| now.duration_since(e.timestamp) < duration);
}
}
pub fn clear(&mut self) {
self.errors.clear();
}
pub fn dismiss(&mut self, index: usize) {
if index < self.errors.len() {
self.errors.remove(index);
}
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn iter(&self) -> impl Iterator<Item = &ErrorEntry> {
self.errors.iter()
}
pub fn drain(&mut self) -> impl Iterator<Item = ErrorEntry> + '_ {
self.errors.drain(..)
}
}
#[derive(Clone, Debug)]
pub enum ErrorConsoleMsg {
Dismiss(usize),
DismissAll,
}
pub struct ErrorConsole;
impl ErrorConsole {
fn level_colors(level: ErrorLevel, theme: &Theme) -> (Color32, Color32, &'static str) {
let (log_color, icon) = match level {
ErrorLevel::Debug => (theme.log_debug, icons::WRENCH),
ErrorLevel::Info => (theme.log_info, icons::INFO),
ErrorLevel::Warning => (theme.log_warn, icons::WARNING),
ErrorLevel::Error => (theme.log_error, icons::X_CIRCLE),
ErrorLevel::Critical => (theme.log_critical, icons::FIRE),
};
let bg_color = Color32::from_rgba_unmultiplied(
log_color.r(),
log_color.g(),
log_color.b(),
30, );
(bg_color, log_color, icon)
}
fn header_color(theme: &Theme) -> Color32 {
theme.log_error
}
pub fn show<Msg>(
ctx: &mut ViewCtx<'_, Msg>,
state: &ErrorConsoleState,
map_msg: impl Fn(ErrorConsoleMsg) -> Msg + Clone,
) {
if state.is_empty() {
return;
}
let theme = Theme::current(ctx.ui.ctx());
let mut dismiss_index: Option<usize> = None;
let mut clear_all = false;
ctx.ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new(format!("Errors ({})", state.len()))
.strong()
.color(Self::header_color(&theme)),
);
ui.add_space(8.0);
if ui.small_button("Clear All").clicked() {
clear_all = true;
}
});
ui.add_space(4.0);
for (index, entry) in state.iter().enumerate() {
let (bg_color, text_color, icon) = Self::level_colors(entry.level, &theme);
egui::Frame::new()
.fill(bg_color)
.corner_radius(4.0)
.inner_margin(egui::Margin::symmetric(8, 4))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new(icon)
.family(FontFamily::Name("icons".into()))
.color(text_color),
);
ui.label(RichText::new(&entry.message).color(text_color));
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
if ui.small_button("×").clicked() {
dismiss_index = Some(index);
}
},
);
});
});
ui.add_space(2.0);
}
});
if clear_all {
ctx.emit(map_msg(ErrorConsoleMsg::DismissAll));
} else if let Some(index) = dismiss_index {
ctx.emit(map_msg(ErrorConsoleMsg::Dismiss(index)));
}
}
pub fn show_ui(ui: &mut Ui, state: &ErrorConsoleState) -> Option<ErrorConsoleMsg> {
if state.is_empty() {
return None;
}
let theme = Theme::current(ui.ctx());
let mut result = None;
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new(format!("Errors ({})", state.len()))
.strong()
.color(Self::header_color(&theme)),
);
ui.add_space(8.0);
if ui.small_button("Clear All").clicked() {
result = Some(ErrorConsoleMsg::DismissAll);
}
});
ui.add_space(4.0);
for (index, entry) in state.iter().enumerate() {
let (bg_color, text_color, icon) = Self::level_colors(entry.level, &theme);
egui::Frame::new()
.fill(bg_color)
.corner_radius(4.0)
.inner_margin(egui::Margin::symmetric(8, 4))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(
RichText::new(icon)
.family(FontFamily::Name("icons".into()))
.color(text_color),
);
ui.label(RichText::new(&entry.message).color(text_color));
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
if ui.small_button("×").clicked() && result.is_none() {
result = Some(ErrorConsoleMsg::Dismiss(index));
}
},
);
});
});
ui.add_space(2.0);
}
});
result
}
}