duat_base/widgets/
notifications.rs

1//! A [`Widget`] that shows notifications
2//!
3//! This is a very simple [`Widget`], and will usually be placed right
4//! under a [`PromptLine`], which, when the `"HidePromptLine"` [hook]
5//! group exists, will be hidden when the [`PromptLine`] is not in
6//! focus, allowing for the [`Notifications`] widget to pop up.
7//!
8//! [`PromptLine`]: super::PromptLine
9//! [hook]: hooks
10use std::sync::{
11    Once,
12    atomic::{AtomicBool, Ordering},
13};
14
15use duat_core::{
16    context::{self, Handle, Level, Record},
17    data::Pass,
18    hook::{self, KeysSent},
19    text::{Text, txt},
20    ui::{PushSpecs, PushTarget, Side, Widget},
21};
22
23/// A [`Widget`] to show notifications
24///
25/// You can style modify it using the [`opts::set_notifs`] function in
26/// Duat:
27///
28/// ```rust
29/// # duat_core::doc_duat!(duat);
30/// # use duat_base::widgets::{Notifications, FooterWidgets};
31/// # mod opts {
32/// #     pub fn set_notifs(set_fn: impl FnMut(&mut duat_base::widgets::NotificationsOpts)) {}
33/// # }
34/// setup_duat!(setup);
35/// use duat::prelude::*;
36///
37/// fn setup() {
38///     opts::set_notifs(|opts| {
39///         opts.fmt(|rec| {
40///             txt!(
41///                 "[notifs.bracket]([log_book.location]{}[notifs.bracket]) {}",
42///                 rec.location(),
43///                 rec.text().clone()
44///             )
45///         })
46///     });
47/// }
48/// ```
49///
50/// [`FooterWidgets`]: super::FooterWidgets
51/// [`PromptLine`]: super::PromptLine
52/// [`StatusLine`]: super::StatusLine
53/// [hook]: duat_core::hook
54/// [`opts::set_notifs`]: https://docs.rs/duat/latest/duat/opts/fn.set_notifs.html
55pub struct Notifications {
56    logs: context::Logs,
57    text: Text,
58    format_rec: Box<dyn FnMut(Record) -> Text + Send>,
59    levels: Vec<Level>,
60    last_rec: Option<usize>,
61    get_mask: Box<dyn FnMut(Record) -> &'static str + Send>,
62    request_width: bool,
63}
64
65static CLEAR_NOTIFS: AtomicBool = AtomicBool::new(false);
66
67impl Notifications {
68    /// Returns a [`NotificationsOpts`], which can be used to push
69    /// `Notifications` around
70    pub fn builder() -> NotificationsOpts {
71        static ONCE: Once = Once::new();
72        ONCE.call_once(|| {
73            hook::add::<KeysSent>(|_, _| {
74                CLEAR_NOTIFS.store(true, Ordering::Relaxed);
75                Ok(())
76            });
77        });
78        NotificationsOpts::default()
79    }
80}
81
82impl Widget for Notifications {
83    fn update(pa: &mut Pass, handle: &Handle<Self>) {
84        let clear_notifs = CLEAR_NOTIFS.swap(false, Ordering::Relaxed);
85        let notifs = handle.write(pa);
86
87        if notifs.logs.has_changed()
88            && let Some((i, rec)) = notifs.logs.last_with_levels(&notifs.levels)
89            && notifs.last_rec.is_none_or(|last_i| last_i < i)
90        {
91            handle.set_mask((notifs.get_mask)(rec.clone()));
92            notifs.text = (notifs.format_rec)(rec);
93            notifs.last_rec = Some(i);
94
95            if notifs.request_width {
96                let notifs = handle.read(pa);
97                let width = handle
98                    .area()
99                    .width_of_text(pa, notifs.get_print_opts(), &notifs.text)
100                    .unwrap();
101                handle.area().set_width(pa, width).unwrap();
102            }
103        } else if clear_notifs {
104            handle.set_mask("");
105            if notifs.text != Text::new() {
106                notifs.text = Text::new();
107
108                if notifs.request_width {
109                    let notifs = handle.read(pa);
110                    let width = handle
111                        .area()
112                        .width_of_text(pa, notifs.get_print_opts(), &notifs.text)
113                        .unwrap();
114                    handle.area().set_width(pa, width).unwrap();
115                }
116            }
117        }
118    }
119
120    fn text(&self) -> &Text {
121        &self.text
122    }
123
124    fn text_mut(&mut self) -> &mut Text {
125        &mut self.text
126    }
127
128    fn needs_update(&self, _: &Pass) -> bool {
129        self.logs.has_changed() || CLEAR_NOTIFS.load(Ordering::Relaxed)
130    }
131}
132
133/// A builder for the [`Notifications`] [`Widget`]
134///
135/// Normally, this `Widget` is placed alongside others in the
136/// [`FooterWidgets`] `Widget` group.
137///
138/// You can create it separately with [`Notifications::builder`],
139/// which will return this struct.
140///
141/// [`PromptLine`]: super::PromptLine
142/// [hook]: hook
143/// [`FooterWidgets`]: super::FooterWidgets
144#[doc(hidden)]
145pub struct NotificationsOpts {
146    fmt: Box<dyn FnMut(Record) -> Text + Send>,
147    get_mask: Box<dyn FnMut(Record) -> &'static str + Send>,
148    allowed_levels: Vec<Level>,
149    request_width: bool,
150}
151
152impl NotificationsOpts {
153    /// Pushes the [`Notifications`] to another [`Widget`]
154    pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<Notifications> {
155        let notifications = Notifications {
156            logs: context::logs(),
157            text: Text::new(),
158            format_rec: self.fmt,
159            get_mask: self.get_mask,
160            levels: self.allowed_levels,
161            last_rec: None,
162            request_width: self.request_width,
163        };
164        let specs = PushSpecs {
165            side: Side::Below,
166            height: Some(1.0),
167            ..Default::default()
168        };
169
170        push_target.push_inner(pa, notifications, specs)
171    }
172
173    /// Changes the way [`Record`]s are formatted by [`Notifications`]
174    ///
175    /// This will be applied to every single [`Level`] of a
176    /// [`Record`]. If you wish to limit which levels will get shown,
177    /// see [`set_allowed_levels`]
178    ///
179    /// [`set_allowed_levels`]: Self::set_allowed_levels
180    pub fn fmt<T: Into<Text>>(&mut self, mut fmt: impl FnMut(Record) -> T + Send + 'static) {
181        self.fmt = Box::new(move |rec| fmt(rec).into());
182    }
183
184    /// Filters which [`Level`]s willl show notifications
185    ///
186    /// Is [`Level::Info`], [`Level::Warn`] and [`Level::Error`] by
187    /// default.
188    pub fn set_allowed_levels(&mut self, levels: impl IntoIterator<Item = Level>) {
189        self.allowed_levels = levels.into_iter().collect();
190    }
191
192    /// Changes how [`Notifications`] decides which [mask] to use
193    ///
194    /// [mask]: duat_core::context::Handle::set_mask
195    pub fn set_mask(&mut self, get_mask: impl FnMut(Record) -> &'static str + Send + 'static) {
196        self.get_mask = Box::new(get_mask);
197    }
198
199    /// Requests the width when printing to the screen
200    pub(crate) fn request_width(&mut self) {
201        self.request_width = true;
202    }
203}
204
205impl Default for NotificationsOpts {
206    fn default() -> Self {
207        fn default_fmt(rec: Record) -> Text {
208            match rec.level() {
209                Level::Error | Level::Warn | Level::Debug => {
210                    txt!(
211                        "[buffer]{}[notifs.colon]:[] {}",
212                        rec.location(),
213                        rec.text().clone()
214                    )
215                }
216                Level::Info => rec.text().clone(),
217                Level::Trace => unreachable!(),
218            }
219        }
220        fn default_get_mask(rec: Record) -> &'static str {
221            match rec.level() {
222                context::Level::Error => "error",
223                context::Level::Warn => "warn",
224                context::Level::Info => "info",
225                context::Level::Debug => "debug",
226                context::Level::Trace => unreachable!(),
227            }
228        }
229
230        Self {
231            fmt: Box::new(default_fmt),
232            get_mask: Box::new(default_get_mask),
233            allowed_levels: vec![Level::Error, Level::Warn, Level::Info],
234            request_width: false,
235        }
236    }
237}