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