stereokit_rust/tools/
log_window.rs

1use crate::{
2    font::Font,
3    material::Cull,
4    maths::{Matrix, Pose, Vec2, Vec3, units::CM},
5    prelude::*,
6    system::{Align, LogItem, LogLevel, Pivot, Text, TextFit, TextStyle},
7    ui::{Ui, UiCut},
8    util::Color128,
9};
10use std::sync::Mutex;
11
12pub const SHOW_LOG_WINDOW: &str = "Tool_ShowLogWindow";
13
14/// A simple log window to display the logs.
15/// ### Fields that can be changed before initialization:
16/// * `log_log` - The log mutex to listen to.
17/// * `enabled` - Whether the tool is enabled or not at start.
18/// * `window_pose` - The pose where to show the log window.
19/// * `x_len` - The width in number of characters.
20/// * `y_len` - The height of the log window in number of lines.
21///
22/// ### Events this stepper is listening to:
23/// * `SHOW_LOG_WINDOW` - Event that triggers when the window is visible ("true") or hidden ("false").
24///
25/// ### Examples
26/// ```
27/// # stereokit_rust::test_init_sk!(); // !!!! Get a proper way to initialize sk !!!!
28/// use stereokit_rust::{maths::Vec3, ui::Ui,
29///                      tools::log_window::{LogWindow, basic_log_fmt, SHOW_LOG_WINDOW},
30///                      system::{LogLevel, LogItem,  Log}};
31/// use std::sync::Mutex;
32///
33/// // Somewhere to copy the log
34/// static LOG_LOG: Mutex<Vec<LogItem>> = Mutex::new(vec![]);
35/// let fn_mut = |level: LogLevel, log_text: &str| {
36///    let items = LOG_LOG.lock().unwrap();
37///    basic_log_fmt(level, log_text, 20, items);
38/// };
39/// Log::subscribe(fn_mut);
40/// let mut log_window = LogWindow::new(&LOG_LOG);
41/// log_window.window_pose = Ui::popup_pose([0.0, 0.04, 1.40]);
42/// log_window.x_len = 20.0;
43/// log_window.y_len = 4.0;
44///
45/// sk.send_event(StepperAction::add("LogWindow", log_window));
46///
47/// filename_scr = "screenshots/log_window.jpeg";
48/// test_screenshot!( // !!!! Get a proper main loop !!!!
49///     if iter == 0  {
50///         Log::info("Info log message");
51///         Log::warn("Warning log message");
52///         Log::err ("Error log message");
53///     } else  if iter == number_of_steps  {
54///        sk.send_event(StepperAction::event( "main", SHOW_LOG_WINDOW, "false",));
55///     }
56/// );
57/// ```
58/// <img src="https://raw.githubusercontent.com/mvvvv/StereoKit-rust/refs/heads/master/screenshots/log_window.jpeg" alt="screenshot" width="200">
59#[derive(IStepper)]
60pub struct LogWindow<'a> {
61    id: StepperId,
62    sk_info: Option<Rc<RefCell<SkInfo>>>,
63    pub enabled: bool,
64
65    pub window_pose: Pose,
66    pub x_len: f32,
67    pub y_len: f32,
68    style_diag: TextStyle,
69    style_info: TextStyle,
70    style_warn: TextStyle,
71    style_err: TextStyle,
72    pub log_log: &'a Mutex<Vec<LogItem>>,
73    log_index: f32,
74    items_size: usize,
75}
76
77unsafe impl Send for LogWindow<'_> {}
78
79impl<'a> LogWindow<'a> {
80    pub fn new(log_log: &'a Mutex<Vec<LogItem>>) -> Self {
81        let enabled = true;
82        let pose = Pose::IDENTITY;
83        let x_len = 110.0;
84        let y_len = 15.0;
85
86        let style_diag = TextStyle::from_font(Font::default(), 0.012, Color128::hsv(1.0, 0.0, 0.7, 1.0));
87        let style_info = TextStyle::from_font(Font::default(), 0.012, Color128::hsv(1.0, 0.0, 1.0, 1.0));
88        let style_warn = TextStyle::from_font(Font::default(), 0.012, Color128::hsv(0.17, 0.7, 1.0, 1.0));
89        let style_err = TextStyle::from_font(Font::default(), 0.012, Color128::hsv(1.0, 0.7, 0.7, 1.0));
90        for ui_text_style in [style_diag, style_info, style_warn, style_err] {
91            ui_text_style.get_material().face_cull(Cull::Back); //.depth_test(DepthTest::Less).depth_write(true);
92        }
93        Self {
94            id: "LogWindow".to_string(),
95            sk_info: None,
96            enabled,
97
98            window_pose: pose,
99            x_len,
100            y_len,
101            style_diag,
102            style_info,
103            style_warn,
104            style_err,
105            log_log,
106            log_index: 0.0,
107            items_size: 0,
108        }
109    }
110
111    /// Called from IStepper::initialize here you can abort the initialization by returning false
112    fn start(&mut self) -> bool {
113        true
114    }
115
116    /// Called from IStepper::step, here you can check the event report
117    fn check_event(&mut self, _id: &StepperId, key: &str, value: &str) {
118        if key.eq(SHOW_LOG_WINDOW) {
119            self.enabled = value.parse().unwrap_or(false)
120        }
121    }
122    /// Called from IStepper::step, after check_event here you can draw your UI
123    fn draw(&mut self, token: &MainThreadToken) {
124        Ui::window_begin("Log", &mut self.window_pose, Some(Vec2::new(self.x_len, 0.0) * CM), None, None);
125        self.draw_logs(token);
126        Ui::hseparator();
127        Ui::window_end();
128    }
129
130    fn draw_logs(&mut self, token: &MainThreadToken) {
131        let text_size = Vec2::new(Ui::get_layout_remaining().x, 0.024);
132        let items = self.log_log.lock().unwrap();
133
134        Ui::layout_push_cut(UiCut::Top, text_size.y * self.y_len, false);
135        Ui::layout_push_cut(UiCut::Right, Ui::get_line_height() * 0.6, false);
136
137        if self.items_size < items.len() {
138            self.items_size = items.len();
139            self.log_index = items.len() as f32;
140
141            // if self.log_index < self.y_len {
142            //     self.log_index = 0.0;
143            // }
144        }
145        if let Some(pos) =
146            Ui::vslider("scroll", &mut self.log_index, 0.0, items.len() as f32, Some(1.0), None, None, None)
147        {
148            self.log_index = f32::max(f32::min(pos, items.len() as f32 - 1.0), 0.0);
149        }
150
151        Ui::layout_pop();
152
153        let start = Ui::get_layout_at();
154        Ui::layout_reserve(Vec2::new(text_size.x, text_size.y * self.y_len), true, 0.0);
155
156        let mut index = (self.log_index - self.y_len) as i32;
157        let mut last_item_printed = self.log_index as i32;
158        if index < 0 {
159            index = 0;
160            last_item_printed = self.y_len as i32;
161        }
162        for i in index..last_item_printed {
163            if let Some(item) = items.get(i as usize) {
164                let ts = match item.level {
165                    LogLevel::Diagnostic => self.style_diag,
166                    LogLevel::Inform => self.style_info,
167                    LogLevel::Warning => self.style_warn,
168                    LogLevel::Error => self.style_err,
169                    _ => self.style_info,
170                };
171
172                let y = (i - index) as f32 * -text_size.y;
173                Text::add_in(
174                    token,
175                    item.text.trim(),
176                    Matrix::t(start + Vec3::new(0.0, y, -0.004)),
177                    text_size,
178                    TextFit::Clip | TextFit::Wrap,
179                    Some(ts),
180                    None,
181                    Some(Pivot::TopLeft),
182                    Some(Align::CenterLeft),
183                    None,
184                    None,
185                    None,
186                );
187
188                if item.count > 1 {
189                    let at = Vec3::new(start.x - text_size.x, start.y + y, start.z - 0.014);
190                    Text::add_in(
191                        token,
192                        item.count.to_string(),
193                        Matrix::t(at),
194                        Vec2::new(text_size.x + 0.22, text_size.y),
195                        TextFit::Clip,
196                        Some(self.style_info),
197                        None,
198                        Some(Pivot::TopLeft),
199                        Some(Align::CenterLeft),
200                        None,
201                        None,
202                        None,
203                    );
204                }
205            }
206        }
207        Ui::layout_pop();
208    }
209}
210
211/// A basic log formatter that splits long lines and counts repeated lines.
212/// * `level` - The log level.
213/// * `log_text` - The log text.
214/// * `line_len` - The maximum length of a line.
215pub fn basic_log_fmt(
216    level: LogLevel,
217    log_text: &str,
218    line_len: usize,
219    mut items: std::sync::MutexGuard<'_, Vec<LogItem>>,
220) {
221    for line_text in log_text.lines() {
222        let subs = line_text.as_bytes().chunks(line_len);
223        for (pos, sub_line) in subs.enumerate() {
224            if let Ok(mut sub_string) = String::from_utf8(sub_line.to_vec()) {
225                if pos > 0 {
226                    sub_string.insert_str(0, "»»»»");
227                }
228                if let Some(item) = items.last_mut() {
229                    if item.text == sub_string {
230                        item.count += 1;
231                        continue;
232                    }
233                }
234
235                items.push(LogItem { level, text: sub_string.to_owned(), count: 1 });
236            };
237        }
238    }
239}