1use bevy::log::BoxedLayer;
2use bevy::log::tracing;
3use bevy::log::tracing_subscriber;
4use bevy::prelude::*;
5use bevy_egui::egui;
6use std::collections::VecDeque;
7use std::sync::mpsc;
8
9use crate::editor_panels::YoleckPanelUi;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum LogLevel {
14 Debug,
15 Info,
16 Warn,
17 Error,
18}
19
20impl LogLevel {
21 pub fn color(&self) -> egui::Color32 {
22 match self {
23 LogLevel::Debug => egui::Color32::LIGHT_GRAY,
24 LogLevel::Info => egui::Color32::WHITE,
25 LogLevel::Warn => egui::Color32::from_rgb(255, 200, 0),
26 LogLevel::Error => egui::Color32::from_rgb(255, 100, 100),
27 }
28 }
29
30 pub fn label(&self) -> &str {
31 match self {
32 LogLevel::Debug => "DEBUG",
33 LogLevel::Info => "INFO",
34 LogLevel::Warn => "WARN",
35 LogLevel::Error => "ERROR",
36 }
37 }
38}
39
40#[derive(Clone, Debug, Message)]
42pub struct LogEntry {
43 pub level: LogLevel,
44 pub message: String,
45 pub target: String,
46}
47
48pub struct CapturedLogMessages(mpsc::Receiver<LogEntry>);
50
51#[derive(Resource)]
53pub struct YoleckConsoleLogHistory {
54 pub logs: VecDeque<LogEntry>,
55 pub max_logs: usize,
56}
57
58impl YoleckConsoleLogHistory {
59 pub fn new(max_logs: usize) -> Self {
60 Self {
61 logs: VecDeque::with_capacity(max_logs),
62 max_logs,
63 }
64 }
65
66 pub fn add_log(&mut self, entry: LogEntry) {
67 if self.logs.len() >= self.max_logs {
68 self.logs.pop_front();
69 }
70 self.logs.push_back(entry);
71 }
72
73 pub fn clear(&mut self) {
74 self.logs.clear();
75 }
76}
77
78impl Default for YoleckConsoleLogHistory {
79 fn default() -> Self {
80 Self::new(1000)
81 }
82}
83
84#[derive(Resource, Default)]
86pub struct YoleckConsoleState {
87 pub log_filters: LogFilters,
88}
89
90#[derive(Resource)]
92pub struct LogFilters {
93 pub show_debug: bool,
94 pub show_info: bool,
95 pub show_warn: bool,
96 pub show_error: bool,
97}
98
99impl Default for LogFilters {
100 fn default() -> Self {
101 Self {
102 show_debug: false,
103 show_info: true,
104 show_warn: true,
105 show_error: true,
106 }
107 }
108}
109
110impl LogFilters {
111 pub fn should_show(&self, level: LogLevel) -> bool {
112 match level {
113 LogLevel::Debug => self.show_debug,
114 LogLevel::Info => self.show_info,
115 LogLevel::Warn => self.show_warn,
116 LogLevel::Error => self.show_error,
117 }
118 }
119}
120
121pub fn console_panel_section(
123 mut ui: ResMut<YoleckPanelUi>,
124 mut console_state: ResMut<YoleckConsoleState>,
125 mut log_history: ResMut<YoleckConsoleLogHistory>,
126) -> Result {
127 ui.horizontal(|ui| {
128 ui.label("Filters:");
129
130 ui.checkbox(&mut console_state.log_filters.show_debug, "DEBUG");
131 ui.checkbox(&mut console_state.log_filters.show_info, "INFO");
132 ui.checkbox(&mut console_state.log_filters.show_warn, "WARN");
133 ui.checkbox(&mut console_state.log_filters.show_error, "ERROR");
134
135 ui.separator();
136
137 if ui.button("Clear").clicked() {
138 log_history.clear();
139 }
140 });
141
142 ui.separator();
143
144 egui::ScrollArea::vertical()
145 .auto_shrink([false, false])
146 .stick_to_bottom(true)
147 .show(&mut ui, |ui| {
148 for log in log_history
149 .logs
150 .iter()
151 .filter(|log| console_state.log_filters.should_show(log.level))
152 {
153 ui.horizontal_wrapped(|ui| {
154 ui.colored_label(log.level.color(), format!("[{}]", log.level.label()));
155 ui.label(&log.message);
156 });
157 }
158 });
159
160 Ok(())
161}
162
163pub struct YoleckConsoleLayer {
165 sender: mpsc::Sender<LogEntry>,
166}
167
168impl YoleckConsoleLayer {
169 pub fn new(sender: mpsc::Sender<LogEntry>) -> Self {
170 Self { sender }
171 }
172}
173
174impl<S> tracing_subscriber::Layer<S> for YoleckConsoleLayer
175where
176 S: tracing::Subscriber,
177{
178 fn on_event(
179 &self,
180 event: &tracing::Event<'_>,
181 _ctx: tracing_subscriber::layer::Context<'_, S>,
182 ) {
183 let metadata = event.metadata();
184 let level = match *metadata.level() {
185 tracing::Level::TRACE => return,
186 tracing::Level::DEBUG => LogLevel::Debug,
187 tracing::Level::INFO => LogLevel::Info,
188 tracing::Level::WARN => LogLevel::Warn,
189 tracing::Level::ERROR => LogLevel::Error,
190 };
191
192 let mut visitor = MessageVisitor::default();
193 event.record(&mut visitor);
194
195 if let Some(message) = visitor.message {
196 let _ = self.sender.send(LogEntry {
197 level,
198 message,
199 target: metadata.target().to_string(),
200 });
201 }
202 }
203}
204
205#[derive(Default)]
206struct MessageVisitor {
207 message: Option<String>,
208}
209
210impl tracing::field::Visit for MessageVisitor {
211 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
212 if field.name() == "message" {
213 self.message = Some(format!("{:?}", value).trim_matches('"').to_string());
214 }
215 }
216}
217
218fn transfer_log_messages(
219 receiver: NonSend<CapturedLogMessages>,
220 mut message_writer: MessageWriter<LogEntry>,
221) {
222 message_writer.write_batch(receiver.0.try_iter());
223}
224
225fn store_log_messages(
226 mut log_reader: MessageReader<LogEntry>,
227 log_history: Option<ResMut<YoleckConsoleLogHistory>>,
228) {
229 let Some(mut log_history) = log_history else {
230 return;
231 };
232 for log in log_reader.read() {
233 log_history.add_log(log.clone());
234 }
235}
236
237pub fn console_layer_factory(app: &mut App) -> Option<BoxedLayer> {
258 let (sender, receiver) = mpsc::channel();
259
260 let layer = YoleckConsoleLayer::new(sender);
261 let resource = CapturedLogMessages(receiver);
262
263 app.insert_non_send_resource(resource);
264 app.add_message::<LogEntry>();
265 app.add_systems(Update, (transfer_log_messages, store_log_messages).chain());
266
267 Some(Box::new(layer))
268}