Skip to main content

ferroui_controls/notifications/
window_notification_manager.rs

1use super::{
2    IManagedNotificationManager, INotification, INotificationManager, NotificationCard, NotificationPosition,
3    NotificationType,
4};
5use crate::primitives::{
6    AdornerLayer, TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl, TemplatedControlImplExt,
7    VisualLayerManager,
8};
9use crate::{ControlImpl, Controls, Panel, TopLevel};
10use ferroui_base::input::{InputElement, InputElementImpl, PointerPressedEventArgs};
11use ferroui_base::interactivity::{Interactive, InteractiveImpl, RoutedEventHandlerToken};
12use ferroui_base::layout::{HorizontalAlignment, Layoutable, LayoutableImpl, VerticalAlignment};
13use ferroui_base::threading::{Dispatcher, DispatcherPriority, DispatcherTimer};
14use ferroui_base::{
15    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, BoxedValue, FerroObject,
16    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty, VisualImpl,
17    VisualImplExt, VisualTreeAttachmentEventArgs,
18};
19use std::cell::{Cell, RefCell};
20use std::rc::Rc;
21use std::time::Duration;
22
23const PC_TOP_LEFT: &str = ":topleft";
24const PC_TOP_RIGHT: &str = ":topright";
25const PC_BOTTOM_LEFT: &str = ":bottomleft";
26const PC_BOTTOM_RIGHT: &str = ":bottomright";
27const PC_TOP_CENTER: &str = ":topcenter";
28const PC_BOTTOM_CENTER: &str = ":bottomcenter";
29
30/// An [`INotificationManager`] that displays notifications in a window.
31#[repr(C)]
32pub struct WindowNotificationManager {
33    base: TemplatedControl,
34    /// The notification cards: a list of its own until the template is
35    /// applied, the children of the items panel of the template afterwards.
36    items: RefCell<Controls>,
37    /// The handler of the template applied event of the hosting top-level.
38    top_level_template_applied: Cell<Option<RoutedEventHandlerToken>>,
39}
40
41ferro_class!(WindowNotificationManager: TemplatedControl);
42
43ferro_class_info!(WindowNotificationManager {
44    new: WindowNotificationManager::new,
45    interfaces: [
46        Rc<dyn IManagedNotificationManager> => managed_notification_manager_of,
47        Rc<dyn INotificationManager> => notification_manager_of,
48    ],
49    markup: {
50        attributes: [
51            TemplatePart("PART_Items", type(Ref<Panel>)),
52            PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright", ":topcenter", ":bottomcenter"),
53        ],
54    },
55});
56
57ferro_impl_classes!(
58    WindowNotificationManager: StyledElementImpl,
59    LayoutableImpl,
60    InteractiveImpl,
61    InputElementImpl,
62    ControlImpl
63);
64
65impl FerroObjectImpl for WindowNotificationManager {
66    fn constructed(this: &Self) {
67        Self::parent_constructed(this);
68        this.update_pseudo_classes(this.position());
69    }
70
71    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
72        Self::parent_on_property_changed(this, change);
73
74        if change.property() == Self::position_property().as_property() {
75            this.update_pseudo_classes(change.get_new_value::<NotificationPosition>());
76        }
77    }
78}
79
80impl VisualImpl for WindowNotificationManager {
81    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
82        Self::parent_on_detached_from_visual_tree(this, e);
83
84        this.items().clear();
85    }
86}
87
88impl TemplatedControlImpl for WindowNotificationManager {
89    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
90        Self::parent_on_apply_template(this, e);
91
92        let items_control = e.name_scope().find_as::<Panel>("PART_Items");
93
94        if let Some(items_control) = items_control {
95            *this.items.borrow_mut() = items_control.children();
96        }
97    }
98}
99
100ferro_properties! {
101    impl WindowNotificationManager {
102        /// Defines the `Position` property.
103        pub fn position_property() -> StyledProperty<NotificationPosition> {
104            FerroProperty::register::<WindowNotificationManager, _>("Position", NotificationPosition::TopRight)
105        }
106
107        /// Defines the `MaxItems` property.
108        pub fn max_items_property() -> StyledProperty<i32> {
109            FerroProperty::register::<WindowNotificationManager, _>("MaxItems", 5)
110        }
111    }
112}
113
114impl WindowNotificationManager {
115    fn static_constructor() {
116        Layoutable::horizontal_alignment_property()
117            .override_default_value::<WindowNotificationManager>(HorizontalAlignment::Stretch);
118        Layoutable::vertical_alignment_property()
119            .override_default_value::<WindowNotificationManager>(VerticalAlignment::Stretch);
120    }
121
122    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
123    pub fn construct() -> Self {
124        Self {
125            base: TemplatedControl::construct(),
126            items: RefCell::new(Controls::new()),
127            top_level_template_applied: Cell::new(None),
128        }
129    }
130
131    /// Creates a notification manager that is not hosted yet.
132    pub fn new() -> Ref<Self> {
133        instantiate(Self::construct())
134    }
135
136    /// Creates a notification manager hosted by `host`: the top-level that
137    /// will host the control.
138    pub fn with_host(host: Option<&Ref<TopLevel>>) -> Ref<Self> {
139        let this = Self::new();
140        if let Some(host) = host {
141            this.install_from_top_level(host);
142        }
143        this
144    }
145
146    /// The list the notification cards are in.
147    fn items(&self) -> Controls {
148        self.items.borrow().clone()
149    }
150
151    /// Currently active notifications.
152    pub(crate) fn notifications(&self) -> Vec<Ref<NotificationCard>> {
153        self.items().snapshot().iter().filter_map(|item| item.cast::<NotificationCard>()).collect()
154    }
155
156    /// Defines which corner of the screen notifications can be displayed in.
157    pub fn position(&self) -> NotificationPosition {
158        self.get_value(Self::position_property())
159    }
160
161    pub fn set_position(&self, value: NotificationPosition) {
162        self.set_value(Self::position_property(), value)
163    }
164
165    /// Defines the maximum number of notifications visible at once.
166    pub fn max_items(&self) -> i32 {
167        self.get_value(Self::max_items_property())
168    }
169
170    pub fn set_max_items(&self, value: i32) {
171        self.set_value(Self::max_items_property(), value)
172    }
173
174    /// Shows a notification.
175    pub fn show(&self, content: Rc<dyn INotification>) {
176        let (type_, expiration, on_click, on_close) =
177            (content.type_(), content.expiration(), content.on_click(), content.on_close());
178        self.show_with(content.to_boxed(), type_, Some(expiration), on_click, on_close, None);
179    }
180
181    /// Shows a notification with the given content: a notification, or any
182    /// other content, which is shown as information.
183    pub fn show_content(&self, content: BoxedValue) {
184        if let Some(notification) = <dyn INotification>::from_boxed(&content) {
185            self.show_with(
186                content,
187                notification.type_(),
188                Some(notification.expiration()),
189                notification.on_click(),
190                notification.on_close(),
191                None,
192            );
193        } else {
194            self.show_with(content, NotificationType::Information, None, None, None, None);
195        }
196    }
197
198    /// Shows a notification.
199    ///
200    /// * `content`: the content of the notification.
201    /// * `type_`: the type of the notification.
202    /// * `expiration`: the expiration time of the notification after which
203    ///   it will automatically close (five seconds when `None`). If the
204    ///   value is zero then the notification will remain open until the user
205    ///   closes it.
206    /// * `on_click`: an action to be run when the notification is clicked.
207    /// * `on_close`: an action to be run when the notification is closed.
208    /// * `classes`: style classes to apply.
209    pub fn show_with(
210        &self,
211        content: BoxedValue,
212        type_: NotificationType,
213        expiration: Option<Duration>,
214        on_click: Option<Rc<dyn Fn()>>,
215        on_close: Option<Rc<dyn Fn()>>,
216        classes: Option<&[&str]>,
217    ) {
218        Dispatcher::ui_thread().verify_access();
219
220        let notification_control = NotificationCard::new();
221        notification_control.set_content(Some(content));
222        notification_control.set_notification_type(type_);
223
224        // Add style classes if any
225        if let Some(classes) = classes {
226            for class in classes {
227                notification_control.classes().add(class);
228            }
229        }
230
231        // The handlers are held by the card: they refer to it, and to the
232        // manager, weakly.
233        let weak_this = self.to_ref().downgrade();
234        let weak_control = notification_control.downgrade();
235        notification_control.notification_closed(move |_, _| {
236            if let Some(on_close) = &on_close {
237                on_close();
238            }
239            if let (Some(this), Some(notification_control)) = (weak_this.upgrade(), weak_control.upgrade()) {
240                this.items().remove(notification_control);
241            }
242        });
243
244        notification_control.add_handler(
245            InputElement::pointer_pressed_event(),
246            move |sender: &Interactive, _: &PointerPressedEventArgs| {
247                if let Some(on_click) = &on_click {
248                    on_click();
249                }
250
251                let sender: &FerroObject = sender;
252                if let Some(card) = sender.downcast_ref::<NotificationCard>() {
253                    card.close();
254                }
255            },
256        );
257
258        self.items().add(notification_control.clone());
259
260        let notifications = self.notifications();
261        let open_count = notifications.iter().filter(|i| !i.is_closing()).count();
262        if i64::try_from(open_count).unwrap_or(i64::MAX) > i64::from(self.max_items()) {
263            if let Some(first) = notifications.iter().find(|i| !i.is_closing()) {
264                first.close();
265            }
266        }
267
268        if expiration == Some(Duration::ZERO) {
269            return;
270        }
271
272        DispatcherTimer::run_once(
273            move || notification_control.close(),
274            expiration.unwrap_or(Duration::from_secs(5)),
275            DispatcherPriority::DEFAULT,
276        );
277    }
278
279    /// Closes a notification.
280    pub fn close(&self, notification: &Rc<dyn INotification>) {
281        self.close_content(&notification.clone().to_boxed())
282    }
283
284    /// Closes the notifications with the given content.
285    pub fn close_content(&self, content: &BoxedValue) {
286        Dispatcher::ui_thread().verify_access();
287
288        // A notification is the same content in either of its untyped forms:
289        // the object itself, or a box that holds its contract handle.
290        let notification = <dyn INotification>::from_boxed(content);
291        for card in self.notifications() {
292            let Some(card_content) = card.content() else {
293                continue;
294            };
295            let equals = *card_content == **content
296                || match (&notification, <dyn INotification>::from_boxed(&card_content)) {
297                    (Some(notification), Some(card_notification)) => **notification == *card_notification,
298                    _ => false,
299                };
300            if equals {
301                card.close();
302            }
303        }
304    }
305
306    /// Closes all notifications.
307    pub fn close_all(&self) {
308        Dispatcher::ui_thread().verify_access();
309
310        for card in self.notifications() {
311            card.close();
312        }
313    }
314
315    /// The manager as the contract of managed notification managers.
316    pub fn as_managed_notification_manager(&self) -> Rc<dyn IManagedNotificationManager> {
317        Rc::new(ManagerHandle(self.to_ref()))
318    }
319
320    /// The manager as the contract of notification managers.
321    pub fn as_notification_manager(&self) -> Rc<dyn INotificationManager> {
322        Rc::new(ManagerHandle(self.to_ref()))
323    }
324
325    /// Installs the manager within the adorner layer of the top-level.
326    fn install_from_top_level(&self, top_level: &Ref<TopLevel>) {
327        // As upstream, the top-level keeps the manager alive through the
328        // handler until its template is applied again.
329        let this = self.to_ref();
330        let token = top_level.template_applied(move |sender, e| this.top_level_on_template_applied(sender, e));
331        self.top_level_template_applied.set(Some(token));
332
333        let adorner =
334            top_level.find_descendant_of_type::<VisualLayerManager>(false).and_then(|manager| manager.adorner_layer());
335        if let Some(adorner) = adorner {
336            adorner.children().add(self.to_ref());
337            AdornerLayer::set_adorned_element(self, adorner);
338        }
339    }
340
341    fn top_level_on_template_applied(&self, sender: &Interactive, _e: &TemplateAppliedEventArgs) {
342        if let Some(adorner_layer) = self.parent().and_then(|parent| parent.cast::<AdornerLayer>()) {
343            adorner_layer.children().remove(self.to_ref());
344            AdornerLayer::set_adorned_element(self, None);
345        }
346
347        // Reinstall notification manager on template reapplied.
348        let top_level = sender.to_ref().cast::<TopLevel>().expect("the sender of the event is the top-level");
349        if let Some(token) = self.top_level_template_applied.take() {
350            top_level.remove_handler(TemplatedControl::template_applied_event(), token);
351        }
352        self.install_from_top_level(&top_level);
353    }
354
355    fn update_pseudo_classes(&self, position: NotificationPosition) {
356        self.pseudo_classes().set(PC_TOP_LEFT, position == NotificationPosition::TopLeft);
357        self.pseudo_classes().set(PC_TOP_RIGHT, position == NotificationPosition::TopRight);
358        self.pseudo_classes().set(PC_BOTTOM_LEFT, position == NotificationPosition::BottomLeft);
359        self.pseudo_classes().set(PC_BOTTOM_RIGHT, position == NotificationPosition::BottomRight);
360        self.pseudo_classes().set(PC_TOP_CENTER, position == NotificationPosition::TopCenter);
361        self.pseudo_classes().set(PC_BOTTOM_CENTER, position == NotificationPosition::BottomCenter);
362    }
363}
364
365/// The handle of a manager as the contracts it implements.
366struct ManagerHandle(Ref<WindowNotificationManager>);
367
368impl INotificationManager for ManagerHandle {
369    fn show(&self, notification: Rc<dyn INotification>) {
370        self.0.show(notification)
371    }
372
373    fn close(&self, notification: &Rc<dyn INotification>) {
374        self.0.close(notification)
375    }
376
377    fn close_all(&self) {
378        self.0.close_all()
379    }
380
381    fn reference_id(&self) -> *const () {
382        let object: &FerroObject = &self.0;
383        object as *const FerroObject as *const ()
384    }
385}
386
387impl IManagedNotificationManager for ManagerHandle {
388    fn show_content(&self, content: BoxedValue) {
389        self.0.show_content(content)
390    }
391
392    fn close_content(&self, content: &BoxedValue) {
393        self.0.close_content(content)
394    }
395}
396
397fn managed_notification_manager_of(manager: Ref<WindowNotificationManager>) -> Rc<dyn IManagedNotificationManager> {
398    manager.as_managed_notification_manager()
399}
400
401fn notification_manager_of(manager: Ref<WindowNotificationManager>) -> Rc<dyn INotificationManager> {
402    manager.as_notification_manager()
403}