Skip to main content

ferroui_native/
ferro_native_application_platform.rs

1use crate::ferro_native_platform::FerroNativePlatform;
2use crate::interop::*;
3use crate::frn_string::frn_string_array_to_vec;
4use crate::mac_os_activatable_lifetime::MacOSActivatableLifetime;
5use crate::storage_provider_api::StorageProviderApi;
6use ferroui_base::platform::storage::file_io::StorageProviderHelpers;
7use ferroui_base::platform::storage::IStorageItem;
8use ferroui_base::reactive::{Disposable, IDisposable};
9use ferroui_base::utilities::{HandlerList, Uri, UriKind};
10use ferroui_base::{FerroLocator, LocatorExtensions};
11use ferroui_controls::application_lifetimes::{
12    ActivationKind, FileActivatedEventArgs, ProtocolActivatedEventArgs, ShutdownRequestedEventArgs,
13};
14use ferroui_controls::platform::IPlatformLifetimeEventsImpl;
15use std::rc::{Rc, Weak};
16
17/// Receives the application events of the native side and exposes them as
18/// the platform lifetime events.
19pub struct FerroNativeApplicationPlatform {
20    weak_self: Weak<FerroNativeApplicationPlatform>,
21    platform: Weak<FerroNativePlatform>,
22    shutdown_requested: HandlerList<dyn Fn(&ShutdownRequestedEventArgs)>,
23}
24
25impl FerroNativeApplicationPlatform {
26    pub(crate) fn new(platform: Weak<FerroNativePlatform>) -> Rc<FerroNativeApplicationPlatform> {
27        Rc::new_cyclic(|weak_self| FerroNativeApplicationPlatform {
28            weak_self: weak_self.clone(),
29            platform,
30            shutdown_requested: HandlerList::new(),
31        })
32    }
33
34    /// The reply to a shutdown request of the system for the outcome of the
35    /// shutdown-requested event.
36    fn shutdown_reply(e: &ShutdownRequestedEventArgs, is_os_shutdown: bool) -> FrnShutdownReply {
37        if e.cancel() {
38            return FrnShutdownReply::ShutdownReplyCancel;
39        }
40
41        // If we know the main loop is going to exit (e.g. via a classic desktop application lifetime),
42        // tell the native side it doesn't have to exit, allowing the managed side to complete its shutdown.
43        if e.will_exit_main_loop() && !is_os_shutdown {
44            return FrnShutdownReply::ShutdownReplyDeferToManagedLoop;
45        }
46
47        FrnShutdownReply::ShutdownReplyTerminateNow
48    }
49}
50
51impl IPlatformLifetimeEventsImpl for FerroNativeApplicationPlatform {
52    fn shutdown_requested(&self, handler: Rc<dyn Fn(&ShutdownRequestedEventArgs)>) -> Rc<dyn IDisposable> {
53        let token = self.shutdown_requested.add(handler);
54        let this = self.weak_self.clone();
55        Disposable::create(move || {
56            if let Some(this) = this.upgrade() {
57                this.shutdown_requested.remove(token);
58            }
59        })
60    }
61}
62
63/// The activatable lifetime of this backend, if it is the registered one.
64fn activatable_lifetime() -> Option<Rc<MacOSActivatableLifetime>> {
65    FerroLocator::current().get_service::<MacOSActivatableLifetime>()
66}
67
68/// The storage API of this backend, if it is the registered storage
69/// provider factory.
70fn storage_api() -> Option<Rc<StorageProviderApi>> {
71    FerroLocator::current().get_service::<StorageProviderApi>()
72}
73
74impl IFrnApplicationEventsImpl for FerroNativeApplicationPlatform {
75    fn files_opened(&self, urls: Option<&IFrnStringArray>) {
76        crate::callback_base::guard((), || {
77            if let (Some(lifetime), Some(storage_api)) = (activatable_lifetime(), storage_api()) {
78                let file_paths = urls.map(frn_string_array_to_vec).unwrap_or_default();
79                let mut files: Vec<Rc<dyn IStorageItem>> = Vec::with_capacity(file_paths.len());
80                for file_path in &file_paths {
81                    if let Some(file) = StorageProviderHelpers::try_get_uri_from_file_path(file_path, false)
82                        .and_then(|file_uri| storage_api.try_get_storage_item(Some(&file_uri), false))
83                    {
84                        files.push(file.into_item());
85                    }
86                }
87
88                if !files.is_empty() {
89                    lifetime.on_activated_with(FileActivatedEventArgs::new(files).into());
90                }
91            }
92        })
93    }
94
95    fn urls_opened(&self, urls: Option<&IFrnStringArray>) {
96        crate::callback_base::guard((), || {
97            if let (Some(lifetime), Some(storage_api)) = (activatable_lifetime(), storage_api()) {
98                let mut files: Vec<Rc<dyn IStorageItem>> = Vec::new();
99                let mut uris: Vec<Uri> = Vec::new();
100                for url in urls.map(frn_string_array_to_vec).unwrap_or_default() {
101                    if let Some(uri) = Uri::try_create(&url, UriKind::RelativeOrAbsolute) {
102                        if uri.scheme() == "file" {
103                            if let Some(file) = storage_api.try_get_storage_item(Some(&uri), false) {
104                                files.push(file.into_item());
105                            }
106                        } else {
107                            uris.push(uri);
108                        }
109                    }
110                }
111
112                for uri in uris {
113                    lifetime.on_activated_with(ProtocolActivatedEventArgs::new(uri).into());
114                }
115                if !files.is_empty() {
116                    lifetime.on_activated_with(FileActivatedEventArgs::new(files).into());
117                }
118            }
119        })
120    }
121
122    fn try_shutdown(&self, is_os_shutdown: bool) -> FrnShutdownReply {
123        crate::callback_base::guard(FrnShutdownReply::ShutdownReplyTerminateNow, || {
124            if self.shutdown_requested.is_empty() {
125                return FrnShutdownReply::ShutdownReplyTerminateNow;
126            }
127
128            let e = ShutdownRequestedEventArgs::with_is_os_shutdown(is_os_shutdown);
129            for (_, handler) in self.shutdown_requested.snapshot().iter() {
130                handler(&e);
131            }
132
133            Self::shutdown_reply(&e, is_os_shutdown)
134        })
135    }
136
137    fn on_reopen(&self) {
138        crate::callback_base::guard((), || {
139            if let Some(lifetime) = activatable_lifetime() {
140                lifetime.on_activated(ActivationKind::Reopen);
141            }
142        })
143    }
144
145    fn on_hide(&self) {}
146
147    fn on_unhide(&self) {}
148
149    fn on_activate(&self) {
150        crate::callback_base::guard((), || {
151            if let Some(lifetime) = activatable_lifetime() {
152                lifetime.on_activated(ActivationKind::Background);
153            }
154        })
155    }
156
157    fn on_deactivate(&self) {
158        crate::callback_base::guard((), || {
159            if let Some(lifetime) = activatable_lifetime() {
160                lifetime.on_deactivated(ActivationKind::Background);
161            }
162        })
163    }
164
165    fn on_terminating(&self) {
166        crate::callback_base::guard((), || {
167            // The OS is terminating us directly: nothing else gets to run, dispose now.
168            if let Some(platform) = self.platform.upgrade() {
169                platform.dispose();
170            }
171        })
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    fn reply(cancel: bool, will_exit_main_loop: bool, is_os_shutdown: bool) -> FrnShutdownReply {
180        let e = ShutdownRequestedEventArgs::with_is_os_shutdown(is_os_shutdown);
181        e.set_cancel(cancel);
182        e.set_will_exit_main_loop(will_exit_main_loop);
183        FerroNativeApplicationPlatform::shutdown_reply(&e, is_os_shutdown)
184    }
185
186    #[test]
187    fn shutdown_replies() {
188        assert_eq!(reply(true, false, false), FrnShutdownReply::ShutdownReplyCancel);
189        assert_eq!(reply(true, true, true), FrnShutdownReply::ShutdownReplyCancel);
190        assert_eq!(reply(false, true, false), FrnShutdownReply::ShutdownReplyDeferToManagedLoop);
191        // The OS shutting down does not wait for the managed loop.
192        assert_eq!(reply(false, true, true), FrnShutdownReply::ShutdownReplyTerminateNow);
193        assert_eq!(reply(false, false, false), FrnShutdownReply::ShutdownReplyTerminateNow);
194    }
195
196    #[test]
197    fn shutdown_without_subscribers_terminates_now() {
198        let platform = FerroNativeApplicationPlatform::new(Weak::new());
199        assert_eq!(platform.try_shutdown(false), FrnShutdownReply::ShutdownReplyTerminateNow);
200
201        let subscription = platform.shutdown_requested(Rc::new(|e| e.set_cancel(true)));
202        assert_eq!(platform.try_shutdown(false), FrnShutdownReply::ShutdownReplyCancel);
203
204        subscription.dispose();
205        assert_eq!(platform.try_shutdown(false), FrnShutdownReply::ShutdownReplyTerminateNow);
206    }
207}