duat_base/widgets/
log_book.rs

1//! Widget to display the history of notifications sent to Duat
2//!
3//! This widget is kind of an extended version of the
4//! [`Notifications`] widget. It is normally placed at the bottom of
5//! the screen, but you can also place it somewhere else. The messages
6//! can be formatted differently, and you can also filter the things
7//! that you don't care about.
8//!
9//! [`Notifications`]: super::Notifications
10use duat_core::{
11    context::{self, Handle, Logs, Record},
12    data::Pass,
13    mode::{MouseEvent, MouseEventKind},
14    opts::PrintOpts,
15    text::{Spacer, Text, txt},
16    ui::{PushSpecs, PushTarget, Side, Widget},
17};
18
19/// A [`Widget`] to display [`Logs`] sent to Duat
20pub struct LogBook {
21    logs: Logs,
22    len_of_taken: usize,
23    text: Text,
24    fmt: Box<dyn FnMut(Record) -> Option<Text> + Send>,
25    has_updated_once: bool,
26    /// Wether to close this [`Widget`] after unfocusing, `true` by
27    /// default
28    pub close_on_unfocus: bool,
29}
30
31impl LogBook {
32    /// Reformats this `LogBook`
33    pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
34        self.fmt = Box::new(fmt)
35    }
36
37    /// Returns a [`LogBookOpts`], so you can push `LogBook`s around
38    pub fn builder() -> LogBookOpts {
39        LogBookOpts::default()
40    }
41}
42
43impl Widget for LogBook {
44    fn update(pa: &mut Pass, handle: &Handle<Self>) {
45        let (lb, area) = handle.write_with_area(pa);
46
47        let Some(new_records) = lb.logs.get(lb.len_of_taken..) else {
48            return;
49        };
50
51        let records_were_added = !new_records.is_empty();
52        lb.len_of_taken += new_records.len();
53
54        for rec_text in new_records.into_iter().filter_map(&mut lb.fmt) {
55            lb.text.insert_text(lb.text.len(), &rec_text);
56        }
57
58        if !lb.has_updated_once {
59            area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
60            lb.has_updated_once = true;
61        } else if records_were_added {
62            area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
63        }
64    }
65
66    fn needs_update(&self, _: &Pass) -> bool {
67        self.logs.has_changed()
68    }
69
70    fn text(&self) -> &Text {
71        &self.text
72    }
73
74    fn text_mut(&mut self) -> &mut Text {
75        &mut self.text
76    }
77
78    fn get_print_opts(&self) -> PrintOpts {
79        let mut opts = PrintOpts::new();
80        opts.wrap_lines = true;
81        opts.wrap_on_word = true;
82        opts
83    }
84
85    fn on_focus(pa: &mut Pass, handle: &Handle<Self>) {
86        handle.area().reveal(pa).unwrap();
87    }
88
89    fn on_unfocus(pa: &mut Pass, handle: &Handle<Self>) {
90        if handle.read(pa).close_on_unfocus {
91            handle.area().hide(pa).unwrap()
92        }
93    }
94
95    fn on_mouse_event(pa: &mut Pass, handle: &Handle<Self>, event: MouseEvent) {
96        match event.kind {
97            MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
98                let (lb, area) = handle.write_with_area(pa);
99                let scroll = if let MouseEventKind::ScrollDown = event.kind {
100                    3
101                } else {
102                    -3
103                };
104                area.scroll_ver(&lb.text, scroll, lb.get_print_opts());
105            }
106            _ => {}
107        }
108    }
109}
110
111/// Configuration for the [`LogBook`]
112pub struct LogBookOpts {
113    fmt: Box<dyn FnMut(Record) -> Option<Text> + Send>,
114    /// Wether to close the `LogBook` when unfocusing
115    pub close_on_unfocus: bool,
116    /// Wether to hide the `LogBook` by default
117    pub hidden: bool,
118    /// To which side to push the [`LogBook`] to
119    pub side: Side,
120    /// Requested height for the [`LogBook`], ignored if pushing
121    /// horizontally
122    pub height: f32,
123    /// Requested width for the [`LogBook`], ignored if pushing
124    /// vertically
125    pub width: f32,
126}
127
128impl LogBookOpts {
129    /// Push a [`LogBook`] around the given [`PushTarget`]
130    pub fn push_on(mut self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
131        let logs = context::logs();
132
133        let mut text = Text::new();
134
135        let records = logs.get(..).unwrap();
136        let len_of_taken = records.len();
137        for rec_text in records.into_iter().filter_map(&mut self.fmt) {
138            text.insert_text(text.len(), &rec_text);
139        }
140
141        let log_book = LogBook {
142            logs,
143            len_of_taken,
144            text,
145            has_updated_once: false,
146            fmt: self.fmt,
147            close_on_unfocus: self.close_on_unfocus,
148        };
149        let specs = match self.side {
150            Side::Right | Side::Left => PushSpecs {
151                side: self.side,
152                width: Some(self.width),
153                hidden: self.hidden,
154                ..Default::default()
155            },
156            Side::Above | Side::Below => PushSpecs {
157                side: self.side,
158                height: Some(self.height),
159                hidden: self.hidden,
160                ..Default::default()
161            },
162        };
163
164        push_target.push_outer(pa, log_book, specs)
165    }
166
167    /// Changes the way [`Record`]s are formatted by the [`LogBook`]
168    ///
169    /// This function returns an [`Option<Text>`], which means you can
170    /// filter out unnecessary [`Record`]s. By default, all valid
171    /// [`Record`]s (those with level [`Debug`] or higher.
172    ///
173    /// [`Debug`]: context::Level::Debug
174    pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
175        self.fmt = Box::new(fmt);
176    }
177}
178
179impl Default for LogBookOpts {
180    fn default() -> Self {
181        fn default_fmt(rec: Record) -> Option<Text> {
182            use duat_core::context::Level::*;
183            let mut builder = Text::builder();
184
185            match rec.level() {
186                Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]:  ")),
187                Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
188                Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]:   ")),
189                Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]:  ")),
190                Trace => unreachable!("Trace is not meant to be useable"),
191            };
192
193            builder.push(txt!(
194                "[log_book.bracket][] {}{Spacer}([log_book.location]{}[log_book.bracket])\n",
195                rec.text().clone(),
196                rec.location(),
197            ));
198
199            Some(builder.build())
200        }
201
202        Self {
203            fmt: Box::new(default_fmt),
204            close_on_unfocus: true,
205            hidden: true,
206            side: Side::Below,
207            height: 8.0,
208            width: 50.0,
209        }
210    }
211}