1use std::fmt;
5use std::str::FromStr;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9#[repr(i32)]
10pub enum IosRenderingMode {
11 OpenGl = 1,
14
15 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
28impl 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#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct IosPlatformOptions {
47 pub rendering_mode: Vec<IosRenderingMode>,
52
53 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#[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 pub trait IosApplicationExtensions {
131 fn use_ios(&self) -> AppBuilder;
135
136 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 #[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 eagl_graphics: Option<Arc<EaglPlatformGraphics>>,
169 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 static STATE: RefCell<PlatformState> = RefCell::new(PlatformState::default());
181 }
182
183 pub struct Platform;
185
186 impl Platform {
187 pub fn options() -> Option<Rc<IosPlatformOptions>> {
189 STATE.with(|state| state.borrow().options.clone())
190 }
191
192 pub fn graphics() -> Option<Arc<dyn IPlatformGraphics>> {
194 STATE.with(|state| state.borrow().graphics.clone())
195 }
196
197 pub(crate) fn eagl_graphics() -> Option<Arc<EaglPlatformGraphics>> {
200 STATE.with(|state| state.borrow().eagl_graphics.clone())
201 }
202
203 pub fn rendering_mode() -> Option<IosRenderingMode> {
207 STATE.with(|state| state.borrow().rendering_mode)
208 }
209
210 pub fn timer() -> Option<Arc<DisplayLinkTimer>> {
212 STATE.with(|state| state.borrow().timer.clone())
213 }
214
215 pub(crate) fn compositor() -> Option<Rc<Compositor>> {
217 STATE.with(|state| state.borrow().compositor.clone())
218 }
219
220 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 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 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 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 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}