Skip to main content

ferroui_android/
android_platform.rs

1//! The Android platform: its options, the selection of the platform with
2//! the application builder, and the initialization of its services.
3
4/// Represents the rendering mode for platform graphics.
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6#[repr(i32)]
7pub enum AndroidRenderingMode {
8    /// The framework is rendered into a framebuffer.
9    Software = 1,
10
11    /// Enables android EGL rendering.
12    Egl = 2,
13
14    /// Enables Vulkan rendering
15    Vulkan = 3,
16}
17
18/// The options of the Android platform, registered with the application
19/// builder (`AppBuilder::with`).
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct AndroidPlatformOptions {
22    /// Gets or sets the rendering modes with fallbacks.
23    /// The first element in the array has the highest priority.
24    /// The default value is: [`AndroidRenderingMode::Egl`], [`AndroidRenderingMode::Software`].
25    ///
26    /// If the application should work on as wide a range of devices as
27    /// possible, at least add [`AndroidRenderingMode::Software`] as a
28    /// fallback value. The platform fails to initialize when no value was
29    /// matched.
30    pub rendering_mode: Vec<AndroidRenderingMode>,
31}
32
33impl Default for AndroidPlatformOptions {
34    fn default() -> Self {
35        Self { rendering_mode: vec![AndroidRenderingMode::Egl, AndroidRenderingMode::Software] }
36    }
37}
38
39/// The loop over the rendering modes (`InitializeGraphics`): the graphics
40/// of the first mode that initializes, `None` for the software mode.
41///
42/// `try_create_egl` creates the platform graphics of EGL, `try_create_vulkan`
43/// those of Vulkan (`VulkanSupport`, with the [`VulkanOptions`] an
44/// application registered; docs/porting/android-platform.md, section 6).
45///
46/// [`VulkanOptions`]: ferroui_vulkan::VulkanOptions
47///
48/// # Panics
49/// Panics, as the reference throws, when the list is empty and when none
50/// of its modes could be applied.
51#[cfg_attr(not(target_os = "android"), allow(dead_code))]
52pub(crate) fn initialize_graphics_core<G>(
53    opts: &AndroidPlatformOptions,
54    try_create_egl: &mut dyn FnMut() -> Option<G>,
55    try_create_vulkan: &mut dyn FnMut() -> Option<G>,
56) -> Option<G> {
57    if opts.rendering_mode.is_empty() {
58        panic!("AndroidPlatformOptions.rendering_mode must not be empty or null");
59    }
60
61    for rendering_mode in &opts.rendering_mode {
62        match rendering_mode {
63            AndroidRenderingMode::Software => return None,
64            AndroidRenderingMode::Egl => {
65                if let Some(egl) = try_create_egl() {
66                    return Some(egl);
67                }
68            }
69            AndroidRenderingMode::Vulkan => {
70                if let Some(vulkan) = try_create_vulkan() {
71                    return Some(vulkan);
72                }
73            }
74        }
75    }
76
77    panic!(
78        "AndroidPlatformOptions.rendering_mode has a value of \"{}\", but no options were applied.",
79        opts.rendering_mode.iter().map(|mode| format!("{mode:?}")).collect::<Vec<_>>().join(", ")
80    );
81}
82
83#[cfg(target_os = "android")]
84pub use imp::{AndroidApplicationExtensions, AndroidPlatform};
85
86#[cfg(target_os = "android")]
87mod imp {
88    use super::{initialize_graphics_core, AndroidPlatformOptions};
89    use crate::android_dispatcher_impl::AndroidDispatcherImpl;
90    use crate::android_egl::AndroidEglPlatformGraphics;
91    use crate::android_runtime_platform::AndroidRuntimePlatformServices;
92    use crate::choreographer_timer::ChoreographerTimer;
93    use crate::cursor_factory::CursorFactory;
94    use crate::platform::AndroidActivatableLifetime;
95    use crate::stubs::{PlatformIconLoaderStub, WindowingPlatformStub};
96    use ferroui_base::input::platform::{KeyGestureFormatInfo, PlatformHotkeyConfiguration};
97    use crate::ferro_android_application::FerroAndroidApplication;
98    use crate::platform::android_platform_settings::ApplicationSettingsContext;
99    use crate::platform::{AndroidKeyboardDevice, AndroidPlatformSettings};
100    use ferroui_base::input::IKeyboardDevice;
101    use ferroui_base::platform::{ICursorFactory, IPlatformGraphics, IPlatformSettings};
102    use ferroui_base::rendering::composition::Compositor;
103    use ferroui_base::rendering::{IRenderLoop, IRenderTimer, RenderLoop};
104    use ferroui_base::threading::Dispatcher;
105    use ferroui_base::{FerroLocator, LocatorExtensions};
106    use ferroui_controls::application_lifetimes::IActivatableLifetime;
107    use ferroui_controls::platform::{IPlatformIconLoader, IWindowingPlatform};
108    use ferroui_controls::AppBuilder;
109    use ferroui_harfbuzz::HarfBuzzApplicationExtensions;
110    use ferroui_skia::SkiaApplicationExtensions;
111    use std::cell::RefCell;
112    use std::collections::HashMap;
113    use std::rc::Rc;
114    use std::sync::Arc;
115
116    /// Selects the Android platform for an application.
117    pub trait AndroidApplicationExtensions {
118        /// Uses the Android platform, with the [`AndroidPlatformOptions`]
119        /// registered with the builder or the default ones, the HarfBuzz
120        /// text shaper and the Skia renderer.
121        fn use_android(&self) -> AppBuilder;
122    }
123
124    impl AndroidApplicationExtensions for AppBuilder {
125        fn use_android(&self) -> AppBuilder {
126            AndroidRuntimePlatformServices::use_android_runtime_platform_subsystem(self)
127                .use_windowing_subsystem(AndroidPlatform::initialize, "Android")
128                .use_harfbuzz()
129                .use_skia()
130        }
131    }
132
133    ferroui_base::ferro_thread_local! {
134        static OPTIONS: RefCell<Option<Rc<AndroidPlatformOptions>>> = const { RefCell::new(None) };
135        static COMPOSITOR: RefCell<Option<Rc<Compositor>>> = const { RefCell::new(None) };
136        static TIMER: RefCell<Option<Arc<ChoreographerTimer>>> = const { RefCell::new(None) };
137    }
138
139    /// The Android platform.
140    pub struct AndroidPlatform;
141
142    impl AndroidPlatform {
143        /// The instance of the platform. Its members are functions of the
144        /// type, so the value only names the platform.
145        pub const INSTANCE: AndroidPlatform = AndroidPlatform;
146
147        /// The options the platform was initialized with.
148        pub fn options() -> Option<Rc<AndroidPlatformOptions>> {
149            OPTIONS.with(|options| options.borrow().clone())
150        }
151
152        pub(crate) fn compositor() -> Option<Rc<Compositor>> {
153            COMPOSITOR.with(|compositor| compositor.borrow().clone())
154        }
155
156        #[allow(dead_code)]
157        pub(crate) fn timer() -> Option<Arc<ChoreographerTimer>> {
158            TIMER.with(|timer| timer.borrow().clone())
159        }
160
161        /// Registers the services of the platform with the current service
162        /// locator and installs the dispatcher of the main looper.
163        ///
164        /// # Panics
165        /// Panics on another thread than the main thread of the
166        /// application, and when the rendering modes of the options are
167        /// empty or none of them applies.
168        pub fn initialize() {
169            let options = FerroLocator::current().get_service::<AndroidPlatformOptions>().unwrap_or_default();
170            OPTIONS.with(|slot| *slot.borrow_mut() = Some(options.clone()));
171
172            Dispatcher::initialize_ui_thread_dispatcher(AndroidDispatcherImpl::new());
173            let timer = Arc::new(ChoreographerTimer::new());
174            TIMER.with(|slot| *slot.borrow_mut() = Some(timer.clone()));
175
176            let windowing_platform: Rc<dyn IWindowingPlatform> = Rc::new(WindowingPlatformStub);
177            let keyboard_device: Rc<dyn IKeyboardDevice> = AndroidKeyboardDevice::new();
178            let platform_settings: Rc<dyn IPlatformSettings> = AndroidPlatformSettings::new(
179                FerroAndroidApplication::try_context()
180                    .map(|context| Box::new(ApplicationSettingsContext::new(context)) as Box<_>),
181            );
182            let render_timer: Arc<dyn IRenderTimer> = timer;
183            let render_loop: Arc<dyn IRenderLoop> = RenderLoop::from_timer(render_timer);
184            let activatable_lifetime = AndroidActivatableLifetime::new();
185            let activatable: Rc<dyn IActivatableLifetime> = activatable_lifetime.clone();
186
187            let locator = FerroLocator::current_mutable();
188            locator
189                .bind::<dyn ICursorFactory>()
190                .to_transient::<CursorFactory>(|instance| instance)
191                .bind::<dyn IWindowingPlatform>()
192                .to_constant(windowing_platform)
193                .bind::<dyn IKeyboardDevice>()
194                .to_constant(keyboard_device)
195                .bind::<dyn IPlatformSettings>()
196                .to_constant(platform_settings)
197                .bind::<dyn IPlatformIconLoader>()
198                .to_singleton::<PlatformIconLoaderStub>(|instance| instance)
199                .bind::<Arc<dyn IRenderLoop>>()
200                .to_constant(Rc::new(render_loop))
201                .bind_to_self_singleton::<PlatformHotkeyConfiguration>()
202                .bind_to_self(Rc::new(KeyGestureFormatInfo::new(Some(HashMap::new()), "Cmd", "Ctrl", "Alt", "Shift")))
203                .bind::<dyn IActivatableLifetime>()
204                .to_constant(activatable)
205                // With its concrete type, for the activities of this backend.
206                .bind_to_self(activatable_lifetime);
207
208            let graphics = Self::initialize_graphics(&options);
209            if let Some(graphics) = &graphics {
210                locator.bind::<Arc<dyn IPlatformGraphics>>().to_constant(Rc::new(graphics.clone()));
211            }
212
213            let compositor = Compositor::new(graphics, false);
214            COMPOSITOR.with(|slot| *slot.borrow_mut() = Some(compositor.clone()));
215            locator.bind_to_self(compositor);
216        }
217
218        fn initialize_graphics(opts: &AndroidPlatformOptions) -> Option<Arc<dyn IPlatformGraphics>> {
219            initialize_graphics_core(
220                opts,
221                &mut || AndroidEglPlatformGraphics::try_create().map(|egl| Arc::new(egl) as Arc<dyn IPlatformGraphics>),
222                &mut Self::try_create_vulkan,
223            )
224        }
225
226        /// The platform graphics of Vulkan, with the options an application
227        /// registered.
228        #[cfg(feature = "vulkan")]
229        fn try_create_vulkan() -> Option<Arc<dyn IPlatformGraphics>> {
230            use crate::platform::vulkan::VulkanSupport;
231            use ferroui_vulkan::VulkanOptions;
232
233            let vulkan_options = FerroLocator::current().get_service::<VulkanOptions>().map(|options| (*options).clone());
234            VulkanSupport::try_initialize(vulkan_options).map(|vulkan| vulkan as Arc<dyn IPlatformGraphics>)
235        }
236
237        /// Without the feature `vulkan` the Skia backend has no Vulkan GPU
238        /// to draw on the graphics of Vulkan with, so the mode is passed
239        /// over, and says why.
240        #[cfg(not(feature = "vulkan"))]
241        fn try_create_vulkan() -> Option<Arc<dyn IPlatformGraphics>> {
242            use ferroui_base::logging::{LogEventLevel, Logger};
243
244            if let Some(logger) = Logger::try_get(LogEventLevel::Warning, "Vulkan") {
245                logger.log(
246                    None,
247                    "The Vulkan rendering mode needs the feature `vulkan` of ferroui-android (Skia with Vulkan); the mode is passed over",
248                );
249            }
250            None
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    // Not from the reference, which has no tests of the platform.
258    use super::*;
259
260    fn options(rendering_mode: Vec<AndroidRenderingMode>) -> AndroidPlatformOptions {
261        AndroidPlatformOptions { rendering_mode }
262    }
263
264    #[test]
265    fn the_default_order_is_egl_then_software() {
266        assert_eq!(
267            AndroidPlatformOptions::default().rendering_mode,
268            vec![AndroidRenderingMode::Egl, AndroidRenderingMode::Software]
269        );
270        assert_eq!(AndroidRenderingMode::Software as i32, 1);
271        assert_eq!(AndroidRenderingMode::Egl as i32, 2);
272        assert_eq!(AndroidRenderingMode::Vulkan as i32, 3);
273    }
274
275    #[test]
276    fn the_default_order_takes_egl_when_it_initializes() {
277        let mut asked = 0;
278        let graphics = initialize_graphics_core(
279            &AndroidPlatformOptions::default(),
280            &mut || {
281                asked += 1;
282                Some(7)
283            },
284            &mut || panic!("Vulkan is not in the default order"),
285        );
286        assert_eq!(graphics, Some(7));
287        assert_eq!(asked, 1);
288    }
289
290    #[test]
291    fn the_default_order_ends_at_software_when_egl_does_not_initialize() {
292        let mut asked = 0;
293        let graphics = initialize_graphics_core::<i32>(
294            &AndroidPlatformOptions::default(),
295            &mut || {
296                asked += 1;
297                None
298            },
299            &mut || panic!("Vulkan is not in the default order"),
300        );
301        assert_eq!(graphics, None);
302        assert_eq!(asked, 1);
303    }
304
305    #[test]
306    fn software_first_does_not_ask_for_egl() {
307        let graphics = initialize_graphics_core::<i32>(
308            &options(vec![AndroidRenderingMode::Software, AndroidRenderingMode::Egl]),
309            &mut || panic!("EGL must not be asked for"),
310            &mut || panic!("Vulkan must not be asked for"),
311        );
312        assert_eq!(graphics, None);
313    }
314
315    #[test]
316    #[should_panic(expected = "must not be empty or null")]
317    fn an_empty_list_fails() {
318        initialize_graphics_core::<i32>(&options(vec![]), &mut || None, &mut || None);
319    }
320
321    #[test]
322    #[should_panic(expected = "has a value of \"Egl, Vulkan\", but no options were applied.")]
323    fn a_list_in_which_nothing_applies_fails() {
324        initialize_graphics_core::<i32>(
325            &options(vec![AndroidRenderingMode::Egl, AndroidRenderingMode::Vulkan]),
326            &mut || None,
327            &mut || None,
328        );
329    }
330
331    #[test]
332    fn vulkan_is_taken_when_it_is_named_and_initializes() {
333        let asked = std::cell::RefCell::new(Vec::new());
334        let graphics = initialize_graphics_core(
335            &options(vec![AndroidRenderingMode::Vulkan, AndroidRenderingMode::Egl, AndroidRenderingMode::Software]),
336            &mut || {
337                asked.borrow_mut().push("egl");
338                Some(2)
339            },
340            &mut || {
341                asked.borrow_mut().push("vulkan");
342                Some(3)
343            },
344        );
345        assert_eq!(graphics, Some(3));
346        assert_eq!(*asked.borrow(), vec!["vulkan"]);
347    }
348
349    #[test]
350    fn vulkan_that_does_not_initialize_is_passed_over() {
351        let asked = std::cell::RefCell::new(Vec::new());
352        let graphics = initialize_graphics_core(
353            &options(vec![AndroidRenderingMode::Vulkan, AndroidRenderingMode::Egl]),
354            &mut || {
355                asked.borrow_mut().push("egl");
356                Some(2)
357            },
358            &mut || {
359                asked.borrow_mut().push("vulkan");
360                None
361            },
362        );
363        assert_eq!(graphics, Some(2));
364        assert_eq!(*asked.borrow(), vec!["vulkan", "egl"]);
365    }
366}