Skip to main content

ferroui_browser/
windowing_platform.rs

1use crate::browser_activatable_lifetime::BrowserActivatableLifetime;
2use crate::browser_app_builder::BrowserPlatformOptions;
3use crate::browser_platform_settings::BrowserPlatformSettings;
4use crate::browser_runtime_platform::BrowserRuntimePlatform;
5use crate::browser_screens::BrowserScreens;
6use crate::browser_system_navigation_manager::BrowserSystemNavigationManagerImpl;
7use crate::browser_single_threaded_dispatcher_impl::BrowserSingleThreadedDispatcherImpl;
8use crate::cursor::CssCursorFactory;
9use crate::interop::{ferro_module, JsObject};
10use crate::rendering::BrowserSharedRenderLoop;
11use crate::win_stubs::IconLoaderStub;
12use ferroui_base::input::platform::{KeyGestureFormatInfo, PlatformHotkeyConfiguration};
13use ferroui_base::input::{IKeyboardDevice, KeyboardDevice};
14use ferroui_base::platform::{ICursorFactory, IPlatformSettings, IRuntimePlatform, ISystemNavigationManagerImpl};
15use ferroui_base::threading::Dispatcher;
16use ferroui_base::{FerroLocator, LocatorExtensions};
17use ferroui_controls::application_lifetimes::IActivatableLifetime;
18use ferroui_controls::platform::{
19    IPlatformIconLoader, IScreenImpl, ITopLevelImpl, ITrayIconImpl, IWindowImpl, IWindowingPlatform,
20};
21use std::cell::RefCell;
22use std::collections::HashMap;
23use std::rc::Rc;
24
25ferroui_base::ferro_thread_local! {
26    // Capture the initial global object of the page.
27    static GLOBAL_THIS: RefCell<Option<JsObject>> = const { RefCell::new(None) };
28    static KEYBOARD: RefCell<Option<Rc<KeyboardDevice>>> = const { RefCell::new(None) };
29}
30
31/// The windowing platform of the browser. There are no windows: content is
32/// shown by a view over an element of the page.
33#[derive(Default)]
34pub struct BrowserWindowingPlatform;
35
36impl BrowserWindowingPlatform {
37    /// The global object of the page.
38    ///
39    /// # Panics
40    /// Panics when the browser backend has not been initialized.
41    pub fn global_this() -> JsObject {
42        match GLOBAL_THIS.with(|global_this| global_this.borrow().clone()) {
43            Some(global_this) => global_this,
44            None => panic!("Browser backend wasn't initialized. GlobalThis is null."),
45        }
46    }
47
48    pub(crate) fn set_global_this(value: JsObject) {
49        GLOBAL_THIS.with(|global_this| *global_this.borrow_mut() = Some(value));
50    }
51
52    /// The keyboard device of the platform.
53    ///
54    /// # Panics
55    /// Panics when the windowing platform has not been registered.
56    pub fn keyboard() -> Rc<KeyboardDevice> {
57        match KEYBOARD.with(|keyboard| keyboard.borrow().clone()) {
58            Some(keyboard) => keyboard,
59            None => panic!("BrowserWindowingPlatform not registered."),
60        }
61    }
62
63    /// Sets the keyboard device of the platform without registering the
64    /// platform, for the tests of the input handler.
65    #[cfg(test)]
66    pub(crate) fn set_keyboard_for_unit_tests(keyboard: Option<Rc<KeyboardDevice>>) {
67        KEYBOARD.with(|slot| *slot.borrow_mut() = keyboard);
68    }
69
70    /// Registers the services of the platform with the current service
71    /// locator and installs the dispatcher of the browser.
72    pub fn register() {
73        let instance: Rc<dyn IWindowingPlatform> = Rc::new(BrowserWindowingPlatform);
74
75        let keyboard = KeyboardDevice::new();
76        KEYBOARD.with(|slot| *slot.borrow_mut() = Some(keyboard.clone()));
77        let keyboard_device: Rc<dyn IKeyboardDevice> = keyboard;
78        let platform_settings = BrowserPlatformSettings::new();
79        let activatable_lifetime = BrowserActivatableLifetime::new();
80        let system_navigation_manager = Rc::new(BrowserSystemNavigationManagerImpl::default());
81        let locator = FerroLocator::current_mutable();
82        locator
83            .bind::<dyn IRuntimePlatform>()
84            .to_singleton::<BrowserRuntimePlatform>(|instance| instance)
85            .bind::<dyn ICursorFactory>()
86            .to_singleton::<CssCursorFactory>(|instance| instance)
87            .bind::<dyn IKeyboardDevice>()
88            .to_constant(keyboard_device)
89            .bind::<dyn IPlatformSettings>()
90            .to_constant(platform_settings.clone())
91            // With their concrete types, for the callbacks of the page: they
92            // only go to the services of this backend.
93            .bind_to_self(platform_settings)
94            .bind::<dyn ISystemNavigationManagerImpl>()
95            .to_constant(system_navigation_manager.clone())
96            .bind_to_self(system_navigation_manager)
97            // Created on first use, as the screens subscribe to the changes of the page.
98            .bind::<dyn IScreenImpl>()
99            .to_func(|| Some(BrowserScreens::instance() as Rc<dyn IScreenImpl>))
100            .bind::<BrowserScreens>()
101            .to_func(|| Some(BrowserScreens::instance()))
102            .bind::<dyn IWindowingPlatform>()
103            .to_constant(instance)
104            .bind::<dyn IPlatformIconLoader>()
105            .to_singleton::<IconLoaderStub>(|instance| instance)
106            .bind_to_self_singleton::<PlatformHotkeyConfiguration>()
107            .bind_to_self(Rc::new(KeyGestureFormatInfo::new(Some(HashMap::new()), "Cmd", "Ctrl", "Alt", "Shift")))
108            .bind::<dyn IActivatableLifetime>()
109            .to_constant(activatable_lifetime.clone())
110            .bind_to_self(activatable_lifetime);
111
112        Dispatcher::initialize_ui_thread_dispatcher(BrowserSingleThreadedDispatcherImpl::new());
113
114        // The render thread of a module built with threads. Upstream starts
115        // its render worker before the platform is set up and waits for it;
116        // here it is started when the dispatcher exists (the thread wakes it
117        // up) and before anything asks for the render loop, and nobody
118        // waits: a view created before the thread has reported itself is
119        // not ready until it has. Without threads this does nothing.
120        let options = FerroLocator::current().get_service::<BrowserPlatformOptions>();
121        if options.as_ref().is_none_or(|options| options.render_thread) {
122            BrowserSharedRenderLoop::start_render_thread();
123        }
124
125        if let Some(options) = options {
126            if options.register_ferro_service_worker {
127                let sw_path = ferro_module::resolve_service_worker_path();
128                ferro_module::register_service_worker(sw_path, options.ferro_service_worker_scope.as_deref());
129            }
130        }
131    }
132}
133
134impl IWindowingPlatform for BrowserWindowingPlatform {
135    fn create_window(&self) -> Rc<dyn IWindowImpl> {
136        panic!(
137            "Browser doesn't support windowing platform. In order to display a single-view content, set ISingleViewApplicationLifetime.MainView."
138        );
139    }
140
141    fn create_embeddable_top_level(&self) -> Rc<dyn ITopLevelImpl> {
142        panic!("The method or operation is not implemented.");
143    }
144
145    fn create_embeddable_window(&self) -> Rc<dyn IWindowImpl> {
146        panic!("Browser doesn't support embeddable windowing platform.");
147    }
148
149    fn create_tray_icon(&self) -> Option<Rc<dyn ITrayIconImpl>> {
150        None
151    }
152
153    fn get_windows_z_order(&self, _windows: &[Rc<dyn IWindowImpl>], _z_order: &mut [i64]) {
154        panic!("Specified method is not supported.");
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn there_is_no_tray_icon() {
164        assert!(BrowserWindowingPlatform.create_tray_icon().is_none());
165    }
166
167    #[test]
168    #[should_panic(expected = "Browser doesn't support windowing platform")]
169    fn windows_cannot_be_created() {
170        BrowserWindowingPlatform.create_window();
171    }
172
173    #[test]
174    #[should_panic(expected = "Browser doesn't support embeddable windowing platform")]
175    fn embeddable_windows_cannot_be_created() {
176        BrowserWindowingPlatform.create_embeddable_window();
177    }
178
179    #[test]
180    #[should_panic(expected = "Specified method is not supported")]
181    fn the_z_order_of_windows_is_not_available() {
182        BrowserWindowingPlatform.get_windows_z_order(&[], &mut []);
183    }
184}