#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum AndroidRenderingMode {
Software = 1,
Egl = 2,
Vulkan = 3,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AndroidPlatformOptions {
pub rendering_mode: Vec<AndroidRenderingMode>,
}
impl Default for AndroidPlatformOptions {
fn default() -> Self {
Self { rendering_mode: vec![AndroidRenderingMode::Egl, AndroidRenderingMode::Software] }
}
}
#[cfg_attr(not(target_os = "android"), allow(dead_code))]
pub(crate) fn initialize_graphics_core<G>(
opts: &AndroidPlatformOptions,
try_create_egl: &mut dyn FnMut() -> Option<G>,
try_create_vulkan: &mut dyn FnMut() -> Option<G>,
) -> Option<G> {
if opts.rendering_mode.is_empty() {
panic!("AndroidPlatformOptions.rendering_mode must not be empty or null");
}
for rendering_mode in &opts.rendering_mode {
match rendering_mode {
AndroidRenderingMode::Software => return None,
AndroidRenderingMode::Egl => {
if let Some(egl) = try_create_egl() {
return Some(egl);
}
}
AndroidRenderingMode::Vulkan => {
if let Some(vulkan) = try_create_vulkan() {
return Some(vulkan);
}
}
}
}
panic!(
"AndroidPlatformOptions.rendering_mode has a value of \"{}\", but no options were applied.",
opts.rendering_mode.iter().map(|mode| format!("{mode:?}")).collect::<Vec<_>>().join(", ")
);
}
#[cfg(target_os = "android")]
pub use imp::{AndroidApplicationExtensions, AndroidPlatform};
#[cfg(target_os = "android")]
mod imp {
use super::{initialize_graphics_core, AndroidPlatformOptions};
use crate::android_dispatcher_impl::AndroidDispatcherImpl;
use crate::android_egl::AndroidEglPlatformGraphics;
use crate::android_runtime_platform::AndroidRuntimePlatformServices;
use crate::choreographer_timer::ChoreographerTimer;
use crate::cursor_factory::CursorFactory;
use crate::platform::AndroidActivatableLifetime;
use crate::stubs::{PlatformIconLoaderStub, WindowingPlatformStub};
use ferroui_base::input::platform::{KeyGestureFormatInfo, PlatformHotkeyConfiguration};
use crate::ferro_android_application::FerroAndroidApplication;
use crate::platform::android_platform_settings::ApplicationSettingsContext;
use crate::platform::{AndroidKeyboardDevice, AndroidPlatformSettings};
use ferroui_base::input::IKeyboardDevice;
use ferroui_base::platform::{ICursorFactory, IPlatformGraphics, IPlatformSettings};
use ferroui_base::rendering::composition::Compositor;
use ferroui_base::rendering::{IRenderLoop, IRenderTimer, RenderLoop};
use ferroui_base::threading::Dispatcher;
use ferroui_base::{FerroLocator, LocatorExtensions};
use ferroui_controls::application_lifetimes::IActivatableLifetime;
use ferroui_controls::platform::{IPlatformIconLoader, IWindowingPlatform};
use ferroui_controls::AppBuilder;
use ferroui_harfbuzz::HarfBuzzApplicationExtensions;
use ferroui_skia::SkiaApplicationExtensions;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
pub trait AndroidApplicationExtensions {
fn use_android(&self) -> AppBuilder;
}
impl AndroidApplicationExtensions for AppBuilder {
fn use_android(&self) -> AppBuilder {
AndroidRuntimePlatformServices::use_android_runtime_platform_subsystem(self)
.use_windowing_subsystem(AndroidPlatform::initialize, "Android")
.use_harfbuzz()
.use_skia()
}
}
ferroui_base::ferro_thread_local! {
static OPTIONS: RefCell<Option<Rc<AndroidPlatformOptions>>> = const { RefCell::new(None) };
static COMPOSITOR: RefCell<Option<Rc<Compositor>>> = const { RefCell::new(None) };
static TIMER: RefCell<Option<Arc<ChoreographerTimer>>> = const { RefCell::new(None) };
}
pub struct AndroidPlatform;
impl AndroidPlatform {
pub const INSTANCE: AndroidPlatform = AndroidPlatform;
pub fn options() -> Option<Rc<AndroidPlatformOptions>> {
OPTIONS.with(|options| options.borrow().clone())
}
pub(crate) fn compositor() -> Option<Rc<Compositor>> {
COMPOSITOR.with(|compositor| compositor.borrow().clone())
}
#[allow(dead_code)]
pub(crate) fn timer() -> Option<Arc<ChoreographerTimer>> {
TIMER.with(|timer| timer.borrow().clone())
}
pub fn initialize() {
let options = FerroLocator::current().get_service::<AndroidPlatformOptions>().unwrap_or_default();
OPTIONS.with(|slot| *slot.borrow_mut() = Some(options.clone()));
Dispatcher::initialize_ui_thread_dispatcher(AndroidDispatcherImpl::new());
let timer = Arc::new(ChoreographerTimer::new());
TIMER.with(|slot| *slot.borrow_mut() = Some(timer.clone()));
let windowing_platform: Rc<dyn IWindowingPlatform> = Rc::new(WindowingPlatformStub);
let keyboard_device: Rc<dyn IKeyboardDevice> = AndroidKeyboardDevice::new();
let platform_settings: Rc<dyn IPlatformSettings> = AndroidPlatformSettings::new(
FerroAndroidApplication::try_context()
.map(|context| Box::new(ApplicationSettingsContext::new(context)) as Box<_>),
);
let render_timer: Arc<dyn IRenderTimer> = timer;
let render_loop: Arc<dyn IRenderLoop> = RenderLoop::from_timer(render_timer);
let activatable_lifetime = AndroidActivatableLifetime::new();
let activatable: Rc<dyn IActivatableLifetime> = activatable_lifetime.clone();
let locator = FerroLocator::current_mutable();
locator
.bind::<dyn ICursorFactory>()
.to_transient::<CursorFactory>(|instance| instance)
.bind::<dyn IWindowingPlatform>()
.to_constant(windowing_platform)
.bind::<dyn IKeyboardDevice>()
.to_constant(keyboard_device)
.bind::<dyn IPlatformSettings>()
.to_constant(platform_settings)
.bind::<dyn IPlatformIconLoader>()
.to_singleton::<PlatformIconLoaderStub>(|instance| instance)
.bind::<Arc<dyn IRenderLoop>>()
.to_constant(Rc::new(render_loop))
.bind_to_self_singleton::<PlatformHotkeyConfiguration>()
.bind_to_self(Rc::new(KeyGestureFormatInfo::new(Some(HashMap::new()), "Cmd", "Ctrl", "Alt", "Shift")))
.bind::<dyn IActivatableLifetime>()
.to_constant(activatable)
.bind_to_self(activatable_lifetime);
let graphics = Self::initialize_graphics(&options);
if let Some(graphics) = &graphics {
locator.bind::<Arc<dyn IPlatformGraphics>>().to_constant(Rc::new(graphics.clone()));
}
let compositor = Compositor::new(graphics, false);
COMPOSITOR.with(|slot| *slot.borrow_mut() = Some(compositor.clone()));
locator.bind_to_self(compositor);
}
fn initialize_graphics(opts: &AndroidPlatformOptions) -> Option<Arc<dyn IPlatformGraphics>> {
initialize_graphics_core(
opts,
&mut || AndroidEglPlatformGraphics::try_create().map(|egl| Arc::new(egl) as Arc<dyn IPlatformGraphics>),
&mut Self::try_create_vulkan,
)
}
#[cfg(feature = "vulkan")]
fn try_create_vulkan() -> Option<Arc<dyn IPlatformGraphics>> {
use crate::platform::vulkan::VulkanSupport;
use ferroui_vulkan::VulkanOptions;
let vulkan_options = FerroLocator::current().get_service::<VulkanOptions>().map(|options| (*options).clone());
VulkanSupport::try_initialize(vulkan_options).map(|vulkan| vulkan as Arc<dyn IPlatformGraphics>)
}
#[cfg(not(feature = "vulkan"))]
fn try_create_vulkan() -> Option<Arc<dyn IPlatformGraphics>> {
use ferroui_base::logging::{LogEventLevel, Logger};
if let Some(logger) = Logger::try_get(LogEventLevel::Warning, "Vulkan") {
logger.log(
None,
"The Vulkan rendering mode needs the feature `vulkan` of ferroui-android (Skia with Vulkan); the mode is passed over",
);
}
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn options(rendering_mode: Vec<AndroidRenderingMode>) -> AndroidPlatformOptions {
AndroidPlatformOptions { rendering_mode }
}
#[test]
fn the_default_order_is_egl_then_software() {
assert_eq!(
AndroidPlatformOptions::default().rendering_mode,
vec![AndroidRenderingMode::Egl, AndroidRenderingMode::Software]
);
assert_eq!(AndroidRenderingMode::Software as i32, 1);
assert_eq!(AndroidRenderingMode::Egl as i32, 2);
assert_eq!(AndroidRenderingMode::Vulkan as i32, 3);
}
#[test]
fn the_default_order_takes_egl_when_it_initializes() {
let mut asked = 0;
let graphics = initialize_graphics_core(
&AndroidPlatformOptions::default(),
&mut || {
asked += 1;
Some(7)
},
&mut || panic!("Vulkan is not in the default order"),
);
assert_eq!(graphics, Some(7));
assert_eq!(asked, 1);
}
#[test]
fn the_default_order_ends_at_software_when_egl_does_not_initialize() {
let mut asked = 0;
let graphics = initialize_graphics_core::<i32>(
&AndroidPlatformOptions::default(),
&mut || {
asked += 1;
None
},
&mut || panic!("Vulkan is not in the default order"),
);
assert_eq!(graphics, None);
assert_eq!(asked, 1);
}
#[test]
fn software_first_does_not_ask_for_egl() {
let graphics = initialize_graphics_core::<i32>(
&options(vec![AndroidRenderingMode::Software, AndroidRenderingMode::Egl]),
&mut || panic!("EGL must not be asked for"),
&mut || panic!("Vulkan must not be asked for"),
);
assert_eq!(graphics, None);
}
#[test]
#[should_panic(expected = "must not be empty or null")]
fn an_empty_list_fails() {
initialize_graphics_core::<i32>(&options(vec![]), &mut || None, &mut || None);
}
#[test]
#[should_panic(expected = "has a value of \"Egl, Vulkan\", but no options were applied.")]
fn a_list_in_which_nothing_applies_fails() {
initialize_graphics_core::<i32>(
&options(vec![AndroidRenderingMode::Egl, AndroidRenderingMode::Vulkan]),
&mut || None,
&mut || None,
);
}
#[test]
fn vulkan_is_taken_when_it_is_named_and_initializes() {
let asked = std::cell::RefCell::new(Vec::new());
let graphics = initialize_graphics_core(
&options(vec![AndroidRenderingMode::Vulkan, AndroidRenderingMode::Egl, AndroidRenderingMode::Software]),
&mut || {
asked.borrow_mut().push("egl");
Some(2)
},
&mut || {
asked.borrow_mut().push("vulkan");
Some(3)
},
);
assert_eq!(graphics, Some(3));
assert_eq!(*asked.borrow(), vec!["vulkan"]);
}
#[test]
fn vulkan_that_does_not_initialize_is_passed_over() {
let asked = std::cell::RefCell::new(Vec::new());
let graphics = initialize_graphics_core(
&options(vec![AndroidRenderingMode::Vulkan, AndroidRenderingMode::Egl]),
&mut || {
asked.borrow_mut().push("egl");
Some(2)
},
&mut || {
asked.borrow_mut().push("vulkan");
None
},
);
assert_eq!(graphics, Some(2));
assert_eq!(*asked.borrow(), vec!["vulkan", "egl"]);
}
}