bevy_yoleck/
console.rs

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/// Log level for console messages.
12#[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/// A single log entry captured from the tracing system.
41#[derive(Clone, Debug, Message)]
42pub struct LogEntry {
43    pub level: LogLevel,
44    pub message: String,
45    pub target: String,
46}
47
48/// Non-send resource containing the receiver for captured log messages.
49pub struct CapturedLogMessages(mpsc::Receiver<LogEntry>);
50
51/// Resource storing the history of log messages displayed in the console.
52#[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/// Resource containing the current state of the console UI.
85#[derive(Resource, Default)]
86pub struct YoleckConsoleState {
87    pub log_filters: LogFilters,
88}
89
90/// Filters for controlling which log levels are displayed in the console.
91#[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
121/// Creates a console panel section for displaying log messages in the editor UI.
122pub 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
163/// Tracing layer that captures log messages and sends them to the console.
164pub 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
237/// Factory function that creates and configures the console logging layer.
238///
239/// This function should be used with Bevy's `LogPlugin` to capture log messages
240/// and display them in the Yoleck editor console.
241///
242/// # Example
243///
244/// ```no_run
245/// # use bevy::{prelude::*, log::LogPlugin};
246/// # use bevy_yoleck::console_layer_factory;
247///
248/// fn main() {
249///     App::new()
250///         .add_plugins(DefaultPlugins.set(LogPlugin {
251///             custom_layer: console_layer_factory,
252///             ..default()
253///         }))
254///         .run();
255/// }
256/// ```
257pub 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}