use android_activity::AndroidApp;
use platform_core::{
Event, EventHandler, Platform, PlatformError, PointerButton, PointerSource, ScrollDelta,
Window, WindowConfig,
};
#[cfg(target_os = "android")]
unsafe fn try_set_frame_rate(window: *mut std::ffi::c_void, fps: f32) {
unsafe extern "C" {
fn dlsym(
handle: *mut core::ffi::c_void,
symbol: *const core::ffi::c_char,
) -> *mut core::ffi::c_void;
}
let sym = unsafe {
dlsym(
core::ptr::null_mut(),
b"ANativeWindow_setFrameRate\0".as_ptr() as _,
)
};
if sym.is_null() {
return;
}
let f: unsafe extern "C" fn(*mut core::ffi::c_void, f32, i8) -> i32 =
unsafe { core::mem::transmute(sym) };
unsafe { f(window, fps, 0) };
}
#[cfg(target_os = "android")]
mod choreographer {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[repr(C)]
pub struct AChoreographer {
_opaque: [u8; 0],
}
pub type FrameCallbackFn =
unsafe extern "C" fn(frame_time_ns: i64, data: *mut core::ffi::c_void);
unsafe fn instance_fn() -> Option<unsafe extern "C" fn() -> *mut AChoreographer> {
unsafe extern "C" {
fn dlsym(
handle: *mut core::ffi::c_void,
symbol: *const core::ffi::c_char,
) -> *mut core::ffi::c_void;
}
let sym = unsafe {
dlsym(
core::ptr::null_mut(),
b"AChoreographer_getInstance\0".as_ptr() as _,
)
};
if sym.is_null() {
None
} else {
Some(unsafe { core::mem::transmute(sym) })
}
}
unsafe fn post_callback_fn()
-> Option<unsafe extern "C" fn(*mut AChoreographer, FrameCallbackFn, *mut core::ffi::c_void)>
{
unsafe extern "C" {
fn dlsym(
handle: *mut core::ffi::c_void,
symbol: *const core::ffi::c_char,
) -> *mut core::ffi::c_void;
}
let sym = unsafe {
dlsym(
core::ptr::null_mut(),
b"AChoreographer_postFrameCallback\0".as_ptr() as _,
)
};
if sym.is_null() {
None
} else {
Some(unsafe { core::mem::transmute(sym) })
}
}
pub unsafe extern "C" fn frame_callback(_frame_time_ns: i64, data: *mut core::ffi::c_void) {
let cb_data = unsafe { &*(data as *const VsyncCallbackData) };
cb_data.is_pending.store(false, Ordering::Release);
let _ = cb_data.proxy.send_event(());
}
pub struct VsyncCallbackData {
pub is_pending: Arc<AtomicBool>,
pub proxy: winit::event_loop::EventLoopProxy<()>,
}
pub struct Choreographer {
instance: *mut AChoreographer,
pub callback_data: Box<VsyncCallbackData>,
}
unsafe impl Send for Choreographer {}
impl Choreographer {
pub fn new(
proxy: winit::event_loop::EventLoopProxy<()>,
pending: Arc<AtomicBool>,
) -> Option<Self> {
let get_instance = unsafe { instance_fn()? };
let instance = unsafe { get_instance() };
if instance.is_null() {
return None;
}
Some(Self {
instance,
callback_data: Box::new(VsyncCallbackData {
is_pending: pending,
proxy,
}),
})
}
pub fn request_vsync(&self) {
let post = match unsafe { post_callback_fn() } {
Some(f) => f,
None => return,
};
let data_ptr =
self.callback_data.as_ref() as *const VsyncCallbackData as *mut core::ffi::c_void;
unsafe { post(self.instance, frame_callback, data_ptr) };
}
}
}
use winit::application::ApplicationHandler;
use winit::event::{ElementState, MouseScrollDelta, StartCause, Touch, TouchPhase, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::keyboard::Key as WinitKey;
use winit::platform::android::EventLoopBuilderExtAndroid;
use winit::window::{WindowAttributes, WindowId};
use platform_winit::WinitWindow as AndroidWindow;
pub struct AndroidPlatform {
event_loop: EventLoop<()>,
app: AndroidApp,
}
const THEME_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
fn prefers_dark(app: &AndroidApp) -> Option<bool> {
use android_activity::ndk::configuration::{Configuration, UiModeNight};
match Configuration::from_asset_manager(&app.asset_manager()).ui_mode_night() {
UiModeNight::Yes => Some(true),
UiModeNight::No => Some(false),
_ => None,
}
}
impl AndroidPlatform {
pub fn try_new(app: AndroidApp) -> Result<Self, PlatformError> {
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*;
let logcat = paranoid_android::layer("telar").with_filter(LevelFilter::DEBUG);
tracing_subscriber::registry().with(logcat).try_init().ok();
let event_loop = EventLoop::builder()
.with_android_app(app.clone())
.build()
.map_err(|e| PlatformError(e.to_string()))?;
Ok(Self { event_loop, app })
}
}
struct AndroidRunner<H: EventHandler<AndroidWindow>> {
handler: H,
window: Option<AndroidWindow>,
config: WindowConfig,
scale_factor: f64,
modifiers: platform_core::ModifiersState,
cursor_position: (f64, f64),
last_touch_pos: Option<(f64, f64, u64)>,
app: AndroidApp,
last_dark: Option<bool>,
last_theme_poll: Option<std::time::Instant>,
#[cfg(target_os = "android")]
choreographer: Option<choreographer::Choreographer>,
#[cfg(target_os = "android")]
is_animation_pending: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl<H: EventHandler<AndroidWindow>> ApplicationHandler<()> for AndroidRunner<H> {
fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: StartCause) {
self.handler.new_events();
if self
.last_theme_poll
.is_none_or(|at| at.elapsed() >= THEME_POLL_INTERVAL)
{
self.last_theme_poll = Some(std::time::Instant::now());
let dark = prefers_dark(&self.app);
if dark != self.last_dark {
self.last_dark = dark;
if let (Some(dark), Some(window)) = (dark, self.window.clone()) {
self.handler
.on_event(Event::ColorSchemeChanged { dark }, &window);
}
}
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if let Some(_d) = self.handler.about_to_wait() {
#[cfg(target_os = "android")]
{
let already_pending = self
.is_animation_pending
.swap(true, std::sync::atomic::Ordering::AcqRel);
if !already_pending {
if let Some(chore) = &self.choreographer {
chore.request_vsync();
} else if let Some(window) = &self.window {
window.request_redraw();
event_loop.set_control_flow(ControlFlow::WaitUntil(
std::time::Instant::now() + _d,
));
return;
}
}
event_loop.set_control_flow(ControlFlow::Wait);
}
#[cfg(not(target_os = "android"))]
{
event_loop.set_control_flow(ControlFlow::WaitUntil(std::time::Instant::now() + _d));
}
} else {
event_loop.set_control_flow(ControlFlow::Wait);
}
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, _event: ()) {
if let Some(window) = &self.window {
window.request_redraw();
}
}
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let attrs = WindowAttributes::default()
.with_title(self.config.title.as_str())
.with_inner_size(winit::dpi::LogicalSize::new(
self.config.width,
self.config.height,
));
use std::sync::Arc;
match event_loop.create_window(attrs) {
Ok(w) => {
self.scale_factor = w.scale_factor();
#[cfg(target_os = "android")]
{
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
if let Ok(handle) = w.window_handle() {
if let RawWindowHandle::AndroidNdk(android_handle) = handle.as_raw() {
unsafe {
try_set_frame_rate(android_handle.a_native_window.as_ptr(), 60.0);
}
}
}
}
let window = AndroidWindow(Arc::new(w));
self.last_dark = prefers_dark(&self.app);
self.last_theme_poll = Some(std::time::Instant::now());
if let Some(dark) = self.last_dark {
self.handler
.on_event(Event::ColorSchemeChanged { dark }, &window);
}
if !self.handler.on_resume(&window) {
event_loop.exit();
return;
}
window.request_redraw();
self.window = Some(window);
}
Err(e) => tracing::error!(error = %e, "failed to create window"),
}
}
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
self.handler.on_suspend();
self.window = None;
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let Some(window) = &self.window else { return };
match event {
WindowEvent::CloseRequested => {
self.handler.on_event(Event::WindowCloseRequested, window);
event_loop.exit();
}
WindowEvent::Resized(size) => {
self.handler.on_event(
Event::WindowResized {
width: (size.width as f64 / self.scale_factor).round() as u32,
height: (size.height as f64 / self.scale_factor).round() as u32,
},
window,
);
window.request_redraw();
}
WindowEvent::RedrawRequested => {
self.handler.on_redraw(window);
}
WindowEvent::Touch(Touch {
phase,
location,
id,
..
}) => {
let x = location.x / self.scale_factor;
let y = location.y / self.scale_factor;
let source = PointerSource::Touch { id };
match phase {
TouchPhase::Started => {
self.last_touch_pos = Some((x, y, id));
self.handler.on_event(
Event::PointerPressed {
x,
y,
button: PointerButton::Primary,
source,
},
window,
);
}
TouchPhase::Moved => {
if let Some((lx, ly, lid)) = self.last_touch_pos {
if lid == id {
let dx = x - lx;
let dy = y - ly;
self.handler.on_event(
Event::Scrolled {
delta: platform_core::ScrollDelta::Pixels {
x: dx as f32,
y: dy as f32,
},
},
window,
);
}
}
self.last_touch_pos = Some((x, y, id));
self.handler
.on_event(Event::PointerMoved { x, y, source }, window);
}
TouchPhase::Ended | TouchPhase::Cancelled => {
self.last_touch_pos = None;
self.handler.on_event(
Event::PointerReleased {
x,
y,
button: PointerButton::Primary,
source,
},
window,
);
}
}
}
WindowEvent::Focused(is_focused) => {
self.handler
.on_event(Event::FocusChanged { is_focused }, window);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
self.scale_factor = scale_factor;
self.handler
.on_event(Event::ScaleFactorChanged { scale_factor }, window);
}
WindowEvent::ModifiersChanged(mods) => {
self.modifiers = platform_winit::map_modifiers(&mods);
self.handler.on_event(
Event::ModifiersChanged {
modifiers: self.modifiers,
},
window,
);
}
WindowEvent::KeyboardInput { event, .. } => {
let key = match &event.logical_key {
WinitKey::Character(c) => {
if let Some(ch) = c.as_str().chars().next() {
platform_core::Key::Char(ch)
} else {
return;
}
}
WinitKey::Named(named) => {
let Some(nk) = platform_winit::map_named_key(*named) else {
return;
};
platform_core::Key::Named(nk)
}
_ => return,
};
let modifiers = self.modifiers;
let ev = match event.state {
ElementState::Pressed => platform_core::Event::KeyPressed { key, modifiers },
ElementState::Released => platform_core::Event::KeyReleased { key, modifiers },
};
self.handler.on_event(ev, window);
}
WindowEvent::CursorMoved { position, .. } => {
let lx = position.x / self.scale_factor;
let ly = position.y / self.scale_factor;
self.cursor_position = (lx, ly);
self.handler.on_event(
Event::PointerMoved {
x: lx,
y: ly,
source: PointerSource::Mouse,
},
window,
);
}
WindowEvent::MouseInput { state, button, .. } => {
let Some(btn) = platform_winit::map_mouse_button(button) else {
return;
};
let (x, y) = self.cursor_position;
let ev = match state {
ElementState::Pressed => Event::PointerPressed {
x,
y,
button: btn,
source: PointerSource::Mouse,
},
ElementState::Released => Event::PointerReleased {
x,
y,
button: btn,
source: PointerSource::Mouse,
},
};
self.handler.on_event(ev, window);
}
WindowEvent::CursorEntered { .. } => {
self.handler.on_event(Event::CursorEntered, window);
}
WindowEvent::CursorLeft { .. } => {
self.handler.on_event(Event::CursorLeft, window);
}
WindowEvent::MouseWheel { delta, .. } => {
let scroll_delta = match delta {
MouseScrollDelta::LineDelta(x, y) => ScrollDelta::Lines { x, y },
MouseScrollDelta::PixelDelta(pos) => ScrollDelta::Pixels {
x: (pos.x / self.scale_factor) as f32,
y: (pos.y / self.scale_factor) as f32,
},
};
self.handler.on_event(
Event::Scrolled {
delta: scroll_delta,
},
window,
);
}
_ => {}
}
}
}
impl Platform for AndroidPlatform {
type Window = AndroidWindow;
fn run<H: EventHandler<Self::Window>>(
self,
config: WindowConfig,
handler: H,
) -> Result<(), PlatformError> {
#[cfg(target_os = "android")]
let animation_pending = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
#[cfg(target_os = "android")]
let choreographer = choreographer::Choreographer::new(
self.event_loop.create_proxy(),
animation_pending.clone(),
);
let mut runner = AndroidRunner {
handler,
window: None,
config,
scale_factor: 1.0,
modifiers: platform_core::ModifiersState::default(),
cursor_position: (0.0, 0.0),
last_touch_pos: None,
app: self.app,
last_dark: None,
last_theme_poll: None,
#[cfg(target_os = "android")]
choreographer,
#[cfg(target_os = "android")]
is_animation_pending: animation_pending,
};
self.event_loop
.run_app(&mut runner)
.map_err(|e| PlatformError(e.to_string()))
}
}