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