duat_base/widgets/
log_book.rs1use std::sync::Mutex;
11
12use duat_core::{
13 context::{self, Handle, Logs, Record},
14 data::Pass,
15 mode::{MouseEvent, MouseEventKind},
16 opts::PrintOpts,
17 text::{Spacer, Text, TextMut, txt},
18 ui::{PushSpecs, PushTarget, Side, Widget},
19};
20
21#[allow(clippy::type_complexity)]
22static GLOBAL_FMT: Mutex<Option<Box<dyn FnMut(Record) -> Option<Text> + Send>>> = Mutex::new(None);
23
24pub struct LogBook {
26 logs: Logs,
27 len_of_taken: usize,
28 text: Text,
29 fmt: Option<Box<dyn FnMut(Record) -> Option<Text> + Send>>,
30 has_updated_once: bool,
31 pub close_on_unfocus: bool,
34 pub show_source: bool,
41}
42
43impl LogBook {
44 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
46 self.fmt = Some(Box::new(fmt))
47 }
48
49 pub fn builder() -> LogBookOpts {
51 LogBookOpts::default()
52 }
53}
54
55impl Widget for LogBook {
56 fn update(pa: &mut Pass, handle: &Handle<Self>) {
57 let (lb, area) = handle.write_with_area(pa);
58
59 let Some(new_records) = lb.logs.get(lb.len_of_taken..) else {
60 return;
61 };
62
63 let records_were_added = !new_records.is_empty();
64 lb.len_of_taken += new_records.len();
65
66 let fmt_recs = |fmt: &mut dyn FnMut(Record) -> Option<Text>| {
67 for rec_text in new_records.into_iter().filter_map(fmt) {
68 lb.text.insert_text(lb.text.len(), &rec_text);
69 }
70 };
71
72 let mut global_fmt = GLOBAL_FMT.lock().unwrap();
73
74 if let Some(fmt) = lb.fmt.as_mut() {
75 fmt_recs(fmt);
76 } else if let Some(fmt) = global_fmt.as_mut() {
77 fmt_recs(fmt);
78 } else {
79 fmt_recs(&mut |rec| default_fmt(lb.show_source, rec));
80 }
81
82 if !lb.has_updated_once {
83 area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
84 lb.has_updated_once = true;
85 } else if records_were_added {
86 area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
87 }
88 }
89
90 fn needs_update(&self, _: &Pass) -> bool {
91 self.logs.has_changed()
92 }
93
94 fn text(&self) -> &Text {
95 &self.text
96 }
97
98 fn text_mut(&mut self) -> TextMut<'_> {
99 self.text.as_mut()
100 }
101
102 fn get_print_opts(&self) -> PrintOpts {
103 let mut opts = PrintOpts::new();
104 opts.wrap_lines = true;
105 opts.wrap_on_word = true;
106 opts
107 }
108
109 fn on_focus(pa: &mut Pass, handle: &Handle<Self>) {
110 handle.area().reveal(pa).unwrap();
111 }
112
113 fn on_unfocus(pa: &mut Pass, handle: &Handle<Self>) {
114 if handle.read(pa).close_on_unfocus {
115 handle.area().hide(pa).unwrap()
116 }
117 }
118
119 fn on_mouse_event(pa: &mut Pass, handle: &Handle<Self>, event: MouseEvent) {
120 match event.kind {
121 MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => {
122 let (lb, area) = handle.write_with_area(pa);
123 let scroll = if let MouseEventKind::ScrollDown = event.kind {
124 3
125 } else {
126 -3
127 };
128 area.scroll_ver(&lb.text, scroll, lb.get_print_opts());
129 }
130 _ => {}
131 }
132 }
133}
134
135#[derive(Clone, Copy)]
137pub struct LogBookOpts {
138 pub close_on_unfocus: bool,
140 pub hidden: bool,
142 pub side: Side,
144 pub height: f32,
147 pub width: f32,
150 pub show_source: bool,
157}
158
159impl LogBookOpts {
160 pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
162 let logs = context::logs();
163
164 let mut text = Text::new();
165
166 let records = logs.get(..).unwrap();
167 let len_of_taken = records.len();
168
169 let fmt_recs = |fmt: &mut dyn FnMut(Record) -> Option<Text>| {
170 for rec_text in records.into_iter().filter_map(fmt) {
171 text.insert_text(text.len(), &rec_text);
172 }
173 };
174
175 let mut global_fmt = GLOBAL_FMT.lock().unwrap();
176
177 if let Some(fmt) = global_fmt.as_mut() {
178 fmt_recs(fmt);
179 } else {
180 fmt_recs(&mut |rec| default_fmt(self.show_source, rec));
181 }
182
183 let log_book = LogBook {
184 logs,
185 len_of_taken,
186 text,
187 has_updated_once: false,
188 fmt: None,
189 show_source: self.show_source,
190 close_on_unfocus: self.close_on_unfocus,
191 };
192 let specs = match self.side {
193 Side::Right | Side::Left => PushSpecs {
194 side: self.side,
195 width: Some(self.width),
196 hidden: self.hidden,
197 ..Default::default()
198 },
199 Side::Above | Side::Below => PushSpecs {
200 side: self.side,
201 height: Some(self.height),
202 hidden: self.hidden,
203 ..Default::default()
204 },
205 };
206
207 push_target.push_outer(pa, log_book, specs)
208 }
209
210 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
218 *GLOBAL_FMT.lock().unwrap() = Some(Box::new(fmt));
219 }
220}
221
222impl Default for LogBookOpts {
223 fn default() -> Self {
224 Self {
225 close_on_unfocus: true,
226 hidden: true,
227 side: Side::Below,
228 height: 8.0,
229 width: 50.0,
230 show_source: true,
231 }
232 }
233}
234
235fn default_fmt(show_source: bool, rec: Record) -> Option<Text> {
236 use duat_core::context::Level::*;
237 let mut builder = Text::builder();
238
239 match rec.level() {
240 Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]: ")),
241 Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
242 Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]: ")),
243 Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]: ")),
244 Trace => unreachable!("Trace is not meant to be useable"),
245 };
246
247 builder.push(txt!("[log_book.bracket][] {}", rec.text().clone(),));
248
249 if show_source {
250 builder.push(txt!(
251 "{Spacer}([log_book.location]{}[log_book.bracket])",
252 rec.location()
253 ));
254 }
255
256 builder.push('\n');
257
258 Some(builder.build())
259}