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::doc_duat as duat;
32/// # use duat_utils::widgets::{Notifications, FooterWidgets};
33/// setup_duat!(setup);
34/// use duat::prelude::*;
35///
36/// fn setup() {
37///     hook::remove("WindowWidgets");
38///     hook::add::<WindowCreated>(|_, builder| {
39///         let footer = FooterWidgets::default().notifs(Notifications::cfg().formatted(|rec| {
40///             txt!(
41///                 "[notifs.bracket]([notifs.target]{}[notifs.bracket]) {}",
42///                 rec.target(),
43///                 rec.text().clone()
44///             )
45///         }));
46///         builder.push(footer);
47///     });
48/// }
49/// ```
50///
51/// [`FooterWidgets`]: super::FooterWidgets
52/// [`WidgetAlias`]: duat_core::ui::WidgetAlias
53/// [`PromptLine`]: super::PromptLine
54/// [`StatusLine`]: super::StatusLine
55/// [hook]: duat_core::hook
56pub struct Notifications<U> {
57    logs: context::Logs,
58    text: Text,
59    format_rec: Box<dyn FnMut(Record) -> Text + Send>,
60    levels: Vec<Level>,
61    last_rec: Option<usize>,
62    get_mask: Box<dyn FnMut(Record) -> &'static str + Send>,
63    _ghost: PhantomData<U>,
64}
65
66static CLEAR_NOTIFS: AtomicBool = AtomicBool::new(false);
67
68impl<U: Ui> Widget<U> for Notifications<U> {
69    type Cfg = NotificationsCfg<U>;
70
71    fn cfg() -> Self::Cfg {
72        Self::Cfg {
73            format_rec: Box::new(|rec| {
74                txt!(
75                    "[notifs.target]{}[notifs.colon]: {}",
76                    rec.target(),
77                    rec.text().clone()
78                )
79                .build()
80            }),
81            get_mask: Box::new(|rec| match rec.level() {
82                context::Level::Error => "error",
83                context::Level::Warn => "warn",
84                context::Level::Info => "info",
85                context::Level::Debug => "debug",
86                context::Level::Trace => unreachable!(),
87            }),
88            levels: vec![Level::Info, Level::Warn, Level::Error],
89            _ghost: PhantomData,
90        }
91    }
92
93    fn update(pa: &mut Pass, handle: &Handle<Self, U>) {
94        let clear_notifs = CLEAR_NOTIFS.swap(false, Ordering::Relaxed);
95        let notifs = handle.write(pa);
96
97        if notifs.logs.has_changed()
98            && let Some((i, rec)) = notifs.logs.last_with_levels(&notifs.levels)
99            && notifs.last_rec.is_none_or(|last_i| last_i < i)
100        {
101            handle.set_mask((notifs.get_mask)(rec.clone()));
102            notifs.text = (notifs.format_rec)(rec);
103            notifs.last_rec = Some(i);
104        } else if clear_notifs {
105            handle.set_mask("");
106            notifs.text = Text::new()
107        }
108    }
109
110    fn text(&self) -> &Text {
111        &self.text
112    }
113
114    fn text_mut(&mut self) -> &mut Text {
115        &mut self.text
116    }
117
118    fn once() -> Result<(), Text> {
119        form::set_weak("default.Notifications.error", Form::red());
120        form::set_weak("accent.error", Form::red().underlined().bold());
121        form::set_weak("default.Notifications.info", Form::cyan());
122        form::set_weak("accent.info", Form::blue().underlined().bold());
123
124        hook::add_grouped::<KeysSent, U>("RemoveNotificationsOnInput", |_, _| {
125            CLEAR_NOTIFS.store(true, Ordering::Relaxed);
126        });
127        Ok(())
128    }
129
130    fn needs_update(&self, _: &Pass) -> bool {
131        self.logs.has_changed() || CLEAR_NOTIFS.load(Ordering::Relaxed)
132    }
133
134    fn print(&mut self, painter: Painter, area: &<U as Ui>::Area) {
135        let cfg = self.print_cfg();
136        area.print(self.text_mut(), cfg, painter)
137    }
138}
139
140/// A [`Widget`] to show notifications
141///
142/// By default, it is expected to be placed "under" a [`PromptLine`],
143/// and with the `"HidePromptLine"` [hook] group, take its place when
144/// the [`PromptLine`] is not in focus.
145///
146/// If you don't want this behaviour, see [`left_with_ratio`]
147///
148/// [`PromptLine`]: super::PromptLine
149/// [hook]: hooks
150/// [`left_with_ratio`]: NotificationsCfg::left_with_ratio
151#[doc(hidden)]
152pub struct NotificationsCfg<U> {
153    format_rec: Box<dyn FnMut(Record) -> Text + Send>,
154    get_mask: Box<dyn FnMut(Record) -> &'static str + Send>,
155    levels: Vec<Level>,
156    _ghost: PhantomData<U>,
157}
158
159impl<U> NotificationsCfg<U> {
160    /// Changes the way [`Record`]s are formatted by [`Notifications`]
161    ///
162    /// This will be applied to every single [`Level`] of a
163    /// [`Record`]. If you wish to limit which levels will get shown,
164    /// see [`filter_levels`]
165    ///
166    /// [`filter_levels`]: Self::filter_levels
167    pub fn formatted<T: Into<Text>>(
168        self,
169        mut fmt: impl FnMut(Record) -> T + Send + 'static,
170    ) -> Self {
171        Self {
172            format_rec: Box::new(move |rec| fmt(rec).into()),
173            ..self
174        }
175    }
176
177    /// Filters which [`Level`]s willl show notifications
178    ///
179    /// Is [`Level::Info`], [`Level::Warn`] and [`Level::Error`] by
180    /// default.
181    pub fn filter_levels(mut self, levels: impl IntoIterator<Item = Level>) -> Self {
182        self.levels = levels.into_iter().collect();
183        self
184    }
185
186    /// Changes how [`Notifications`] decides which [mask] to use
187    ///
188    /// [mask]: duat_core::context::Handle::set_mask
189    pub fn with_mask(self, get_mask: impl FnMut(Record) -> &'static str + Send + 'static) -> Self {
190        Self { get_mask: Box::new(get_mask), ..self }
191    }
192}
193
194impl<U: Ui> WidgetCfg<U> for NotificationsCfg<U> {
195    type Widget = Notifications<U>;
196
197    fn build(self, _: &mut Pass, _: BuildInfo<U>) -> (Self::Widget, PushSpecs) {
198        let widget = Notifications {
199            logs: context::logs(),
200            text: Text::new(),
201            format_rec: self.format_rec,
202            get_mask: self.get_mask,
203            levels: self.levels,
204            last_rec: None,
205            _ghost: PhantomData,
206        };
207
208        (widget, PushSpecs::below().ver_len(1.0))
209    }
210}
211
212impl<U: Ui> Default for NotificationsCfg<U> {
213    fn default() -> Self {
214        Notifications::cfg()
215    }
216}