Skip to main content

ferroui_ios/
platform.rs

1//! The platform: its options, `use_ios`, and the registration of the
2//! services of the platform.
3
4use std::fmt;
5use std::str::FromStr;
6
7/// Represents the rendering mode for platform graphics.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9#[repr(i32)]
10pub enum IosRenderingMode {
11    /// Enables EAGL rendering for iOS and tvOS. Not supported on Mac
12    /// Catalyst.
13    OpenGl = 1,
14
15    /// Enables Metal rendering for all Apple targets.
16    Metal,
17}
18
19impl fmt::Display for IosRenderingMode {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        f.write_str(match self {
22            IosRenderingMode::OpenGl => "OpenGl",
23            IosRenderingMode::Metal => "Metal",
24        })
25    }
26}
27
28/// The mode of a name, as `Display` writes it, in any case: what parsing
29/// the name of a member of an enumeration does in the reference. An
30/// addition of the port, for an application that reads the modes from its
31/// environment.
32impl FromStr for IosRenderingMode {
33    type Err = String;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        match s.trim() {
37            name if name.eq_ignore_ascii_case("OpenGl") => Ok(IosRenderingMode::OpenGl),
38            name if name.eq_ignore_ascii_case("Metal") => Ok(IosRenderingMode::Metal),
39            name => Err(format!("Requested value '{name}' was not found.")),
40        }
41    }
42}
43
44/// iOS backend options.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct IosPlatformOptions {
47    /// The rendering modes with fallbacks. The first element has the
48    /// highest priority. The default value is Metal, then OpenGL.
49    ///
50    /// The platform fails to start when no value could be applied.
51    pub rendering_mode: Vec<IosRenderingMode>,
52
53    /// Gets or sets whether rendering follows the display refresh rate, up to 120 Hz on ProMotion
54    /// displays. The default value is true.
55    ///
56    /// On iPhone, rates above 60 Hz also need CADisableMinimumFrameDurationOnPhone set to true in
57    /// the app's Info.plist. Set this to false to keep the system's default rate, for example if
58    /// the app can't sustain high frame rates.
59    pub enable_high_refresh_rate: bool,
60}
61
62impl Default for IosPlatformOptions {
63    fn default() -> Self {
64        Self { rendering_mode: vec![IosRenderingMode::Metal, IosRenderingMode::OpenGl], enable_high_refresh_rate: true }
65    }
66}
67
68/// Walks the rendering modes of the options and returns the graphics of
69/// the first one that can be created.
70///
71/// `try_create` answers for one mode. The errors are the messages the
72/// platform fails with.
73#[cfg_attr(not(target_os = "ios"), allow(dead_code))]
74pub(crate) fn initialize_graphics_with<T>(
75    opts: &IosPlatformOptions,
76    try_create: impl Fn(IosRenderingMode) -> Option<T>,
77) -> Result<T, String> {
78    if opts.rendering_mode.is_empty() {
79        return Err("IosPlatformOptions.rendering_mode must not be empty or null".to_string());
80    }
81
82    for rendering_mode in &opts.rendering_mode {
83        if let Some(graphics) = try_create(*rendering_mode) {
84            return Ok(graphics);
85        }
86    }
87
88    let modes: Vec<String> = opts.rendering_mode.iter().map(ToString::to_string).collect();
89    Err(format!(
90        "IosPlatformOptions.rendering_mode has a value of \"{}\", but no options were applied.",
91        modes.join(", ")
92    ))
93}
94
95#[cfg(target_os = "ios")]
96pub use uikit::{IosApplicationExtensions, Platform};
97
98#[cfg(target_os = "ios")]
99mod uikit {
100    use super::{initialize_graphics_with, IosPlatformOptions, IosRenderingMode};
101    use crate::activatable_lifetime::ActivatableLifetime;
102    use crate::dispatcher_impl::DispatcherImpl;
103    use crate::display_link_timer::DisplayLinkTimer;
104    use crate::eagl::EaglPlatformGraphics;
105    use crate::ferro_app_delegate::IFerroAppDelegate;
106    use crate::ios_screens::IosScreens;
107    use crate::metal::MetalPlatformGraphics;
108    use crate::platform_settings::PlatformSettings;
109    use crate::stubs::{CursorFactoryStub, PlatformIconLoaderStub, WindowingPlatformStub};
110    use ferroui_base::input::platform::{KeyGestureFormatInfo, PlatformHotkeyConfiguration};
111    use ferroui_base::input::{IKeyboardDevice, Key, KeyModifiers, KeyboardDevice};
112    use ferroui_base::platform::{ICursorFactory, IPlatformGraphics, IPlatformSettings};
113    use ferroui_base::rendering::composition::Compositor;
114    use ferroui_base::rendering::{IRenderLoop, IRenderTimer, RenderLoop};
115    use ferroui_base::threading::Dispatcher;
116    use ferroui_base::{FerroLocator, LocatorExtensions};
117    use ferroui_controls::application_lifetimes::IActivatableLifetime;
118    use ferroui_controls::platform::{IPlatformIconLoader, IScreenImpl, IWindowingPlatform};
119    use ferroui_controls::AppBuilder;
120    #[cfg(not(ferroui_docsrs))]
121    use ferroui_harfbuzz::HarfBuzzApplicationExtensions;
122    #[cfg(not(ferroui_docsrs))]
123    use ferroui_skia::SkiaApplicationExtensions;
124    use std::cell::RefCell;
125    use std::collections::HashMap;
126    use std::rc::Rc;
127    use std::sync::Arc;
128
129    /// Selects the iOS platform for an application.
130    pub trait IosApplicationExtensions {
131        /// Uses the iOS platform, with the Skia renderer and the HarfBuzz
132        /// text shaper, without an application delegate (an application
133        /// that embeds views in a UIKit application of its own).
134        fn use_ios(&self) -> AppBuilder;
135
136        /// Uses the iOS platform with the application delegate whose
137        /// activations the activatable lifetime of the application reports.
138        fn use_ios_with_delegate(&self, app_delegate: Rc<dyn IFerroAppDelegate>) -> AppBuilder;
139    }
140
141    fn use_ios(builder: &AppBuilder, app_delegate: Option<Rc<dyn IFerroAppDelegate>>) -> AppBuilder {
142        let builder = builder
143            .use_standard_runtime_platform_subsystem()
144            .use_windowing_subsystem(move || Platform::register(app_delegate.clone()), "iOS");
145        // The documentation build of docs.rs has neither (the `ferroui_docsrs` configuration of
146        // the manifest); every other build selects both.
147        #[cfg(not(ferroui_docsrs))]
148        let builder = builder.use_harfbuzz().use_skia();
149        builder
150    }
151
152    impl IosApplicationExtensions for AppBuilder {
153        fn use_ios(&self) -> AppBuilder {
154            use_ios(self, None)
155        }
156
157        fn use_ios_with_delegate(&self, app_delegate: Rc<dyn IFerroAppDelegate>) -> AppBuilder {
158            use_ios(self, Some(app_delegate))
159        }
160    }
161
162    #[derive(Default)]
163    struct PlatformState {
164        options: Option<Rc<IosPlatformOptions>>,
165        graphics: Option<Arc<dyn IPlatformGraphics>>,
166        /// The graphics again when they are those of EAGL: what the
167        /// reference finds by testing the type of the graphics.
168        eagl_graphics: Option<Arc<EaglPlatformGraphics>>,
169        /// The rendering mode the graphics are of.
170        rendering_mode: Option<IosRenderingMode>,
171        timer: Option<Arc<DisplayLinkTimer>>,
172        compositor: Option<Rc<Compositor>>,
173        settings: Option<Rc<PlatformSettings>>,
174    }
175
176    ferroui_base::ferro_thread_local! {
177        /// The state of the platform: what the reference keeps in static
178        /// fields. The platform is registered on the main thread, and its
179        /// state is read there.
180        static STATE: RefCell<PlatformState> = RefCell::new(PlatformState::default());
181    }
182
183    /// The iOS platform.
184    pub struct Platform;
185
186    impl Platform {
187        /// The options the platform was registered with.
188        pub fn options() -> Option<Rc<IosPlatformOptions>> {
189            STATE.with(|state| state.borrow().options.clone())
190        }
191
192        /// The graphics of the platform.
193        pub fn graphics() -> Option<Arc<dyn IPlatformGraphics>> {
194            STATE.with(|state| state.borrow().graphics.clone())
195        }
196
197        /// The graphics of the platform when they are those of OpenGL ES
198        /// over EAGL.
199        pub(crate) fn eagl_graphics() -> Option<Arc<EaglPlatformGraphics>> {
200            STATE.with(|state| state.borrow().eagl_graphics.clone())
201        }
202
203        /// The rendering mode of the graphics of the platform: the first
204        /// mode of the options that could be applied. An addition of the
205        /// port, for an application that tests itself.
206        pub fn rendering_mode() -> Option<IosRenderingMode> {
207            STATE.with(|state| state.borrow().rendering_mode)
208        }
209
210        /// The render timer of the platform.
211        pub fn timer() -> Option<Arc<DisplayLinkTimer>> {
212            STATE.with(|state| state.borrow().timer.clone())
213        }
214
215        /// The compositor of the platform.
216        pub(crate) fn compositor() -> Option<Rc<Compositor>> {
217            STATE.with(|state| state.borrow().compositor.clone())
218        }
219
220        /// The settings of the platform, once the platform is registered.
221        /// They are created when first asked for, here or through the
222        /// locator. The reference finds them in the locator and casts
223        /// them to their class; the contract of the port has no cast, so
224        /// the platform keeps the object it registers.
225        pub(crate) fn settings() -> Option<Rc<PlatformSettings>> {
226            STATE.with(|state| {
227                let mut state = state.borrow_mut();
228                state.options.as_ref()?;
229                Some(state.settings.get_or_insert_with(PlatformSettings::new).clone())
230            })
231        }
232
233        /// Registers the services of the platform.
234        ///
235        /// # Panics
236        /// Panics when none of the rendering modes of the options can be
237        /// applied.
238        pub fn register(app_delegate: Option<Rc<dyn IFerroAppDelegate>>) {
239            let options =
240                FerroLocator::current().get_service::<IosPlatformOptions>().unwrap_or_default();
241
242            let (graphics, eagl_graphics, rendering_mode) = Self::initialize_graphics(&options);
243            // The timer reads the options of the platform.
244            STATE.with(|state| state.borrow_mut().options = Some(options.clone()));
245            let timer = match Self::timer() {
246                Some(timer) => timer,
247                None => DisplayLinkTimer::new(),
248            };
249            let keyboard = KeyboardDevice::new();
250
251            STATE.with(|state| {
252                let mut state = state.borrow_mut();
253                state.options = Some(options);
254                state.graphics = Some(graphics.clone());
255                state.eagl_graphics = eagl_graphics;
256                state.rendering_mode = Some(rendering_mode);
257                state.timer = Some(timer.clone());
258            });
259
260            Dispatcher::initialize_ui_thread_dispatcher(DispatcherImpl::instance());
261
262            let cursor_factory: Rc<dyn ICursorFactory> = Rc::new(CursorFactoryStub);
263            let windowing_platform: Rc<dyn IWindowingPlatform> = Rc::new(WindowingPlatformStub);
264            let icon_loader: Rc<dyn IPlatformIconLoader> = Rc::new(PlatformIconLoaderStub);
265            let key_names = HashMap::from([
266                (Key::Back, "\u{232B}".to_string()),
267                (Key::Down, "\u{2193}".to_string()),
268                (Key::End, "\u{2198}".to_string()),
269                (Key::Escape, "\u{238B}".to_string()),
270                (Key::Home, "\u{2196}".to_string()),
271                (Key::Left, "\u{2190}".to_string()),
272                (Key::Return, "\u{21A9}".to_string()),
273                (Key::PageDown, "\u{21DF}".to_string()),
274                (Key::PageUp, "\u{21DE}".to_string()),
275                (Key::Right, "\u{2192}".to_string()),
276                (Key::Space, "\u{2423}".to_string()),
277                (Key::Tab, "\u{21E5}".to_string()),
278                (Key::Up, "\u{2191}".to_string()),
279            ]);
280            let render_timer: Arc<dyn IRenderTimer> = timer;
281            let render_loop: Arc<dyn IRenderLoop> = RenderLoop::from_timer(render_timer);
282            let keyboard: Rc<dyn IKeyboardDevice> = keyboard;
283
284            let locator = FerroLocator::current_mutable();
285            locator
286                .bind::<Arc<dyn IPlatformGraphics>>()
287                .to_constant(Rc::new(graphics.clone()))
288                .bind::<dyn ICursorFactory>()
289                .to_constant(cursor_factory)
290                .bind::<dyn IWindowingPlatform>()
291                .to_constant(windowing_platform)
292                .bind::<dyn IPlatformSettings>()
293                .to_lazy(|| Platform::settings().map(|settings| settings as Rc<dyn IPlatformSettings>))
294                .bind::<dyn IPlatformIconLoader>()
295                .to_constant(icon_loader)
296                .bind::<dyn IScreenImpl>()
297                .to_lazy(|| Some(IosScreens::new() as Rc<dyn IScreenImpl>))
298                .bind_to_self(Rc::new(PlatformHotkeyConfiguration::new(KeyModifiers::CONTROL)))
299                .bind_to_self(Rc::new(KeyGestureFormatInfo::new(
300                    Some(key_names),
301                    "\u{2318}",
302                    "\u{2303}",
303                    "\u{2325}",
304                    "\u{21E7}",
305                )))
306                .bind::<Arc<dyn IRenderLoop>>()
307                .to_constant(Rc::new(render_loop))
308                .bind::<dyn IKeyboardDevice>()
309                .to_constant(keyboard);
310
311            if let Some(app_delegate) = app_delegate {
312                let lifetime: Rc<dyn IActivatableLifetime> = ActivatableLifetime::new(&*app_delegate);
313                locator.bind::<dyn IActivatableLifetime>().to_constant(lifetime);
314            }
315
316            let compositor = Compositor::new(Some(graphics), false);
317            STATE.with(|state| state.borrow_mut().compositor = Some(compositor.clone()));
318            locator.bind_to_self(compositor);
319        }
320
321        /// The graphics of the first mode of the options that can be
322        /// created, the graphics of EAGL again when they are those, and the
323        /// mode.
324        fn initialize_graphics(
325            opts: &IosPlatformOptions,
326        ) -> (Arc<dyn IPlatformGraphics>, Option<Arc<EaglPlatformGraphics>>, IosRenderingMode) {
327            let graphics = initialize_graphics_with(opts, |rendering_mode| match rendering_mode {
328                IosRenderingMode::OpenGl => EaglPlatformGraphics::try_create().map(|eagl_graphics| {
329                    let graphics: Arc<dyn IPlatformGraphics> = eagl_graphics.clone();
330                    (graphics, Some(eagl_graphics), rendering_mode)
331                }),
332                IosRenderingMode::Metal => MetalPlatformGraphics::try_create().map(|metal_graphics| {
333                    let graphics: Arc<dyn IPlatformGraphics> = metal_graphics;
334                    (graphics, None, rendering_mode)
335                }),
336            });
337            match graphics {
338                Ok(graphics) => graphics,
339                Err(message) => panic!("{message}"),
340            }
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    // Not from the reference, which has no tests of this file.
348    use super::*;
349
350    #[test]
351    fn the_default_options_are_those_of_the_reference() {
352        let options = IosPlatformOptions::default();
353        assert_eq!(options.rendering_mode, vec![IosRenderingMode::Metal, IosRenderingMode::OpenGl]);
354    }
355
356    #[test]
357    fn the_modes_have_the_values_of_the_reference() {
358        assert_eq!(1, IosRenderingMode::OpenGl as i32);
359        assert_eq!(2, IosRenderingMode::Metal as i32);
360    }
361
362    #[test]
363    fn the_first_mode_that_can_be_created_is_used() {
364        let options = IosPlatformOptions {
365            rendering_mode: vec![IosRenderingMode::OpenGl, IosRenderingMode::Metal],
366            ..IosPlatformOptions::default()
367        };
368        let graphics =
369            initialize_graphics_with(&options, |mode| (mode == IosRenderingMode::Metal).then_some("metal"));
370        assert_eq!(Ok("metal"), graphics);
371
372        let graphics = initialize_graphics_with(&IosPlatformOptions::default(), |mode| Some(mode.to_string()));
373        assert_eq!(Ok("Metal".to_string()), graphics);
374    }
375
376    #[test]
377    fn a_mode_is_parsed_from_its_name_in_any_case() {
378        assert_eq!(Ok(IosRenderingMode::OpenGl), "OpenGl".parse());
379        assert_eq!(Ok(IosRenderingMode::OpenGl), " opengl ".parse());
380        assert_eq!(Ok(IosRenderingMode::Metal), "METAL".parse());
381        assert_eq!(Err("Requested value 'Vulkan' was not found.".to_string()), "Vulkan".parse::<IosRenderingMode>());
382        for mode in [IosRenderingMode::OpenGl, IosRenderingMode::Metal] {
383            assert_eq!(Ok(mode), mode.to_string().parse());
384        }
385    }
386
387    #[test]
388    fn no_mode_is_an_error() {
389        let options = IosPlatformOptions { rendering_mode: Vec::new(), ..IosPlatformOptions::default() };
390        let graphics = initialize_graphics_with(&options, |_| Some(()));
391        assert_eq!(Err("IosPlatformOptions.rendering_mode must not be empty or null".to_string()), graphics);
392    }
393
394    #[test]
395    fn modes_of_which_none_applies_are_an_error_that_names_them() {
396        let graphics = initialize_graphics_with(&IosPlatformOptions::default(), |_| None::<()>);
397        assert_eq!(
398            Err("IosPlatformOptions.rendering_mode has a value of \"Metal, OpenGl\", but no options were applied."
399                .to_string()),
400            graphics
401        );
402    }
403}