duat_core/widgets/
notifier.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] group
5//! exists, will be hidden when the [`PromptLine`] is not in focus,
6//! 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 super::{CheckerFn, Widget, WidgetCfg};
16use crate::{
17    context::{self, Notifications},
18    hooks::{self, KeySent},
19    text::Text,
20    ui::{PushSpecs, Ui},
21};
22
23/// A [`Widget`] to show notifications
24///
25/// By default, it is expected to be placed "under" a [`PromptLine`], and
26/// with the `"HidePromptLine"` [hook] group, take its place when the
27/// [`PromptLine`] is not in focus.
28///
29/// If you don't want this behaviour, see [`left_with_ratio`]
30///
31/// [`PromptLine`]: super::PromptLine
32/// [hook]: hooks
33/// [`left_with_ratio`]: NotificationsCfg::left_with_ratio
34pub struct Notifier<U> {
35    notifications: Notifications,
36    text: Text,
37    _ghost: PhantomData<U>,
38}
39
40static CLEAR_NOTIFS: AtomicBool = AtomicBool::new(false);
41
42impl<U: Ui> Widget<U> for Notifier<U> {
43    type Cfg = NotificationsCfg<U>;
44
45    fn cfg() -> Self::Cfg {
46        NotificationsCfg(None, PhantomData)
47    }
48
49    fn update(&mut self, _area: &<U as Ui>::Area) {
50        let clear_notifs = CLEAR_NOTIFS.swap(false, Ordering::Relaxed);
51        if self.notifications.has_changed() {
52            let notifications = self.notifications.read();
53            self.text = notifications.last().cloned().unwrap_or_default()
54        } else if clear_notifs {
55            self.text = Text::new()
56        }
57    }
58
59    fn text(&self) -> &Text {
60        &self.text
61    }
62
63    fn text_mut(&mut self) -> &mut Text {
64        &mut self.text
65    }
66
67    fn once() -> Result<(), Text> {
68        hooks::add_grouped::<KeySent>("RemoveNotificationsOnInput", |_| {
69            CLEAR_NOTIFS.store(true, Ordering::Relaxed);
70        });
71        Ok(())
72    }
73}
74
75/// A [`Widget`] to show notifications
76///
77/// By default, it is expected to be placed "under" a [`PromptLine`], and
78/// with the `"HidePromptLine"` [hook] group, take its place when the
79/// [`PromptLine`] is not in focus.
80///
81/// If you don't want this behaviour, see [`left_with_ratio`]
82///
83/// [`PromptLine`]: super::PromptLine
84/// [hook]: hooks
85/// [`left_with_ratio`]: NotificationsCfg::left_with_ratio
86#[doc(hidden)]
87pub struct NotificationsCfg<U>(Option<(u16, u16)>, PhantomData<U>);
88
89impl<U> NotificationsCfg<U> {
90    /// Pushes to the left and sets a height
91    ///
92    /// Use this if you want notifications that don't occupy the same
93    /// space as a [`PromptLine`].
94    ///
95    /// [`PromptLine`]: super::PromptLine
96    pub fn left_with_ratio(self, den: u16, div: u16) -> Self {
97        Self(Some((den, div)), PhantomData)
98    }
99}
100
101impl<U: Ui> WidgetCfg<U> for NotificationsCfg<U> {
102    type Widget = Notifier<U>;
103
104    fn build(self, _: bool) -> (Self::Widget, impl CheckerFn, PushSpecs) {
105        let widget = Notifier {
106            notifications: context::notifications(),
107            text: Text::new(),
108            _ghost: PhantomData,
109        };
110
111        let checker = {
112            let checker = widget.notifications.checker();
113            move || checker() || CLEAR_NOTIFS.load(Ordering::Relaxed)
114        };
115
116        let specs = if let Some((den, div)) = self.0 {
117            PushSpecs::left().with_hor_ratio(den, div)
118        } else {
119            PushSpecs::below().with_ver_len(1.0)
120        };
121
122        (widget, checker, specs)
123    }
124}