Skip to main content

ferroui_controls/notifications/
notification_card.rs

1use super::{INotification, NotificationType};
2use crate::primitives::TemplatedControlImpl;
3use crate::{Button, ContentControl, ContentControlImpl, ControlImpl};
4use ferroui_base::input::InputElementImpl;
5use ferroui_base::interactivity::{
6    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
7};
8use ferroui_base::layout::LayoutableImpl;
9use ferroui_base::{
10    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate,
11    AttachedProperty, BoxedValue, DirectProperty, FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref,
12    StyledElement, StyledElementImpl, StyledProperty, VisualImpl, WeakRef,
13};
14use std::cell::{Cell, RefCell};
15
16const PC_ERROR: &str = ":error";
17const PC_INFORMATION: &str = ":information";
18const PC_SUCCESS: &str = ":success";
19const PC_WARNING: &str = ":warning";
20
21ferroui_base::ferro_thread_local! {
22    /// The click handlers of the buttons whose `CloseOnClick` is set: what
23    /// removing the handler needs when the property is reset.
24    static CLOSE_ON_CLICK_HANDLERS: RefCell<Vec<(WeakRef<Button>, RoutedEventHandlerToken)>> =
25        const { RefCell::new(Vec::new()) };
26}
27
28/// Control that represents and displays a notification.
29#[repr(C)]
30pub struct NotificationCard {
31    base: ContentControl,
32    is_closing: Cell<bool>,
33}
34
35ferro_class!(NotificationCard: ContentControl);
36
37ferro_class_info!(NotificationCard {
38    new: NotificationCard::new,
39    markup: {
40        attributes: [PseudoClasses(":error", ":information", ":success", ":warning")],
41    },
42});
43
44ferro_impl_classes!(
45    NotificationCard: StyledElementImpl,
46    VisualImpl,
47    LayoutableImpl,
48    InteractiveImpl,
49    InputElementImpl,
50    ControlImpl,
51    TemplatedControlImpl,
52    ContentControlImpl
53);
54
55impl FerroObjectImpl for NotificationCard {
56    fn constructed(this: &Self) {
57        Self::parent_constructed(this);
58        this.update_notification_type();
59    }
60
61    fn on_property_changed(this: &Self, e: &FerroPropertyChangedEventArgs<'_>) {
62        Self::parent_on_property_changed(this, e);
63
64        if e.property() == ContentControl::content_property().as_property() {
65            let content = e.get_new_value::<Option<BoxedValue>>();
66            if let Some(notification) = content.as_ref().and_then(<dyn INotification>::from_boxed) {
67                this.set_value(Self::notification_type_property(), notification.type_());
68            }
69        }
70
71        if e.property() == Self::notification_type_property().as_property() {
72            this.update_notification_type();
73        }
74
75        if e.property() == Self::is_closed_property().as_property() {
76            if !this.is_closing() && !this.is_closed() {
77                return;
78            }
79
80            this.raise_event(&RoutedEventArgs::with_event(Self::notification_closed_event()));
81        }
82    }
83}
84
85ferro_properties! {
86    impl NotificationCard {
87        /// Defines the `IsClosing` property.
88        pub fn is_closing_property() -> DirectProperty<NotificationCard, bool> {
89            FerroProperty::register_direct::<NotificationCard, _>("IsClosing", |o| o.is_closing(), None, false)
90        }
91
92        /// Defines the `IsClosed` property.
93        pub fn is_closed_property() -> StyledProperty<bool> {
94            FerroProperty::register::<NotificationCard, _>("IsClosed", false)
95        }
96
97        /// Defines the `NotificationType` property.
98        pub fn notification_type_property() -> StyledProperty<NotificationType> {
99            FerroProperty::register::<NotificationCard, _>("NotificationType", NotificationType::Information)
100        }
101
102        /// Defines the `CloseOnClick` attached property.
103        pub fn close_on_click_property() -> AttachedProperty<bool> {
104            FerroProperty::register_attached::<NotificationCard, Button, _>("CloseOnClick", false)
105        }
106    }
107}
108
109impl NotificationCard {
110    ferro_routed_event!(
111        /// Defines the `NotificationClosed` event.
112        pub fn notification_closed_event() -> RoutedEvent<RoutedEventArgs> {
113            RoutedEvent::register::<NotificationCard, _>("NotificationClosed", RoutingStrategies::BUBBLE)
114        }
115    );
116
117    fn static_constructor() {
118        Self::close_on_click_property()
119            .changed()
120            .add_class_handler::<Button>(Self::on_close_on_click_property_changed);
121    }
122
123    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
124    pub fn construct() -> Self {
125        Self { base: ContentControl::construct(), is_closing: Cell::new(false) }
126    }
127
128    pub fn new() -> Ref<Self> {
129        instantiate(Self::construct())
130    }
131
132    /// Determines if the notification is already closing.
133    pub fn is_closing(&self) -> bool {
134        self.is_closing.get()
135    }
136
137    fn set_is_closing(&self, value: bool) {
138        self.set_and_raise_cell(Self::is_closing_property(), &self.is_closing, value);
139    }
140
141    /// Determines if the notification is closed.
142    pub fn is_closed(&self) -> bool {
143        self.get_value(Self::is_closed_property())
144    }
145
146    pub fn set_is_closed(&self, value: bool) {
147        self.set_value(Self::is_closed_property(), value)
148    }
149
150    /// The type of the notification.
151    pub fn notification_type(&self) -> NotificationType {
152        self.get_value(Self::notification_type_property())
153    }
154
155    pub fn set_notification_type(&self, value: NotificationType) {
156        self.set_value(Self::notification_type_property(), value)
157    }
158
159    /// Raised when the notification card has closed.
160    pub fn notification_closed(
161        &self,
162        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
163    ) -> RoutedEventHandlerToken {
164        self.add_handler(Self::notification_closed_event(), handler)
165    }
166
167    /// Gets the value of the `CloseOnClick` attached property of a button.
168    pub fn get_close_on_click(obj: &Button) -> bool {
169        obj.get_value(Self::close_on_click_property())
170    }
171
172    /// Sets the value of the `CloseOnClick` attached property of a button:
173    /// whether clicking the button closes the notification card it is in.
174    pub fn set_close_on_click(obj: &Button, value: bool) {
175        obj.set_value(Self::close_on_click_property(), value)
176    }
177
178    fn on_close_on_click_property_changed(button: &Button, e: &FerroPropertyChangedEventArgs<'_>) {
179        let value = e.get_new_value::<bool>();
180        let button_ref = button.to_ref();
181
182        // The handler of this button, if it has one; entries of buttons that
183        // no longer exist are dropped on the way.
184        let existing = CLOSE_ON_CLICK_HANDLERS.with(|handlers| {
185            let mut handlers = handlers.borrow_mut();
186            let mut existing = None;
187            handlers.retain(|(candidate, token)| match candidate.upgrade() {
188                Some(candidate) if candidate.ptr_eq(&button_ref) => {
189                    existing = Some(*token);
190                    false
191                }
192                Some(_) => true,
193                None => false,
194            });
195            existing
196        });
197
198        if let Some(token) = existing {
199            button.remove_handler(Button::click_event(), token);
200        }
201
202        if value {
203            let token = button.add_handler(Button::click_event(), Self::button_click);
204            CLOSE_ON_CLICK_HANDLERS.with(|handlers| handlers.borrow_mut().push((button_ref.downgrade(), token)));
205        }
206    }
207
208    /// Called when a button inside the notification is clicked.
209    fn button_click(sender: &Interactive, _e: &RoutedEventArgs) {
210        let btn: &StyledElement = sender;
211        let mut ancestor = btn.parent();
212        while let Some(current) = ancestor {
213            if let Some(notification) = current.cast::<NotificationCard>() {
214                notification.close();
215                break;
216            }
217            ancestor = current.parent();
218        }
219    }
220
221    /// Closes the notification card.
222    pub fn close(&self) {
223        if self.is_closing() {
224            return;
225        }
226
227        self.set_is_closing(true);
228    }
229
230    fn update_notification_type(&self) {
231        let notification_type = self.notification_type();
232        self.pseudo_classes().set(PC_ERROR, notification_type == NotificationType::Error);
233        self.pseudo_classes().set(PC_INFORMATION, notification_type == NotificationType::Information);
234        self.pseudo_classes().set(PC_SUCCESS, notification_type == NotificationType::Success);
235        self.pseudo_classes().set(PC_WARNING, notification_type == NotificationType::Warning);
236    }
237}