Skip to main content

ferroui_native/
ferro_native_platform.rs

1//! The macOS windowing platform: creates the native factory, registers the
2//! platform services and creates the top-levels.
3
4use crate::clipboard_impl::ClipboardImpl;
5use crate::cursor::CursorFactory;
6use crate::dispatcher_impl::DispatcherImpl;
7use crate::embeddable_top_level_impl::EmbeddableTopLevelImpl;
8use crate::extensions::as_com_bool;
9use crate::ferro_native_application_platform::FerroNativeApplicationPlatform;
10use crate::ferro_native_menu_exporter::FerroNativeMenuExporter;
11use crate::ferro_native_drag_source::{free_data_transfer_handle, FerroNativeDragSource};
12use crate::ferro_native_gl_platform_graphics::FerroNativeGlPlatformGraphics;
13use crate::ferro_native_platform_extensions::{
14    FerroNativePlatformOptions, FerroNativeRenderingMode, MacOSPlatformOptions,
15};
16use crate::ferro_native_render_timer::FerroNativeRenderTimer;
17use crate::frn_dispatcher::FrnDispatcher;
18use crate::frn_string::to_c_string;
19use crate::helpers::ComResultExt;
20use crate::icon_loader::IconLoader;
21use crate::interop::*;
22use crate::mac_os_activatable_lifetime::MacOSActivatableLifetime;
23use crate::mac_os_mounted_volume_info_provider::MacOSMountedVolumeInfoProvider;
24use crate::mac_os_native_menu_commands::MacOSNativeMenuCommands;
25use crate::metal::MetalPlatformGraphics;
26use crate::native_platform_settings::NativePlatformSettings;
27use crate::screen_impl::ScreenImpl;
28use crate::storage_provider_api::StorageProviderApi;
29use crate::tray_icon_impl::TrayIconImpl;
30use crate::window_impl::WindowImpl;
31use ferroui_base::input::platform::{
32    Clipboard, IClipboard, IClipboardImpl, IPlatformDragSource, KeyGestureFormatInfo, PlatformHotkeyConfiguration,
33};
34use ferroui_base::input::{IKeyboardDevice, Key, KeyGesture, KeyModifiers, KeyboardDevice};
35use ferroui_base::platform::{ICursorFactory, IPlatformGraphics, IPlatformGraphicsContext, IPlatformSettings};
36use ferroui_base::rendering::composition::Compositor;
37use ferroui_base::rendering::{IRenderLoop, RenderLoop, ThreadProxyRenderTimer};
38use ferroui_base::threading::Dispatcher;
39use ferroui_base::{FerroLocator, LocatorExtensions};
40use ferroui_controls::application_lifetimes::IActivatableLifetime;
41use ferroui_controls::Application;
42use ferroui_controls::platform::{
43    IMountedVolumeInfoProvider, INativeApplicationCommands, IPlatformIconLoader, IPlatformLifetimeEventsImpl,
44    IScreenImpl, IStorageProviderFactory, ITopLevelImpl, ITrayIconImpl, IWindowImpl, IWindowingPlatform,
45};
46use ferroui_microcom::ComPtr;
47use std::cell::{Cell, RefCell};
48use std::collections::HashMap;
49use std::ffi::{c_char, c_int, c_void, CString};
50use std::rc::Rc;
51use std::sync::Arc;
52
53ferroui_base::ferro_thread_local! {
54    static KEYBOARD_DEVICE: Rc<KeyboardDevice> = KeyboardDevice::new();
55    static COMPOSITOR: RefCell<Option<Rc<Compositor>>> = const { RefCell::new(None) };
56}
57
58extern "C" {
59    fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
60    fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
61}
62
63const RTLD_NOW: c_int = 0x2;
64
65/// Frees the handles native code holds on to: the data transfers of drag
66/// operations started by this backend. (Callback objects are reference
67/// counted COM objects, never handles.)
68struct GCHandleDeallocator;
69
70impl IFrnGCHandleDeallocatorCallbackImpl for GCHandleDeallocator {
71    fn free_gc_handle(&self, handle: *mut c_void) {
72        // SAFETY: the only handles this backend gives to native code are
73        // the data transfer handles of the drag source.
74        crate::callback_base::guard((), || unsafe { free_data_transfer_handle(handle) })
75    }
76}
77
78/// The macOS windowing platform.
79pub struct FerroNativePlatform {
80    factory: RefCell<Option<ComPtr<IFerroNativeFactory>>>,
81    options: RefCell<Option<FerroNativePlatformOptions>>,
82    platform_graphics: RefCell<Option<Arc<dyn IPlatformGraphics>>>,
83    is_disposed: Cell<bool>,
84    /// The exporters of the application menu and of the dock menu.
85    application_menu_exporter: RefCell<Option<Rc<FerroNativeMenuExporter>>>,
86    application_dock_menu_exporter: RefCell<Option<Rc<FerroNativeMenuExporter>>>,
87}
88
89impl FerroNativePlatform {
90    /// The keyboard device of the platform.
91    pub fn keyboard_device() -> Rc<KeyboardDevice> {
92        KEYBOARD_DEVICE.with(Rc::clone)
93    }
94
95    /// The compositor of the platform.
96    ///
97    /// # Panics
98    /// Panics when the platform is not initialized on this thread.
99    pub fn compositor() -> Rc<Compositor> {
100        COMPOSITOR.with(|compositor| compositor.borrow().clone()).expect("the platform is initialized")
101    }
102
103    /// Initializes the platform over an existing native factory.
104    pub fn initialize_with_factory(
105        factory: ComPtr<IFerroNativeFactory>,
106        options: FerroNativePlatformOptions,
107    ) -> Rc<FerroNativePlatform> {
108        FerroLocator::current_mutable().bind_to_self(Rc::new(factory.clone()));
109
110        let result = Rc::new(FerroNativePlatform {
111            factory: RefCell::new(Some(factory)),
112            options: RefCell::new(None),
113            platform_graphics: RefCell::new(None),
114            is_disposed: Cell::new(false),
115            application_menu_exporter: RefCell::new(None),
116            application_dock_menu_exporter: RefCell::new(None),
117        });
118
119        result.do_initialize(options);
120
121        result
122    }
123
124    /// Initializes the platform: creates the native factory (from the
125    /// library named by the options, or the one linked into the
126    /// application) and registers the platform services in the service
127    /// locator. Must be called on the main thread.
128    ///
129    /// # Panics
130    /// Panics when the native library named by the options cannot be
131    /// loaded or has no factory export, and when the factory cannot be
132    /// created.
133    pub fn initialize(options: FerroNativePlatformOptions) -> Rc<FerroNativePlatform> {
134        let factory = match &options.ferro_native_library_path {
135            Some(path) => {
136                let path = CString::new(path.as_str()).expect("the library path has no NUL");
137                // SAFETY: plain dynamic loading; the export, when present,
138                // has the signature of the factory entry point.
139                unsafe {
140                    let lib = dlopen(path.as_ptr(), RTLD_NOW);
141                    if lib.is_null() {
142                        panic!("Unable to load the native library {path:?}");
143                    }
144                    let proc = dlsym(lib, c"CreateFerroNative".as_ptr());
145                    if proc.is_null() {
146                        panic!("Unable to get \"CreateFerroNative\" export from the native library");
147                    }
148                    let d: unsafe extern "C" fn() -> *mut IFerroNativeFactory = std::mem::transmute(proc);
149                    ComPtr::from_raw(d())
150                }
151            }
152            None => create_ferro_native(),
153        };
154
155        Self::initialize_with_factory(factory.expect("the native factory"), options)
156    }
157
158    #[track_caller]
159    fn factory(&self) -> ComPtr<IFerroNativeFactory> {
160        match self.factory.borrow().clone() {
161            Some(factory) => factory,
162            None => panic!("Cannot access a disposed object: the native factory"),
163        }
164    }
165
166    /// Exports the application menu: the menu attached to the application
167    /// or, without one, a default menu; the standard items (Services, Hide,
168    /// Quit, ...) are appended unless the options disable them.
169    pub fn setup_application_menu_exporter(&self) {
170        let exporter = FerroNativeMenuExporter::for_application(&self.factory());
171        *self.application_menu_exporter.borrow_mut() = Some(exporter);
172    }
173
174    /// Exports the dock menu attached to the application, now and whenever
175    /// it changes.
176    pub fn setup_application_dock_menu_exporter(&self) {
177        let exporter = FerroNativeMenuExporter::for_dock(&self.factory());
178        *self.application_dock_menu_exporter.borrow_mut() = Some(exporter);
179    }
180
181    /// Sets the application title shown by the system (menu bar, dock) to
182    /// the name of the current application, unless it is empty or white
183    /// space.
184    ///
185    /// # Panics
186    /// Panics when there is no current application.
187    pub fn setup_application_name(&self) {
188        let application = Application::current().expect("the current application");
189        self.setup_application_name_with(application.name().as_deref());
190    }
191
192    /// [`setup_application_name`](Self::setup_application_name) for a host
193    /// without an application object.
194    pub fn setup_application_name_with(&self, name: Option<&str>) {
195        if let Some(name) = name.filter(|name| !name.trim().is_empty()) {
196            if let Some(mac_options) = self.factory().get_mac_options() {
197                mac_options.set_application_title(Some(&to_c_string(name))).check();
198            }
199        }
200    }
201
202    fn do_initialize(self: &Rc<Self>, options: FerroNativePlatformOptions) {
203        *self.options.borrow_mut() = Some(options.clone());
204        let factory = self.factory();
205        let locator = FerroLocator::current_mutable();
206
207        let application_platform = FerroNativeApplicationPlatform::new(Rc::downgrade(self));
208
209        let mac_opts = FerroLocator::current().get_service::<MacOSPlatformOptions>();
210        let mac_opts = mac_opts.as_deref().cloned().unwrap_or_default();
211
212        if let Some(mac_options) = factory.get_mac_options() {
213            mac_options.set_disable_app_delegate(as_com_bool(mac_opts.disable_ferro_app_delegate)).check();
214        }
215
216        let deallocator = IFrnGCHandleDeallocatorCallback::from_impl(GCHandleDeallocator);
217        let application_events = IFrnApplicationEvents::from_impl(application_platform.clone());
218        let dispatcher = IFrnDispatcher::from_impl(FrnDispatcher);
219        factory.initialize(Some(&deallocator), Some(&application_events), Some(&dispatcher)).check();
220
221        if let Some(mac_options) = factory.get_mac_options() {
222            mac_options.set_show_in_dock(as_com_bool(mac_opts.show_in_dock)).check();
223            mac_options.set_disable_set_process_name(as_com_bool(mac_opts.disable_set_process_name)).check();
224        }
225
226        let clipboard_impl: Rc<dyn IClipboardImpl> =
227            Rc::new(ClipboardImpl::new(factory.create_clipboard().check().expect("the native clipboard")));
228        let clipboard: Rc<dyn IClipboard> = Clipboard::new(clipboard_impl.clone());
229
230        Dispatcher::initialize_ui_thread_dispatcher(DispatcherImpl::new(
231            factory.create_platform_threading_interface().check().expect("the native threading interface"),
232        ));
233
234        let cursor_factory: Rc<dyn ICursorFactory> =
235            Rc::new(CursorFactory::new(factory.create_cursor_factory().check().expect("the native cursor factory")));
236        let screens: Rc<dyn IScreenImpl> = {
237            let factory = factory.clone();
238            ScreenImpl::new(move |events| factory.create_screens(Some(events)).check())
239        };
240        let keyboard_device: Rc<dyn IKeyboardDevice> = Self::keyboard_device();
241        let platform_settings: Rc<dyn IPlatformSettings> =
242            NativePlatformSettings::new(factory.create_platform_settings().check().expect("the native settings"));
243        let windowing_platform: Rc<dyn IWindowingPlatform> = self.clone();
244        let render_timer = Arc::new(FerroNativeRenderTimer::new(
245            factory.create_platform_render_timer().check().expect("the native render timer"),
246        ));
247        let render_loop: Arc<dyn IRenderLoop> = RenderLoop::from_timer(ThreadProxyRenderTimer::new(render_timer));
248        let lifetime_events: Rc<dyn IPlatformLifetimeEventsImpl> = application_platform;
249        let application_commands: Rc<dyn INativeApplicationCommands> = Rc::new(MacOSNativeMenuCommands::new(
250            factory.create_application_commands().check().expect("the native application commands"),
251        ));
252
253        let activatable_lifetime = Rc::new(MacOSActivatableLifetime::new());
254
255        locator
256            .bind::<dyn ICursorFactory>()
257            .to_constant(cursor_factory)
258            .bind::<dyn IScreenImpl>()
259            .to_constant(screens)
260            .bind::<dyn IPlatformIconLoader>()
261            .to_singleton::<IconLoader>(|loader| loader)
262            .bind::<dyn IKeyboardDevice>()
263            .to_constant(keyboard_device)
264            .bind::<dyn IPlatformSettings>()
265            .to_constant(platform_settings)
266            .bind::<dyn IWindowingPlatform>()
267            .to_constant(windowing_platform)
268            .bind::<dyn IClipboardImpl>()
269            .to_constant(clipboard_impl)
270            .bind::<dyn IClipboard>()
271            .to_constant(clipboard)
272            .bind::<Arc<dyn IRenderLoop>>()
273            .to_constant(Rc::new(render_loop))
274            .bind::<dyn IMountedVolumeInfoProvider>()
275            .to_constant(Rc::new(MacOSMountedVolumeInfoProvider))
276            .bind::<dyn IPlatformDragSource>()
277            .to_constant(Rc::new(FerroNativeDragSource::new()))
278            .bind::<dyn IPlatformLifetimeEventsImpl>()
279            .to_constant(lifetime_events)
280            .bind::<dyn INativeApplicationCommands>()
281            .to_constant(application_commands)
282            .bind::<dyn IActivatableLifetime>()
283            .to_constant(activatable_lifetime.clone())
284            // With its concrete type, for the application events of the
285            // native side: they only go to the lifetime of this backend.
286            .bind_to_self(activatable_lifetime);
287
288        let storage_provider_api = StorageProviderApi::new(
289            factory.create_storage_provider().check().expect("the native storage provider"),
290            options.app_sandbox_enabled,
291        );
292        let storage_provider_factory: Rc<dyn IStorageProviderFactory> = storage_provider_api.clone();
293        locator
294            .bind::<dyn IStorageProviderFactory>()
295            .to_constant(storage_provider_factory)
296            // With its concrete type, for the receivers of files of this
297            // backend (file activation, the clipboard): they ask for the
298            // storage API of this backend only.
299            .bind_to_self(storage_provider_api);
300
301        let mut hotkeys =
302            PlatformHotkeyConfiguration::with_modifiers(KeyModifiers::META, KeyModifiers::SHIFT, KeyModifiers::ALT);
303        let command_modifiers = hotkeys.command_modifiers;
304        let selection_modifiers = hotkeys.selection_modifiers;
305        hotkeys.move_cursor_to_the_start_of_line.push(KeyGesture::new(Key::Left, command_modifiers));
306        hotkeys
307            .move_cursor_to_the_start_of_line_with_selection
308            .push(KeyGesture::new(Key::Left, command_modifiers | selection_modifiers));
309        hotkeys.move_cursor_to_the_end_of_line.push(KeyGesture::new(Key::Right, command_modifiers));
310        hotkeys
311            .move_cursor_to_the_end_of_line_with_selection
312            .push(KeyGesture::new(Key::Right, command_modifiers | selection_modifiers));
313
314        locator.bind_to_self(Rc::new(hotkeys));
315
316        locator.bind_to_self(Rc::new(KeyGestureFormatInfo::new(
317            Some(HashMap::from([
318                (Key::Back, "⌫".to_string()),
319                (Key::Down, "↓".to_string()),
320                (Key::End, "↘".to_string()),
321                (Key::Escape, "⎋".to_string()),
322                (Key::Home, "↖".to_string()),
323                (Key::Left, "←".to_string()),
324                (Key::Return, "↩".to_string()),
325                (Key::PageDown, "⇟".to_string()),
326                (Key::PageUp, "⇞".to_string()),
327                (Key::Right, "→".to_string()),
328                (Key::Space, "␣".to_string()),
329                (Key::Tab, "⇥".to_string()),
330                (Key::Up, "↑".to_string()),
331            ])),
332            "⌘",
333            "⌃",
334            "⌥",
335            "⇧",
336        )));
337
338        for mode in &options.rendering_mode {
339            match mode {
340                FerroNativeRenderingMode::OpenGl => {
341                    // A failure is ignored, as in the original: the next
342                    // mode is tried.
343                    let Ok(Some(display)) = factory.obtain_gl_display() else {
344                        continue;
345                    };
346                    let Ok(gl) = FerroNativeGlPlatformGraphics::new(display, &factory) else {
347                        continue;
348                    };
349                    *self.platform_graphics.borrow_mut() = Some(Arc::new(gl));
350                    break;
351                }
352                FerroNativeRenderingMode::Metal => {
353                    let Ok(metal) = MetalPlatformGraphics::new(&factory) else {
354                        continue;
355                    };
356                    let Ok(context) = metal.try_create_context() else {
357                        continue;
358                    };
359                    context.dispose();
360                    *self.platform_graphics.borrow_mut() = Some(Arc::new(metal));
361                    break;
362                }
363                FerroNativeRenderingMode::Software => break,
364            }
365        }
366
367        let platform_graphics = self.platform_graphics.borrow().clone();
368        if let Some(platform_graphics) = &platform_graphics {
369            // One object for the services of this thread and for the
370            // thread that renders, as in the reference: shared by its
371            // contract.
372            locator.bind::<Arc<dyn IPlatformGraphics>>().to_constant(Rc::new(platform_graphics.clone()));
373        }
374
375        // The render loop of this platform runs on the thread of the render
376        // timer, so the compositor renders there; this thread renders at the
377        // synchronous points, as the reference does on this platform.
378        let compositor = Compositor::new(platform_graphics, true);
379        COMPOSITOR.with(|slot| *slot.borrow_mut() = Some(compositor.clone()));
380        locator.bind_to_self(compositor);
381    }
382
383    /// Releases the native factory. A host calls this before the process
384    /// exits; the native side calls it when the system terminates the
385    /// application.
386    pub fn dispose(&self) {
387        if self.is_disposed.replace(true) {
388            return;
389        }
390
391        let exporter = self.application_menu_exporter.borrow_mut().take();
392        drop(exporter);
393        let exporter = self.application_dock_menu_exporter.borrow_mut().take();
394        drop(exporter);
395        let factory = self.factory.borrow_mut().take();
396        drop(factory);
397    }
398
399    fn options(&self) -> FerroNativePlatformOptions {
400        self.options.borrow().clone().unwrap_or_default()
401    }
402}
403
404impl IWindowingPlatform for FerroNativePlatform {
405    fn create_window(&self) -> Rc<dyn IWindowImpl> {
406        WindowImpl::new(self.factory(), self.options())
407    }
408
409    fn create_embeddable_top_level(&self) -> Rc<dyn ITopLevelImpl> {
410        EmbeddableTopLevelImpl::new(self.factory())
411    }
412
413    fn create_embeddable_window(&self) -> Rc<dyn IWindowImpl> {
414        panic!("The method or operation is not implemented.");
415    }
416
417    fn create_tray_icon(&self) -> Option<Rc<dyn ITrayIconImpl>> {
418        Some(Rc::new(TrayIconImpl::new(&self.factory())))
419    }
420
421    fn get_windows_z_order(&self, windows: &[Rc<dyn IWindowImpl>], z_order: &mut [i64]) {
422        for (window, z_order) in windows.iter().zip(z_order.iter_mut()) {
423            *z_order = window
424                .as_any()
425                .downcast_ref::<WindowImpl>()
426                .and_then(WindowImpl::z_order)
427                .map_or(0, |z_order| z_order as i64);
428        }
429    }
430}