imgui_log/
lib.rs

1/*!
2A logger that routes logs to an imgui window.
3
4Supports both standalone mode (hook into your ui yourself), and an amethyst-imgui system (automatically rendered every frame).
5
6# Setup
7
8Add this to your `Cargo.toml`
9
10```toml
11[dependencies]
12imgui-log = "0.1.0"
13```
14
15# Basic Example
16```no_run
17// Start the logger
18let log = imgui_log::init(); 
19
20// Create your UI
21let ui: imgui::Ui = ... ;
22
23// Render loop
24loop {
25    // Output some info
26    info!("Hello World");
27
28    // Draw to a window
29    let window = imgui::Window::new(im_str!("My Log"));
30    log.draw(&ui, window);
31}
32```
33
34# Configuring
35
36A default config is provided, but you are free to customize the
37format string, coloring, etc if desired.
38
39```no_run
40imgui_log::init_with_config(LoggerConfig::default()
41    .stdout(false)
42    .colors(LogColors {
43        trace: [1., 1., 1., 1.],
44        debug: [1., 1., 1., 1.],
45        info: [1., 1., 1., 1.],
46        warn: [1., 1., 1., 1.],
47        error: [1., 1., 1., 1.],
48    })
49);
50```
51
52# Amethyst usage
53
54Enable the `amethyst-system` feature.
55
56```toml
57[dependencies]
58imgui-log = { version = "0.1.0", features = ["amethyst-system"] }
59```
60
61Replace `imgui::init` with `imgui_log::create_system` and add it to your app's `.with()` statements
62
63Add the `RenderImgui` plugin if it is not already being used.
64(This is re-exported from the `amethyst-imgui` crate for your convenience)
65
66```no_run
67    use imgui_log::amethyst_imgui::RenderImgui;
68
69    /// ....
70
71    let app_root = application_root_dir()?;
72    let display_config_path = app_root.join("examples/display.ron");
73    let game_data = GameDataBuilder::default()
74        .with_barrier()
75        .with(imgui_log::create_system(), "imgui_log", &[]) // <--- ADDED LINE 
76        .with_bundle(InputBundle::<StringBindings>::default())?
77        .with_bundle(
78            RenderingBundle::<DefaultBackend>::new()
79                .with_plugin(
80                    RenderToWindow::from_config_path(display_config_path)
81                        .with_clear([0.34, 0.36, 0.52, 1.0]),
82                )
83                .with_plugin(RenderImgui::<StringBindings>::default()), // <--- ADDED LINE
84        )?;
85
86    Application::build("/", Example)?.build(game_data)?.run();
87```
88
89*/
90
91#[cfg(feature = "amethyst-system")]
92mod amethyst;
93
94#[cfg(feature = "amethyst-system")]
95pub use crate::amethyst::*;
96
97use imgui::im_str;
98use log::{Level, LevelFilter, Record};
99use std::sync::mpsc;
100
101/// A single line of formatted text
102///
103/// Call `.to_string()` if needed.
104/// level can be used to visually mark certian lines.
105pub struct LogLine {
106    pub level: log::Level,
107    pub text: String,
108}
109
110impl std::fmt::Display for LogLine {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        write!(f, "{}", self.text)
113    }
114}
115
116fn default_formatter(record: &Record) -> String {
117    let msg = record.args().to_string();
118    if let (Some(file), Some(line)) = (record.file(), record.line()) {
119        format!("{}:{} --- {}: {}\n", file, line, record.level(), msg)
120    } else {
121        format!("{} --- {}: {}\n", record.target(), record.level(), msg)
122    }
123}
124
125/// Backend for the log crate facade
126///
127/// Formats strings then passes them to a chaenel to be displayed in the gui,
128/// this avoids threading issues (logging must be Send+Sync).
129pub struct ChanneledLogger {
130    channel: mpsc::SyncSender<LogLine>,
131    formatter: Box<dyn (Fn(&Record) -> String) + Send + Sync>,
132    stdout: bool,
133}
134
135impl log::Log for ChanneledLogger {
136    fn enabled(&self, metadata: &log::Metadata) -> bool {
137        // TODO: filter by module
138        metadata.level() <= Level::Debug
139    }
140
141    fn log(&self, record: &Record) {
142        if self.enabled(record.metadata()) {
143            let text = (self.formatter)(record);
144
145            if self.stdout {
146                // TODO: Console coloring
147                print!("{}", text);
148            }
149
150            // TODO: File logging
151
152            let line = LogLine {
153                text,
154                level: record.level(),
155            };
156            let _ = self.channel.try_send(line);
157        }
158    }
159
160    fn flush(&self) {}
161}
162
163/// Colors used by LogWindow when rendering
164#[derive(Clone, Copy)]
165pub struct LogColors {
166    pub trace: [f32; 4],
167    pub debug: [f32; 4],
168    pub info: [f32; 4],
169    pub warn: [f32; 4],
170    pub error: [f32; 4],
171}
172
173impl Default for LogColors {
174    fn default() -> Self {
175        LogColors {
176            trace: [0., 1., 0., 1.],
177            debug: [0., 0., 1., 1.],
178            info: [1., 1., 1., 1.],
179            warn: [1., 1., 0., 1.],
180            error: [1., 0., 0., 1.],
181        }
182    }
183}
184
185impl LogColors {
186    pub fn level(&self, level: Level) -> [f32; 4] {
187        match level {
188            Level::Trace => self.trace,
189            Level::Debug => self.debug,
190            Level::Info => self.info,
191            Level::Warn => self.warn,
192            Level::Error => self.error,
193        }
194    }
195}
196
197/// The imgui frontend for ChanneledLogger.
198/// Call `build` during your rendering stage
199pub struct LogWindow {
200    buf: Vec<LogLine>,
201    channel: mpsc::Receiver<LogLine>,
202    autoscroll: bool,
203    colors: LogColors,
204}
205
206impl LogWindow {
207    pub fn new(channel: mpsc::Receiver<LogLine>) -> Self {
208        LogWindow {
209            buf: vec![],
210            channel,
211            autoscroll: false,
212            colors: LogColors::default(),
213        }
214    }
215}
216
217impl LogWindow {
218    fn sync(&mut self) {
219        while let Ok(line) = self.channel.try_recv() {
220            self.buf.push(line);
221        }
222    }
223
224    pub fn clear(&mut self) {
225        self.buf.clear();
226    }
227
228    pub fn set_colors(&mut self, colors: LogColors) {
229        self.colors = colors;
230    }
231
232    pub fn build(&mut self, ui: &imgui::Ui, window: imgui::Window) {
233        self.sync();
234        window.build(ui, || {
235            ui.popup(im_str!("Options"), || {
236                ui.checkbox(im_str!("Auto-scroll"), &mut self.autoscroll);
237            });
238
239            if ui.button(im_str!("Options"), [0., 0.]) {
240                ui.open_popup(im_str!("Options"));
241            }
242            ui.same_line(0.);
243            let clear = ui.button(im_str!("Clear"), [0., 0.]);
244            ui.same_line(0.);
245            let copy = ui.button(im_str!("Copy"), [0., 0.]);
246
247            ui.separator();
248            let child = imgui::ChildWindow::new(imgui::Id::Str("scrolling"))
249                .size([0., 0.])
250                .horizontal_scrollbar(true);
251            child.build(ui, || {
252                if clear {
253                    self.clear();
254                }
255                let buf = &mut self.buf;
256                if copy {
257                    ui.set_clipboard_text(&imgui::ImString::new(
258                        buf.iter()
259                            .map(|l| l.to_string())
260                            .collect::<Vec<String>>()
261                            .join("\n"),
262                    ));
263                }
264
265                let style = ui.push_style_var(imgui::StyleVar::ItemSpacing([0., 0.]));
266
267                for record in buf {
268                    ui.text_colored(self.colors.level(record.level), &record.text);
269                }
270
271                style.pop(ui);
272
273                if self.autoscroll || ui.scroll_y() >= ui.scroll_max_y() {
274                    ui.set_scroll_here_y_with_ratio(1.0);
275                }
276            });
277        });
278    }
279}
280
281/// ChanneledLogger builder
282///
283/// Use `LoggerConfig::default()` to intialize.
284///
285/// Call `.build()` to finalize.
286pub struct LoggerConfig {
287    formatter: Option<Box<dyn (Fn(&Record) -> String) + Send + Sync>>,
288    colors: Option<LogColors>,
289    stdout: bool,
290}
291
292impl Default for LoggerConfig {
293    fn default() -> Self {
294        LoggerConfig {
295            formatter: None,
296            colors: None,
297            stdout: true,
298        }
299    }
300}
301
302impl LoggerConfig {
303    pub fn formatter(mut self, formatter: fn(&Record) -> String) -> Self {
304        self.formatter = Some(Box::new(formatter));
305        self
306    }
307
308    pub fn colors(mut self, colors: LogColors) -> Self {
309        self.colors = Some(colors);
310        self
311    }
312
313    pub fn stdout(mut self, stdout: bool) -> Self {
314        self.stdout = stdout;
315        self
316    }
317
318    pub fn build(self, channel: mpsc::SyncSender<LogLine>) -> ChanneledLogger {
319        let formatter = {
320            if let Some(f) = self.formatter {
321                f
322            } else {
323                Box::new(default_formatter)
324            }
325        };
326
327        ChanneledLogger {
328            channel,
329            formatter,
330            stdout: self.stdout,
331        }
332    }
333}
334
335/// Hook into the log system.
336/// This consumes the ChanneledLogger. Edit any configurations before this.
337fn set_logger(logger: ChanneledLogger) -> Result<(), log::SetLoggerError> {
338    log::set_boxed_logger(Box::new(logger)).map(|()| log::set_max_level(LevelFilter::Debug))
339}
340
341/// Create a window and initialize the logging backend.
342/// Be sure to call build on the returned window during your rendering stage
343pub fn init_with_config(config: LoggerConfig) -> LogWindow {
344    let (log_writer, log_reader) = mpsc::sync_channel(128);
345
346    let mut window = LogWindow::new(log_reader);
347    if let Some(colors) = config.colors {
348        window.set_colors(colors);
349    }
350
351    let logger = config.build(log_writer);
352    set_logger(logger).unwrap();
353
354    window
355}
356
357/// Create a window and initialize the logging backend with the default config.
358/// Be sure to call build on the returned window during your rendering stage
359pub fn init() -> LogWindow {
360    init_with_config(LoggerConfig::default())
361}