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,
100 pub hidden: bool,
102 pub side: Side,
104 pub height: f32,
107 pub width: f32,
110}
111
112impl LogBookOpts {
113 pub fn push_on(mut self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<LogBook> {
115 let logs = context::logs();
116
117 let mut text = Text::new();
118
119 let records = logs.get(..).unwrap();
120 let len_of_taken = records.len();
121 for rec_text in records.into_iter().filter_map(&mut self.fmt) {
122 text.insert_text(text.len(), rec_text);
123 }
124
125 let log_book = LogBook {
126 logs,
127 len_of_taken,
128 text,
129 has_updated_once: false,
130 fmt: self.fmt,
131 close_on_unfocus: self.close_on_unfocus,
132 };
133 let specs = match self.side {
134 Side::Right | Side::Left => PushSpecs {
135 side: self.side,
136 width: Some(self.width),
137 hidden: self.hidden,
138 ..Default::default()
139 },
140 Side::Above | Side::Below => PushSpecs {
141 side: self.side,
142 height: Some(self.height),
143 hidden: self.hidden,
144 ..Default::default()
145 },
146 };
147
148 push_target.push_outer(pa, log_book, specs)
149 }
150
151 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
159 self.fmt = Box::new(fmt);
160 }
161}
162
163impl Default for LogBookOpts {
164 fn default() -> Self {
165 fn default_fmt(rec: Record) -> Option<Text> {
166 use duat_core::context::Level::*;
167 let mut builder = Text::builder();
168
169 match rec.level() {
170 Error => builder.push(txt!("[log_book.error][[ERROR]][log_book.colon]: ")),
171 Warn => builder.push(txt!("[log_book.warn][[WARNING]][log_book.colon]:")),
172 Info => builder.push(txt!("[log_book.info][[INFO]][log_book.colon]: ")),
173 Debug => builder.push(txt!("[log_book.debug][[DEBUG]][log_book.colon]: ")),
174 Trace => unreachable!("Trace is not meant to be useable"),
175 };
176
177 builder.push(txt!(
178 "[log_book.bracket][] {}{Spacer}([log_book.location]{}[log_book.bracket])\n",
179 rec.text().clone(),
180 rec.location(),
181 ));
182
183 Some(builder.build())
184 }
185
186 Self {
187 fmt: Box::new(default_fmt),
188 close_on_unfocus: true,
189 hidden: true,
190 side: Side::Below,
191 height: 8.0,
192 width: 50.0,
193 }
194 }
195}