duat_utils/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::{
11    marker::PhantomData,
12    sync::atomic::{AtomicBool, Ordering},
13};
14
15use duat_core::{
16    context::{Level, Record},
17    form::Painter,
18    hook::KeysSent,
19    prelude::*,
20};
21
22/// A [`Widget`] to show notifications
23///
24/// With the [`FooterWidgets`] (a [`WidgetAlias`]), this [`Widget`]
25/// can be conveniently placed alongside a [`PromptLine`] and a
26/// [`StatusLine`], in a combination that hides the [`PromptLine`]
27/// when it is not in use, covering it with the [`Notifications`], and
28/// vice-versa. This is the default behaviour of Duat.
29///
30/// ```rust
31/// use duat_core::{hook::OnWindowOpen, prelude::*};
32/// use duat_utils::{state::*, widgets::*};
33///
34/// fn setup_generic_over_ui<U: Ui>() {
35///     hook::remove("WindowWidgets");
36///     hook::add::<OnWindowOpen<U>, U>(|pa, builder| {
37///         let footer = FooterWidgets::default().notifs(Notifications::cfg().formatted(|rec| {
38///             Some(txt!(
39///                 "[notifs.bracket]([notifs.target]{}[notifs.bracket]) {}",
40///                 rec.target(),
41///                 rec.text().clone()
42///             ))
43///         }));
44///         builder.push(pa, footer);
45///     });
46/// }
47/// ```
48///
49/// [`FooterWidgets`]: super::FooterWidgets
50/// [`WidgetAlias`]: duat_core::ui::WidgetAlias
51/// [`PromptLine`]: super::PromptLine
52/// [`StatusLine`]: super::StatusLine
53/// [hook]: duat_core::hook
54pub struct Notifications<U> {
55    logs: context::Logs,
56    text: Text,
57    _ghost: PhantomData<U>,
58    format_rec: Box<dyn FnMut(Record) -> Option<Text>>,
59    get_mask: Box<dyn FnMut(Record) -> &'static str>,
60}
61
62static CLEAR_NOTIFS: AtomicBool = AtomicBool::new(false);
63
64impl<U: Ui> Widget<U> for Notifications<U> {
65    type Cfg = NotificationsCfg<U>;
66
67    fn cfg() -> Self::Cfg {
68        NotificationsCfg {
69            format_rec: Box::new(|rec| {
70                // This is so stupid
71                (rec.level() < Level::Debug).then(|| {
72                    txt!(
73                        "[notifs.target]{}[notifs.colon]: {}",
74                        rec.target(),
75                        rec.text().clone()
76                    )
77                    .build()
78                })
79            }),
80            get_mask: Box::new(|rec| match rec.level() {
81                context::Level::Error => "error",
82                context::Level::Warn => "warn",
83                context::Level::Info => "info",
84                context::Level::Debug => "debug",
85                context::Level::Trace => unreachable!(),
86            }),
87            _ghost: PhantomData,
88        }
89    }
90
91    fn update(pa: &mut Pass, handle: Handle<Self, U>) {
92        let clear_notifs = CLEAR_NOTIFS.swap(false, Ordering::Relaxed);
93        handle.write(pa, |wid, _| {
94            if wid.logs.has_changed()
95                && let Some(rec) = wid.logs.last()
96                && let Some(text) = (wid.format_rec)(rec.clone())
97            {
98                handle.set_mask((wid.get_mask)(rec));
99                wid.text = text
100            } else if clear_notifs {
101                handle.set_mask("");
102                wid.text = Text::new()
103            }
104        });
105    }
106
107    fn text(&self) -> &Text {
108        &self.text
109    }
110
111    fn text_mut(&mut self) -> &mut Text {
112        &mut self.text
113    }
114
115    fn once() -> Result<(), Text> {
116        form::set_weak("default.Notifications.error", Form::red());
117        form::set_weak("accent.error", Form::red().underlined().bold());
118        form::set_weak("default.Notifications.info", Form::cyan());
119        form::set_weak("accent.info", Form::blue().underlined().bold());
120
121        hook::add_grouped::<KeysSent, U>("RemoveNotificationsOnInput", |_, _| {
122            CLEAR_NOTIFS.store(true, Ordering::Relaxed);
123        });
124        Ok(())
125    }
126
127    fn needs_update(&self) -> bool {
128        self.logs.has_changed() || CLEAR_NOTIFS.load(Ordering::Relaxed)
129    }
130
131    fn print(&mut self, painter: Painter, area: &<U as Ui>::Area) {
132        let cfg = self.print_cfg();
133        area.print(self.text_mut(), cfg, painter)
134    }
135}
136
137/// A [`Widget`] to show notifications
138///
139/// By default, it is expected to be placed "under" a [`PromptLine`],
140/// and with the `"HidePromptLine"` [hook] group, take its place when
141/// the [`PromptLine`] is not in focus.
142///
143/// If you don't want this behaviour, see [`left_with_ratio`]
144///
145/// [`PromptLine`]: super::PromptLine
146/// [hook]: hooks
147/// [`left_with_ratio`]: NotificationsCfg::left_with_ratio
148#[doc(hidden)]
149pub struct NotificationsCfg<U> {
150    format_rec: Box<dyn FnMut(Record) -> Option<Text>>,
151    get_mask: Box<dyn FnMut(Record) -> &'static str>,
152    _ghost: PhantomData<U>,
153}
154
155impl<U> NotificationsCfg<U> {
156    /// Changes the way [`Record`]s are formatted by [`Notifications`]
157    ///
158    /// This function returns an [`Option<Text>`], which means you can
159    /// filter out unnecessary [`Record`]s. By default, only records
160    /// with a level of [`Level::Info`] or higher will get shown.
161    pub fn formatted<T: Into<Text>>(
162        self,
163        mut format_rec: impl FnMut(Record) -> Option<T> + 'static,
164    ) -> Self {
165        Self {
166            format_rec: Box::new(move |rec| format_rec(rec).map(Into::into)),
167            ..self
168        }
169    }
170
171    /// Changes how [`Notifications`] decides which [mask] to use
172    ///
173    /// [mask]: duat_core::context::Handle::set_mask
174    pub fn with_mask(self, get_mask: impl FnMut(Record) -> &'static str + 'static) -> Self {
175        Self { get_mask: Box::new(get_mask), ..self }
176    }
177}
178
179impl<U: Ui> WidgetCfg<U> for NotificationsCfg<U> {
180    type Widget = Notifications<U>;
181
182    fn build(self, _: &mut Pass, _: Option<FileHandle<U>>) -> (Self::Widget, PushSpecs) {
183        let widget = Notifications {
184            logs: context::logs(),
185            text: Text::new(),
186            format_rec: self.format_rec,
187            get_mask: self.get_mask,
188            _ghost: PhantomData,
189        };
190
191        (widget, PushSpecs::below().with_ver_len(1.0))
192    }
193}
194
195impl<U: Ui> Default for NotificationsCfg<U> {
196    fn default() -> Self {
197        Notifications::cfg()
198    }
199}