ferroui_controls/notifications/
window_notification_manager.rs1use 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#[repr(C)]
32pub struct WindowNotificationManager {
33 base: TemplatedControl,
34 items: RefCell<Controls>,
37 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 pub fn position_property() -> StyledProperty<NotificationPosition> {
104 FerroProperty::register::<WindowNotificationManager, _>("Position", NotificationPosition::TopRight)
105 }
106
107 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 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 pub fn new() -> Ref<Self> {
133 instantiate(Self::construct())
134 }
135
136 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 fn items(&self) -> Controls {
148 self.items.borrow().clone()
149 }
150
151 pub(crate) fn notifications(&self) -> Vec<Ref<NotificationCard>> {
153 self.items().snapshot().iter().filter_map(|item| item.cast::<NotificationCard>()).collect()
154 }
155
156 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 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 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 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 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 if let Some(classes) = classes {
226 for class in classes {
227 notification_control.classes().add(class);
228 }
229 }
230
231 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 pub fn close(&self, notification: &Rc<dyn INotification>) {
281 self.close_content(¬ification.clone().to_boxed())
282 }
283
284 pub fn close_content(&self, content: &BoxedValue) {
286 Dispatcher::ui_thread().verify_access();
287
288 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 (¬ification, <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 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 pub fn as_managed_notification_manager(&self) -> Rc<dyn IManagedNotificationManager> {
317 Rc::new(ManagerHandle(self.to_ref()))
318 }
319
320 pub fn as_notification_manager(&self) -> Rc<dyn INotificationManager> {
322 Rc::new(ManagerHandle(self.to_ref()))
323 }
324
325 fn install_from_top_level(&self, top_level: &Ref<TopLevel>) {
327 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 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
365struct 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}