duat_base/widgets/
notifications.rs1use std::sync::Mutex;
11
12use duat_core::{
13 Ns,
14 context::{self, Handle, Level, Record},
15 data::Pass,
16 hook::{self, KeyTyped, MsgLogged},
17 text::{Mask, Text, TextMut},
18 ui::{PushSpecs, PushTarget, Side, Widget},
19};
20
21pub fn add_notifications_hook() {
22 hook::add::<MsgLogged>(|pa, rec| {
23 let Some(notifications) = context::handle_of::<Notifications>(pa) else {
24 return;
25 };
26
27 let notifs = notifications.write(pa);
28
29 if !notifs.levels.contains(&rec.level()) {
30 return;
31 }
32
33 let mut global_fmt = GLOBAL_FMT.lock().unwrap();
34 let mut global_get_mask = GLOBAL_GET_MASK.lock().unwrap();
35
36 let mask = if let Some(get_mask) = notifs.get_mask.as_mut() {
37 get_mask(rec.clone())
38 } else if let Some(get_mask) = global_get_mask.as_mut() {
39 get_mask(rec.clone())
40 } else {
41 default_get_mask(rec.clone())
42 };
43
44 notifs.text = if let Some(fmt) = notifs.fmt.as_mut() {
45 fmt(rec)
46 } else if let Some(fmt) = global_fmt.as_mut() {
47 fmt(rec)
48 } else {
49 default_fmt(rec)
50 };
51
52 notifs.text.insert_tag(Ns::basic(), .., Mask(mask));
53
54 if notifs.request_width {
55 let notifs = notifications.read(pa);
56 let size = notifications
57 .area()
58 .size_of_text(pa, notifs.print_opts(), ¬ifs.text)
59 .unwrap();
60 notifications.area().set_width(pa, size.x).unwrap();
61 notifications.area().set_height(pa, size.y).unwrap();
62 }
63 });
64
65 hook::add::<KeyTyped>(|pa, _| {
66 for notifications in context::windows().handles_of::<Notifications>(pa) {
67 let (notifs, area) = notifications.write_with_area(pa);
68
69 if !notifs.text.is_empty_empty() {
70 notifs.text = Text::new();
71
72 if notifs.request_width {
73 let size = area
74 .size_of_text(notifs.print_opts(), ¬ifs.text)
75 .unwrap();
76 area.set_width(size.x).unwrap();
77 area.set_height(size.y).unwrap();
78 }
79 }
80 }
81 })
82 .lateness(0);
83}
84
85pub struct Notifications {
118 text: Text,
119 fmt: Option<Box<dyn FnMut(Record) -> Text + Send>>,
120 levels: Vec<Level>,
121 get_mask: Option<Box<dyn FnMut(Record) -> &'static str + Send>>,
122 request_width: bool,
123}
124
125#[allow(clippy::type_complexity)]
126static GLOBAL_FMT: Mutex<Option<Box<dyn FnMut(Record) -> Text + Send>>> = Mutex::new(None);
127#[allow(clippy::type_complexity)]
128static GLOBAL_GET_MASK: Mutex<Option<Box<dyn FnMut(Record) -> &'static str + Send>>> =
129 Mutex::new(None);
130
131impl Notifications {
132 pub fn builder() -> NotificationsOpts {
135 NotificationsOpts::default()
136 }
137}
138
139impl Widget for Notifications {
140 fn text(&self) -> &Text {
141 &self.text
142 }
143
144 fn text_mut(&mut self) -> TextMut<'_> {
145 self.text.as_mut()
146 }
147}
148
149#[doc(hidden)]
161#[derive(Clone)]
162pub struct NotificationsOpts {
163 allowed_levels: Vec<Level>,
164 request_width: bool,
165}
166
167impl NotificationsOpts {
168 pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<Notifications> {
170 let notifications = Notifications {
171 text: Text::new(),
172 fmt: None,
173 get_mask: None,
174 levels: self.allowed_levels,
175 request_width: self.request_width,
176 };
177 let specs = PushSpecs {
178 side: Side::Below,
179 height: Some(1.0),
180 ..Default::default()
181 };
182
183 push_target.push_inner(pa, notifications, specs)
184 }
185
186 pub fn fmt(&mut self, fmt: impl FnMut(Record) -> Text + Send + 'static) {
194 *GLOBAL_FMT.lock().unwrap() = Some(Box::new(fmt));
195 }
196
197 pub fn set_mask(&mut self, get_mask: impl FnMut(Record) -> &'static str + Send + 'static) {
201 *GLOBAL_GET_MASK.lock().unwrap() = Some(Box::new(get_mask));
202 }
203
204 pub fn set_allowed_levels(&mut self, levels: impl IntoIterator<Item = Level>) {
209 self.allowed_levels = levels.into_iter().collect();
210 }
211
212 pub(crate) fn request_width(&mut self) {
214 self.request_width = true;
215 }
216}
217
218impl Default for NotificationsOpts {
219 fn default() -> Self {
220 Self {
221 allowed_levels: vec![Level::Error, Level::Warn, Level::Info],
222 request_width: false,
223 }
224 }
225}
226
227fn default_fmt(rec: Record) -> Text {
228 match rec.level() {
229 Level::Error | Level::Warn | Level::Debug => rec.text().clone(),
230 Level::Info => rec.text().clone(),
231 Level::Trace => unreachable!(),
232 }
233}
234fn default_get_mask(rec: Record) -> &'static str {
235 match rec.level() {
236 context::Level::Error => "error",
237 context::Level::Warn => "warn",
238 context::Level::Info => "info",
239 context::Level::Debug => "debug",
240 context::Level::Trace => unreachable!(),
241 }
242}