Skip to main content

ferroui_controls/notifications/
notification.rs

1use super::{INotification, NotificationType};
2use ferroui_base::data::core::{Maybe, Value, ValueTypes};
3use ferroui_base::data::model::{Event, INotifyPropertyChanged, Model};
4use ferroui_base::{ferro_model, BoxedValue};
5use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7use std::time::Duration;
8
9/// The overridable members of a [`Notification`]: what a class deriving
10/// from it can replace. Every member defaults to the implementation of the
11/// notification.
12///
13/// Install the overrides with [`Notification::with_overrides`].
14pub trait NotificationOverrides {
15    /// Raises the property changed event. `property_name` is `None` when
16    /// all properties changed.
17    fn on_property_changed(&self, notification: &Notification, property_name: Option<&str>) {
18        notification.base_on_property_changed(property_name)
19    }
20}
21
22/// A notification that can be shown in a window or by the host operating
23/// system.
24///
25/// This class represents a notification that can be displayed either in a
26/// window using a
27/// [`WindowNotificationManager`](super::WindowNotificationManager) or by the
28/// host operating system (to be implemented).
29pub struct Notification {
30    overrides: Option<Rc<dyn NotificationOverrides>>,
31    title: RefCell<Option<String>>,
32    message: RefCell<Option<String>>,
33    type_: Cell<NotificationType>,
34    expiration: Cell<Duration>,
35    on_click: RefCell<Option<Rc<dyn Fn()>>>,
36    on_close: RefCell<Option<Rc<dyn Fn()>>>,
37    property_changed: Event<str>,
38}
39
40impl Notification {
41    /// The expiration of a notification that states none.
42    const DEFAULT_EXPIRATION: Duration = Duration::from_secs(5);
43
44    /// Creates a notification.
45    ///
46    /// * `title`: the title of the notification.
47    /// * `message`: the message to be displayed in the notification.
48    /// * `type_`: the [`NotificationType`] of the notification.
49    /// * `expiration`: the expiry time at which the notification will close
50    ///   (five seconds when `None`). Use [`Duration::ZERO`] for notifications
51    ///   that will remain open.
52    /// * `on_click`: an action to call when the notification is clicked.
53    /// * `on_close`: an action to call when the notification is closed.
54    pub fn new(
55        title: Option<String>,
56        message: Option<String>,
57        type_: NotificationType,
58        expiration: Option<Duration>,
59        on_click: Option<Rc<dyn Fn()>>,
60        on_close: Option<Rc<dyn Fn()>>,
61    ) -> Rc<Self> {
62        Self::create(title, message, type_, expiration, on_click, on_close, None)
63    }
64
65    /// Creates a notification without title and message, of the
66    /// information type, that expires after five seconds.
67    pub fn empty() -> Rc<Self> {
68        Self::new(None, None, NotificationType::Information, None, None, None)
69    }
70
71    /// Creates a notification whose overridable members are replaced by
72    /// `overrides`: the form a class deriving from the notification takes.
73    pub fn with_overrides(
74        title: Option<String>,
75        message: Option<String>,
76        type_: NotificationType,
77        expiration: Option<Duration>,
78        on_click: Option<Rc<dyn Fn()>>,
79        on_close: Option<Rc<dyn Fn()>>,
80        overrides: Rc<dyn NotificationOverrides>,
81    ) -> Rc<Self> {
82        Self::create(title, message, type_, expiration, on_click, on_close, Some(overrides))
83    }
84
85    fn create(
86        title: Option<String>,
87        message: Option<String>,
88        type_: NotificationType,
89        expiration: Option<Duration>,
90        on_click: Option<Rc<dyn Fn()>>,
91        on_close: Option<Rc<dyn Fn()>>,
92        overrides: Option<Rc<dyn NotificationOverrides>>,
93    ) -> Rc<Self> {
94        Self::ensure_registered();
95
96        let notification = Rc::new(Self {
97            overrides,
98            title: RefCell::new(None),
99            message: RefCell::new(None),
100            type_: Cell::new(type_),
101            expiration: Cell::new(expiration.unwrap_or(Self::DEFAULT_EXPIRATION)),
102            on_click: RefCell::new(on_click),
103            on_close: RefCell::new(on_close),
104            property_changed: Event::new(),
105        });
106        // As upstream, the constructor assigns the title and the message
107        // through their setters, which notify.
108        notification.set_title(title);
109        notification.set_message(message);
110        notification
111    }
112
113    /// Makes the binding metadata of the class and its cast to
114    /// [`INotification`] known on the current thread, once.
115    fn ensure_registered() {
116        ferroui_base::ferro_thread_local! {
117            static REGISTERED: Cell<bool> = const { Cell::new(false) };
118        }
119
120        if REGISTERED.replace(true) {
121            return;
122        }
123
124        <Self as Model>::register();
125        ValueTypes::register_boxed_cast::<Notification, Rc<dyn INotification>>(|object| {
126            let any: Rc<dyn std::any::Any> = object.clone();
127            any.downcast::<Notification>().ok().map(|notification| notification as Rc<dyn INotification>)
128        });
129    }
130
131    /// The title of the notification.
132    pub fn title(&self) -> Option<String> {
133        self.title.borrow().clone()
134    }
135
136    pub fn set_title(&self, value: Option<String>) {
137        if *self.title.borrow() != value {
138            *self.title.borrow_mut() = value;
139            self.on_property_changed(Some("Title"));
140        }
141    }
142
143    /// Sets the title; returns the notification for chaining.
144    pub fn with_title(self: Rc<Self>, value: Option<String>) -> Rc<Self> {
145        self.set_title(value);
146        self
147    }
148
149    /// The notification message.
150    pub fn message(&self) -> Option<String> {
151        self.message.borrow().clone()
152    }
153
154    pub fn set_message(&self, value: Option<String>) {
155        if *self.message.borrow() != value {
156            *self.message.borrow_mut() = value;
157            self.on_property_changed(Some("Message"));
158        }
159    }
160
161    /// Sets the message; returns the notification for chaining.
162    pub fn with_message(self: Rc<Self>, value: Option<String>) -> Rc<Self> {
163        self.set_message(value);
164        self
165    }
166
167    /// The [`NotificationType`] of the notification.
168    pub fn type_(&self) -> NotificationType {
169        self.type_.get()
170    }
171
172    pub fn set_type(&self, value: NotificationType) {
173        self.type_.set(value)
174    }
175
176    /// Sets the type; returns the notification for chaining.
177    pub fn with_type(self: Rc<Self>, value: NotificationType) -> Rc<Self> {
178        self.set_type(value);
179        self
180    }
181
182    /// The expiration time of the notification after which it will
183    /// automatically close. If the value is [`Duration::ZERO`] then the
184    /// notification will remain open until the user closes it.
185    pub fn expiration(&self) -> Duration {
186        self.expiration.get()
187    }
188
189    pub fn set_expiration(&self, value: Duration) {
190        self.expiration.set(value)
191    }
192
193    /// Sets the expiration; returns the notification for chaining.
194    pub fn with_expiration(self: Rc<Self>, value: Duration) -> Rc<Self> {
195        self.set_expiration(value);
196        self
197    }
198
199    /// An action to be run when the notification is clicked.
200    pub fn on_click(&self) -> Option<Rc<dyn Fn()>> {
201        self.on_click.borrow().clone()
202    }
203
204    pub fn set_on_click(&self, value: Option<Rc<dyn Fn()>>) {
205        *self.on_click.borrow_mut() = value;
206    }
207
208    /// Sets the click action; returns the notification for chaining.
209    pub fn with_on_click(self: Rc<Self>, value: Option<Rc<dyn Fn()>>) -> Rc<Self> {
210        self.set_on_click(value);
211        self
212    }
213
214    /// An action to be run when the notification is closed.
215    pub fn on_close(&self) -> Option<Rc<dyn Fn()>> {
216        self.on_close.borrow().clone()
217    }
218
219    pub fn set_on_close(&self, value: Option<Rc<dyn Fn()>>) {
220        *self.on_close.borrow_mut() = value;
221    }
222
223    /// Sets the close action; returns the notification for chaining.
224    pub fn with_on_close(self: Rc<Self>, value: Option<Rc<dyn Fn()>>) -> Rc<Self> {
225        self.set_on_close(value);
226        self
227    }
228
229    /// Raises the property changed event. Overridable: see
230    /// [`NotificationOverrides::on_property_changed`].
231    pub fn on_property_changed(&self, property_name: Option<&str>) {
232        match &self.overrides {
233            Some(overrides) => overrides.on_property_changed(self, property_name),
234            None => self.base_on_property_changed(property_name),
235        }
236    }
237
238    /// The implementation of
239    /// [`on_property_changed`](Self::on_property_changed) of this class, for
240    /// overrides.
241    pub fn base_on_property_changed(&self, property_name: Option<&str>) {
242        // An empty name means that all properties changed.
243        self.property_changed.raise(property_name.unwrap_or(""));
244    }
245}
246
247impl INotification for Notification {
248    fn title(&self) -> Option<String> {
249        Notification::title(self)
250    }
251
252    fn message(&self) -> Option<String> {
253        Notification::message(self)
254    }
255
256    fn type_(&self) -> NotificationType {
257        Notification::type_(self)
258    }
259
260    fn expiration(&self) -> Duration {
261        Notification::expiration(self)
262    }
263
264    fn on_click(&self) -> Option<Rc<dyn Fn()>> {
265        Notification::on_click(self)
266    }
267
268    fn on_close(&self) -> Option<Rc<dyn Fn()>> {
269        Notification::on_close(self)
270    }
271
272    fn to_boxed(self: Rc<Self>) -> BoxedValue {
273        self
274    }
275}
276
277impl INotifyPropertyChanged for Notification {
278    fn property_changed(&self) -> &Event<str> {
279        &self.property_changed
280    }
281}
282
283// The action properties are not declared: actions are not comparable values.
284ferro_model!(Notification, |b| b
285    .notify_property_changed()
286    .property::<Maybe<String>>("Title", |n| n.title(), |n, v| n.set_title(v))
287    .property::<Maybe<String>>("Message", |n| n.message(), |n, v| n.set_message(v))
288    .property::<Value<NotificationType>>("Type", |n| n.type_(), |n, v| n.set_type(v))
289    .property::<Value<Duration>>("Expiration", |n| n.expiration(), |n, v| n.set_expiration(v)));