Skip to main content

ferroui_native/
lib.rs

1//! ferroui-native
2//!
3//! macOS native windowing backend: the Objective-C++ library in
4//! `native/FerroUI.Native` (compiled and linked by `build.rs`), the raw
5//! COM bindings generated from `frn.idl` and the platform implementation on
6//! top of them: dispatcher, render timer, windows, popups, screens, cursors,
7//! platform settings and the Metal and OpenGL platform graphics.
8//!
9//! An application selects the backend with
10//! [`FerroNativePlatformExtensions::use_ferro_native`] on its application
11//! builder; a host without one calls [`FerroNativePlatform::initialize`].
12//!
13//! The crate is empty on every other target.
14
15#[cfg(target_os = "macos")]
16mod callback_base;
17#[cfg(target_os = "macos")]
18mod clipboard_data_format_helper;
19#[cfg(target_os = "macos")]
20mod clipboard_data_transfer;
21#[cfg(target_os = "macos")]
22mod clipboard_data_transfer_item;
23#[cfg(target_os = "macos")]
24mod clipboard_impl;
25#[cfg(target_os = "macos")]
26mod clipboard_read_session;
27#[cfg(target_os = "macos")]
28mod cursor;
29#[cfg(target_os = "macos")]
30mod data_transfer_item_to_frn_clipboard_data_item_wrapper;
31#[cfg(target_os = "macos")]
32mod data_transfer_to_frn_clipboard_data_source_wrapper;
33#[cfg(target_os = "macos")]
34mod deferred_framebuffer;
35#[cfg(target_os = "macos")]
36mod dispatcher_impl;
37#[cfg(target_os = "macos")]
38mod double_click_helper;
39#[cfg(target_os = "macos")]
40mod embeddable_top_level_impl;
41#[cfg(target_os = "macos")]
42mod extensions;
43#[cfg(target_os = "macos")]
44mod ferro_native_application_platform;
45#[cfg(target_os = "macos")]
46mod ferro_native_drag_source;
47#[cfg(target_os = "macos")]
48mod ferro_native_gl_platform_graphics;
49#[cfg(target_os = "macos")]
50mod ferro_native_menu_exporter;
51#[cfg(target_os = "macos")]
52mod ferro_native_platform;
53mod ferro_native_platform_extensions;
54#[cfg(target_os = "macos")]
55mod ferro_native_render_timer;
56#[cfg(target_os = "macos")]
57mod ferro_native_text_input_method;
58#[cfg(target_os = "macos")]
59mod frn_automation_peer;
60#[cfg(target_os = "macos")]
61mod frn_dispatcher;
62#[cfg(target_os = "macos")]
63mod frn_menu;
64#[cfg(target_os = "macos")]
65mod frn_menu_item;
66#[cfg(target_os = "macos")]
67mod frn_string;
68#[cfg(target_os = "macos")]
69mod gpu_handle_wrap_feature;
70#[cfg(target_os = "macos")]
71mod helpers;
72#[cfg(target_os = "macos")]
73mod icon_loader;
74#[cfg(target_os = "macos")]
75mod mac_os_activatable_lifetime;
76
77#[cfg(target_os = "macos")]
78mod mac_os_mounted_volume_info_provider;
79#[cfg(target_os = "macos")]
80mod mac_os_native_menu_commands;
81#[cfg(target_os = "macos")]
82mod menu_action_callback;
83#[cfg(target_os = "macos")]
84mod metal;
85#[cfg(target_os = "macos")]
86mod native_control_host_impl;
87
88#[cfg(target_os = "macos")]
89mod native_platform_settings;
90#[cfg(target_os = "macos")]
91mod platform_behavior_inhibition;
92#[cfg(target_os = "macos")]
93mod popup_impl;
94#[cfg(target_os = "macos")]
95mod predicate_callback;
96#[cfg(target_os = "macos")]
97mod screen_impl;
98#[cfg(target_os = "macos")]
99mod storage_item;
100
101#[cfg(target_os = "macos")]
102mod storage_provider_api;
103
104#[cfg(target_os = "macos")]
105mod storage_provider_impl;
106
107#[cfg(target_os = "macos")]
108mod top_level_impl;
109#[cfg(target_os = "macos")]
110mod tray_icon_impl;
111#[cfg(target_os = "macos")]
112mod window_impl;
113#[cfg(target_os = "macos")]
114mod window_impl_base;
115
116pub use ferro_native_platform_extensions::{FerroNativePlatformOptions, FerroNativeRenderingMode, MacOSPlatformOptions};
117
118#[cfg(target_os = "macos")]
119pub use self::{
120    clipboard_impl::ClipboardImpl,
121    cursor::{CursorFactory, FerroNativeCursor},
122    dispatcher_impl::DispatcherImpl,
123    embeddable_top_level_impl::EmbeddableTopLevelImpl,
124    ferro_native_application_platform::FerroNativeApplicationPlatform,
125    ferro_native_drag_source::FerroNativeDragSource,
126    ferro_native_gl_platform_graphics::{
127        FerroNativeGlPlatformGraphics, GlContext, GlDisplay, GlPlatformSurface, GlPlatformSurfaceRenderTarget,
128        GlPlatformSurfaceRenderingSession,
129    },
130    ferro_native_menu_exporter::FerroNativeMenuExporter,
131    ferro_native_platform::FerroNativePlatform,
132    ferro_native_render_timer::FerroNativeRenderTimer,
133    ferro_native_text_input_method::FerroNativeTextInputMethod,
134    frn_string::{frn_string_array_to_vec, frn_string_bytes, frn_string_to_string, FrnString, FrnStringArray},
135    icon_loader::IconLoader,
136    ferro_native_platform_extensions::FerroNativePlatformExtensions,
137    mac_os_activatable_lifetime::MacOSActivatableLifetime,
138    mac_os_native_menu_commands::MacOSNativeMenuCommands,
139    metal::{MetalDevice, MetalDrawingSession, MetalPlatformGraphics, MetalPlatformSurface, MetalRenderTarget},
140    native_platform_settings::NativePlatformSettings,
141    platform_behavior_inhibition::PlatformBehaviorInhibition,
142    popup_impl::PopupImpl,
143    screen_impl::{NativeScreen, ScreenImpl},
144    top_level_impl::{MacOSTopLevelHandle, TopLevelFramebufferSurface, TopLevelImpl},
145    tray_icon_impl::TrayIconImpl,
146    window_impl::WindowImpl,
147    window_impl_base::WindowBaseImpl,
148};
149
150/// Raw COM bindings generated from `frn.idl` by `microcom-codegen`.
151#[cfg(target_os = "macos")]
152#[allow(non_camel_case_types, non_snake_case, clippy::all)]
153pub mod interop {
154    include!(concat!(env!("OUT_DIR"), "/frn_interop.rs"));
155
156    extern "C" {
157        /// Entry point of the native library (`main.mm`). Returns a new
158        /// factory with one reference owned by the caller.
159        pub fn CreateFerroNative() -> *mut IFerroNativeFactory;
160    }
161
162    impl IFrnWindowBase {
163        /// `GetFrameSize` with the value the caller starts the result with:
164        /// the native side leaves the result as it is when the window has no
165        /// frame yet (it is not shown), and the reference starts it at
166        /// (-1, -1) to tell that apart (`WindowImplBase.FrameSize`). The
167        /// generated [`get_frame_size`](Self::get_frame_size) starts it at
168        /// zero, which reads as a frame of no size.
169        pub fn get_frame_size_from(&self, initial: FrnSize) -> Result<FrnSize, ferroui_microcom::HResult> {
170            let mut result = initial;
171            // SAFETY: the slot of `GetFrameSize` of the table of this live
172            // interface, with a place for the result that outlives the call.
173            unsafe {
174                ferroui_microcom::HResult::check(((*self.vtbl).GetFrameSize)(
175                    self as *const Self as *mut ::core::ffi::c_void,
176                    &mut result,
177                ))?;
178            }
179            Ok(result)
180        }
181    }
182
183    /// Creates the native factory, the root object of the backend.
184    pub fn create_ferro_native() -> Option<ComPtr<IFerroNativeFactory>> {
185        // SAFETY: the function returns null or an owned factory reference.
186        unsafe { ComPtr::from_raw(CreateFerroNative()) }
187    }
188}
189
190#[cfg(all(test, target_os = "macos"))]
191mod menu_tests;
192
193#[cfg(all(test, target_os = "macos"))]
194mod tests {
195    use super::interop::*;
196    use ferroui_microcom::{ComPtr, HResult, IUnknown, Interface};
197    use std::cell::Cell;
198    use std::ffi::CStr;
199    use std::mem::size_of;
200    use std::rc::Rc;
201
202    const PTR: usize = size_of::<usize>();
203
204    /// Slots are laid out linearly: IUnknown (3) + every interface in the
205    /// chain, in declaration order (also for `cpp-virtual-inherits`).
206    #[test]
207    fn vtable_sizes() {
208        assert_eq!(size_of::<IFrnStringVtbl>(), (3 + 2) * PTR);
209        assert_eq!(size_of::<IFrnTopLevelVtbl>(), (3 + 16) * PTR);
210        assert_eq!(size_of::<IFrnWindowBaseVtbl>(), (3 + 16 + 17) * PTR);
211        assert_eq!(size_of::<IFrnPopupVtbl>(), (3 + 16 + 17 + 1) * PTR);
212        assert_eq!(size_of::<IFrnWindowVtbl>(), (3 + 16 + 17 + 14) * PTR);
213        assert_eq!(size_of::<IFrnWindowEventsVtbl>(), (3 + 11 + 3 + 3) * PTR);
214        assert_eq!(size_of::<IFrnWindow>(), PTR);
215    }
216
217    #[test]
218    fn struct_layouts_match_cpp() {
219        assert_eq!(size_of::<FrnSize>(), 16);
220        assert_eq!(size_of::<FrnRect>(), 32);
221        assert_eq!(size_of::<FrnColor>(), 4);
222        assert_eq!(size_of::<FrnScreen>(), 32 + 32 + 4 + 4 + 4 + 4);
223        assert_eq!(size_of::<FrnFramebuffer>(), 8 + 4 + 4 + 4 + 4 + 16 + 4 + 4);
224        assert_eq!(size_of::<FrnKey>(), 4);
225        assert_eq!(FrnLandmarkType::LandmarkNone.0, -1);
226        assert_eq!(FrnLandmarkType::LandmarkBanner.0, 0);
227        assert_eq!(FrnKey::FrnKeyEnter, FrnKey::FrnKeyReturn);
228        assert_eq!(format!("{:?}", FrnWindowState(99)), "FrnWindowState(99)");
229    }
230
231    struct Source {
232        calls: Rc<Cell<u32>>,
233    }
234    impl IFrnClipboardDataSourceImpl for Source {
235        fn get_item_count(&self) -> i32 {
236            self.calls.set(self.calls.get() + 1);
237            7
238        }
239        fn get_item(&self, index: i32) -> Result<Option<ComPtr<IFrnClipboardDataItem>>, HResult> {
240            if index == 0 {
241                Ok(None)
242            } else {
243                Err(HResult::INVALIDARG)
244            }
245        }
246    }
247
248    /// Rust implementation called back through its own COM vtable.
249    #[test]
250    fn rust_callback_roundtrip() {
251        let calls = Rc::new(Cell::new(0));
252        let source = IFrnClipboardDataSource::from_impl(Source { calls: calls.clone() });
253        assert_eq!(source.get_item_count(), 7);
254        assert_eq!(calls.get(), 1);
255        assert_eq!(source.get_item(0), Ok(None));
256        assert_eq!(source.get_item(3), Err(HResult::INVALIDARG));
257
258        // QueryInterface: own IID (both spellings) and IUnknown, nothing else.
259        assert!(source.cast::<IFrnClipboardDataSource>().is_ok());
260        assert!(source.cast::<IUnknown>().is_ok());
261        assert_eq!(source.cast::<IFrnClipboard>().unwrap_err(), HResult::NOINTERFACE);
262        assert!(IFrnWindowEvents::matches_iid(&IFrnTopLevelEvents::IID));
263        assert!(!IFrnTopLevelEvents::matches_iid(&IFrnWindowEvents::IID));
264        assert_eq!(Rc::strong_count(&calls), 2);
265        drop(source);
266        assert_eq!(Rc::strong_count(&calls), 1, "implementation dropped with the last reference");
267    }
268
269    struct Peer;
270    impl IFrnTextInputMethodClientImpl for Peer {
271        fn set_preedit_text(&self, preedit_text: Option<&CStr>) {
272            assert_eq!(preedit_text, Some(c"abc"));
273        }
274        fn select_in_surrounding_text(&self, start: i32, end: i32) {
275            assert_eq!((start, end), (1, 2));
276        }
277    }
278
279    #[test]
280    fn rc_and_string_arguments() {
281        let client = IFrnTextInputMethodClient::from_impl(Rc::new(Peer));
282        client.set_preedit_text(Some(c"abc"));
283        client.select_in_surrounding_text(1, 2);
284    }
285
286    /// Talks to the real native library without needing a window server
287    /// connection: `GetMacOptions` only allocates a C++ object.
288    #[test]
289    fn native_factory_basic_calls() {
290        let factory = create_ferro_native().expect("factory");
291        let options = factory.get_mac_options().expect("mac options");
292        options.set_disable_set_process_name(1).unwrap();
293        // Native QueryInterface with the IID as materialized by com.h.
294        assert!(factory.cast::<IFerroNativeFactory>().is_ok());
295        assert!(factory.cast::<IUnknown>().is_ok());
296        assert_eq!(factory.cast::<IFrnWindow>().unwrap_err(), HResult::NOINTERFACE);
297        let helper = factory.create_memory_management_helper().unwrap().expect("helper");
298        drop(helper);
299    }
300}