1use std::{
2 collections::BTreeMap,
3 sync::{
4 Mutex,
5 atomic::{AtomicBool, Ordering},
6 },
7};
8
9use ecolor::Color32;
10use egui::{RichText, TextWrapMode};
11use egui_extras::{Column, TableBuilder, TableRow};
12use eyre::Result;
13use tracing::{
14 Level,
15 field::{Field, Visit},
16};
17use tracing_subscriber::Layer;
18
19use crate::{SystemState, message::Message};
20
21static RECORD_MUTEX: Mutex<Vec<LogMessage>> = Mutex::new(vec![]);
22static SHOW_LOGS_ON_ERROR: AtomicBool = AtomicBool::new(false);
23static LOG_FILTER: Mutex<(bool, bool, bool, bool, bool)> =
24 Mutex::new((true, true, true, true, true));
25#[macro_export]
26macro_rules! try_log_error {
27 ($expr:expr, $what:expr $(,)?) => {
28 if let Err(e) = $expr {
29 error!("{}: {}", $what, e)
30 }
31 };
32}
33
34#[derive(Clone)]
35pub struct LogMessage {
36 pub name: String,
37 pub msg: String,
38 pub level: Level,
39}
40
41struct EguiLogger {}
42
43struct FieldVisitor<'a>(&'a mut BTreeMap<String, String>);
44
45impl Visit for FieldVisitor<'_> {
46 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
47 self.0
48 .insert(field.name().to_string(), format!("{value:?}"));
49 }
50}
51
52impl<S> Layer<S> for EguiLogger
53where
54 S: tracing::Subscriber,
55{
56 fn on_event(
57 &self,
58 event: &tracing::Event<'_>,
59 _ctx: tracing_subscriber::layer::Context<'_, S>,
60 ) {
61 let mut fields = BTreeMap::new();
62 event.record(&mut FieldVisitor(&mut fields));
63
64 RECORD_MUTEX
65 .lock()
66 .expect("Failed to lock logger. Thread poisoned?")
67 .push(LogMessage {
68 name: event.metadata().module_path().unwrap_or("-").to_string(),
69 msg: fields.get("message").cloned().unwrap_or("-".to_string()),
70 level: *event.metadata().level(),
71 });
72
73 if *event.metadata().level() == Level::ERROR {
74 SHOW_LOGS_ON_ERROR.store(true, Ordering::Release);
75 if let Some(ctx) = crate::EGUI_CONTEXT.read().unwrap().as_ref() {
76 ctx.request_repaint();
77 }
78 }
79 }
80}
81
82pub(crate) fn take_error_notification() -> bool {
83 SHOW_LOGS_ON_ERROR.swap(false, Ordering::AcqRel)
84}
85
86impl SystemState {
87 pub fn draw_log_window(&self, ctx: &egui::Context, msgs: &mut Vec<Message>) {
88 let mut open = true;
89 egui::Window::new("Logs")
90 .open(&mut open)
91 .collapsible(true)
92 .resizable(true)
93 .show(ctx, |ui| {
94 {
95 let mut filters = LOG_FILTER.lock().unwrap();
96 ui.horizontal(|ui| {
97 ui.checkbox(&mut filters.0, "Error");
98 ui.checkbox(&mut filters.1, "Warn");
99 ui.checkbox(&mut filters.2, "Info");
100 ui.checkbox(&mut filters.3, "Debug");
101 ui.checkbox(&mut filters.4, "Trace");
102 });
103 }
104
105 ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
106
107 egui::ScrollArea::new([true, false]).show(ui, |ui| {
108 TableBuilder::new(ui)
109 .column(Column::auto().resizable(true))
110 .column(Column::auto().resizable(true))
111 .column(Column::remainder())
112 .vscroll(true)
113 .stick_to_bottom(true)
114 .header(20.0, |mut header| {
115 header.col(|ui| {
116 ui.heading("Level");
117 });
118 header.col(|ui| {
119 ui.heading("Source");
120 });
121 header.col(|ui| {
122 ui.heading("Message");
123 });
124 })
125 .body(|body| {
126 let records = RECORD_MUTEX.lock().unwrap();
127 let filters = LOG_FILTER.lock().unwrap();
128 let filtered: Vec<&LogMessage> = records
129 .iter()
130 .filter(|record| match record.level {
131 Level::ERROR => filters.0,
132 Level::WARN => filters.1,
133 Level::INFO => filters.2,
134 Level::DEBUG => filters.3,
135 Level::TRACE => filters.4,
136 })
137 .collect();
138
139 let heights = filtered
140 .iter()
141 .map(|record| record.msg.lines().count() as f32 * 15.0)
142 .collect::<Vec<_>>();
143
144 body.heterogeneous_rows(heights.into_iter(), |mut row: TableRow| {
145 let record = filtered[row.index()];
146 row.col(|ui| {
147 let (color, text) = match record.level {
148 Level::ERROR => (Color32::RED, "Error"),
149 Level::WARN => (Color32::YELLOW, "Warn"),
150 Level::INFO => (Color32::GREEN, "Info"),
151 Level::DEBUG => (Color32::BLUE, "Debug"),
152 Level::TRACE => (Color32::GRAY, "Trace"),
153 };
154
155 ui.colored_label(color, text);
156 });
157 row.col(|ui| {
158 ui.label(
159 RichText::new(record.name.clone())
160 .color(Color32::GRAY)
161 .monospace(),
162 );
163 });
164 row.col(|ui| {
165 ui.label(RichText::new(record.msg.clone()).monospace());
166 });
167 });
168 });
169 })
170 });
171 if !open {
172 msgs.push(Message::SetLogsVisible(false));
173 }
174 }
175}
176
177#[cfg(not(target_arch = "wasm32"))]
181pub fn start_logging() -> Result<()> {
182 use std::io::stdout;
183
184 use tracing_subscriber::{Registry, fmt, layer::SubscriberExt};
185
186 let filter =
187 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
188 let subscriber = Registry::default()
189 .with(
190 fmt::layer()
191 .without_time()
192 .with_writer(stdout)
193 .with_filter(filter.clone()),
194 )
195 .with(EguiLogger {}.with_filter(filter));
196
197 tracing::subscriber::set_global_default(subscriber).expect("unable to set global subscriber");
198
199 Ok(())
200}
201
202#[cfg(target_arch = "wasm32")]
206pub fn start_logging() -> Result<()> {
207 use tracing_subscriber::{Registry, fmt, layer::SubscriberExt};
208 use wasm_tracing::WasmLayer;
209
210 let filter =
211 tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into());
212 let subscriber = Registry::default()
213 .with(fmt::layer().without_time().with_filter(filter.clone()))
214 .with(WasmLayer::default())
215 .with(EguiLogger {}.with_filter(filter));
216
217 tracing::subscriber::set_global_default(subscriber).expect("unable to set global subscriber");
218
219 Ok(())
220}