duat_base/widgets/
log_book.rs1use duat_core::{
11 context::{self, Handle, Logs, Record},
12 data::Pass,
13 opts::PrintOpts,
14 text::{Spacer, Text, txt},
15 ui::{PushSpecs, PushTarget, Side, Widget},
16};
17
18pub struct LogBook {
20 logs: Logs,
21 len_of_taken: usize,
22 text: Text,
23 fmt: Box<dyn FnMut(Record) -> Option<Text> + Send>,
24 has_updated_once: bool,
25 pub close_on_unfocus: bool,
28}
29
30impl LogBook {
31 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
33 self.fmt = Box::new(fmt)
34 }
35
36 pub fn builder() -> LogBookOpts {
38 LogBookOpts::default()
39 }
40}
41
42impl Widget for LogBook {
43 fn update(pa: &mut Pass, handle: &Handle<Self>) {
44 let (lb, area) = handle.write_with_area(pa);
45
46 let Some(new_records) = lb.logs.get(lb.len_of_taken..) else {
47 return;
48 };
49
50 let records_were_added = !new_records.is_empty();
51 lb.len_of_taken += new_records.len();
52
53 for rec_text in new_records.into_iter().filter_map(&mut lb.fmt) {
54 lb.text.insert_text(lb.text.len(), rec_text);
55 }
56
57 if !lb.has_updated_once {
58 area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
59 lb.has_updated_once = true;
60 } else if records_were_added {
61 area.scroll_ver(&lb.text, i32::MAX, lb.get_print_opts());
62 }
63 }
64
65 fn needs_update(&self, _: &Pass) -> bool {
66 self.logs.has_changed()
67 }
68
69 fn text(&self) -> &Text {
70 &self.text
71 }
72
73 fn text_mut(&mut self) -> &mut Text {
74 &mut self.text
75 }
76
77 fn get_print_opts(&self) -> PrintOpts {
78 let mut opts = PrintOpts::new();
79 opts.wrap_lines = true;
80 opts.wrap_on_word = true;
81 opts
82 }
83
84 fn on_focus(pa: &mut Pass, handle: &Handle<Self>) {
85 handle.area().reveal(pa).unwrap();
86 }
87
88 fn on_unfocus(pa: &mut Pass, handle: &Handle<Self>) {
89 if handle.read(pa).close_on_unfocus {
90 handle.area().hide(pa).unwrap()
91 }
92 }
93}
94
95pub struct LogBookOpts {
97 fmt: Box<dyn FnMut(Record) -> Option<Text> + Send>,
98 pub close_on_unfocus: bool = true,
100 pub hidden: bool = true,
102 pub side: Side = Side::Below,
104 pub height: f32 = 8.0,
106 pub width: f32 = 50.0,
108}
109
110impl LogBookOpts {
111 pub fn push_on(mut self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
113 let logs = context::logs();
114
115 let mut text = Text::new();
116
117 let records = logs.get(..).unwrap();
118 let len_of_taken = records.len();
119 for rec_text in records.into_iter().filter_map(&mut self.fmt) {
120 text.insert_text(text.len(), rec_text);
121 }
122
123 let log_book = LogBook {
124 logs,
125 len_of_taken,
126 text,
127 has_updated_once: false,
128 fmt: self.fmt,
129 close_on_unfocus: self.close_on_unfocus,
130 };
131 let specs = match self.side {
132 Side::Right | Side::Left => PushSpecs {
133 side: self.side,
134 width: Some(self.width),
135 hidden: self.hidden,
136 ..
137 },
138 Side::Above | Side::Below => PushSpecs {
139 side: self.side,
140 height: Some(self.height),
141 hidden: self.hidden,
142 ..
143 },
144 };
145
146 push_target.push_outer(pa, log_book, specs)
147 }
148
149 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
157 self.fmt = Box::new(fmt);
158 }
159}
160
161impl Default for LogBookOpts {
162 fn default() -> Self {
163 fn default_fmt(rec: Record) -> Option<Text> {
164 use duat_core::context::Level::*;
165 let mut builder = Text::builder();
166
167 match rec.level() {
168 Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]: ")),
169 Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
170 Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]: ")),
171 Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]: ")),
172 Trace => unreachable!("Trace is not meant to be useable"),
173 };
174
175 builder.push(txt!(
176 "[log_book.bracket][] {}{Spacer}([log_book.location]{}[log_book.bracket])\n",
177 rec.text().clone(),
178 rec.location(),
179 ));
180
181 Some(builder.build())
182 }
183
184 Self { fmt: Box::new(default_fmt), .. }
185 }
186}