duat_base/widgets/
log_book.rs1use 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
19pub 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 pub close_on_unfocus: bool,
29}
30
31impl LogBook {
32 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Option<Text> + Send + 'static) {
34 self.fmt = Box::new(fmt)
35 }
36
37 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
111pub struct LogBookOpts {
113 fmt: Box<dyn FnMut(Record) -> Option<Text> + Send>,
114 pub close_on_unfocus: bool,
116 pub hidden: bool,
118 pub side: Side,
120 pub height: f32,
123 pub width: f32,
126}
127
128impl LogBookOpts {
129 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 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}