use std::{
sync::{
Arc, Mutex,
mpsc::{self, Sender},
},
time::{Duration, Instant},
};
#[cfg(target_os = "android")]
use android_gpu::Presenter;
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
use gl::Presenter;
#[cfg(any(target_os = "macos", target_os = "ios"))]
use metal::Presenter;
use winit::{
application::ApplicationHandler,
dpi::{LogicalPosition, LogicalSize},
event::{ElementState, Ime, MouseButton as WinitButton, MouseScrollDelta, StartCause, WindowEvent},
event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy},
keyboard::{Key, KeyCode, NamedKey, PhysicalKey},
window::{CursorIcon, Window, WindowAttributes, WindowId},
};
use crate::host_access;
use crate::keyboard::with_key_codes;
use crate::ui::AccessibilityNode;
use crate::{
App, BakeRequest, ContextMenuSource, Cursor, Decoded, Frame, Gpu, Host, ImageRequest, Images, InputKey, KeyKind, Modifiers, MouseButton,
PointerKind, RenderingModeType, skia,
};
#[cfg(target_os = "android")]
static ANDROID_APP: std::sync::Mutex<Option<crate::AndroidApp>> = std::sync::Mutex::new(None);
#[cfg(target_os = "android")]
pub(crate) fn set_android_app(app: crate::AndroidApp) {
*ANDROID_APP.lock().unwrap() = Some(app);
android::forget_field();
}
#[cfg(target_os = "android")]
fn android_app() -> Option<crate::AndroidApp> {
ANDROID_APP.lock().unwrap().clone()
}
const RESIZE_SYNC: Duration = Duration::from_millis(500);
enum Arrived {
Image(u32, Option<Decoded>),
Baked(u32, Option<skia::Image>),
Access(accesskit_winit::Event),
#[cfg(target_os = "android")]
Vsync(i64),
#[cfg(any(target_os = "macos", target_os = "ios"))]
Tick,
}
impl From<accesskit_winit::Event> for Arrived {
fn from(event: accesskit_winit::Event) -> Self {
Arrived::Access(event)
}
}
struct Desktop {
app: Box<dyn App>,
host: Host,
images: Sender<ImageRequest>,
bakes: Sender<BakeRequest>,
surface: Option<skia::Surface>,
gpu: Gpu,
cpu: Option<CpuFrames>,
presenter: Presenter,
access: Option<accesskit_winit::Adapter>,
access_nodes: Vec<AccessibilityNode>,
access_focus: accesskit::NodeId,
#[cfg_attr(target_os = "android", allow(dead_code))]
proxy: EventLoopProxy<Arrived>,
window: Window,
occluded: bool,
suspended: bool,
#[cfg(any(target_os = "ios", target_os = "android"))]
touch: Option<u64>,
#[cfg(any(target_os = "ios", target_os = "android"))]
safe_area: crate::Thickness,
#[cfg(target_os = "android")]
insets_read: bool,
#[cfg(target_os = "android")]
frame_posted: bool,
#[cfg(target_os = "android")]
frame_wanted: bool,
#[cfg(target_os = "android")]
ime_open: bool,
#[cfg(target_os = "android")]
vsync_ns: Option<i64>,
#[cfg(target_os = "android")]
vsync: android::VsyncClock,
#[cfg(any(target_os = "macos", target_os = "ios"))]
ticker: Option<ticker::Ticker>,
#[cfg(any(target_os = "macos", target_os = "ios"))]
frame_wanted: bool,
start: Instant,
wake: Option<Instant>,
resized_at: Option<Instant>,
cursor: (f32, f32),
pressed: Option<MouseButton>,
modifiers: Modifiers,
ime_composing: bool,
stats: Stats,
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
pacing: Pacing,
#[cfg(target_os = "android")]
size: winit::dpi::PhysicalSize<u32>,
}
struct CpuFrames {
gpu: Gpu,
surface: Option<skia::Surface>,
}
impl CpuFrames {
fn frame(&mut self, width: i32, height: i32) -> Option<(&mut skia::Surface, &mut Gpu)> {
if self.surface.as_ref().is_none_or(|s| s.width() != width || s.height() != height) {
self.surface = skia::surfaces::raster_n32_premul((width, height));
}
Some((self.surface.as_mut()?, &mut self.gpu))
}
}
impl Drop for Desktop {
fn drop(&mut self) {
#[cfg(target_os = "android")]
self.presenter.wait_idle();
self.gpu.abandon();
}
}
pub fn run(title: &str, size: (f64, f64), make: fn() -> Box<dyn App>) {
let mut builder = EventLoop::<Arrived>::with_user_event();
#[cfg(target_os = "android")]
{
use winit::platform::android::EventLoopBuilderExtAndroid;
android::log_panics();
builder.with_android_app(android_app().expect("drawnui::set_android_app before run"));
}
let event_loop = builder.build().expect("no event loop");
let attributes = WindowAttributes::default().with_title(title).with_visible(false);
#[cfg(not(any(target_os = "ios", target_os = "android")))]
let attributes = attributes.with_inner_size(LogicalSize::new(size.0, size.1));
#[cfg(any(target_os = "ios", target_os = "android"))]
let _ = size;
event_loop.set_control_flow(ControlFlow::Wait);
#[cfg(any(target_os = "ios", target_os = "android"))]
{
let mut launch = ios::Launch { attributes: Some(attributes), make, proxy: event_loop.create_proxy(), desktop: None };
event_loop.run_app(&mut launch).expect("event loop failed");
}
#[cfg(not(any(target_os = "ios", target_os = "android")))]
{
#[cfg(not(target_os = "macos"))]
let (window, presenter) = Presenter::new(&event_loop, attributes);
#[cfg(target_os = "macos")]
#[allow(deprecated)]
let (window, presenter) = Presenter::new(event_loop.create_window(attributes).expect("no window"), skia::Color::TRANSPARENT);
let mut desktop = Desktop::new(window, presenter, make(), event_loop.create_proxy());
event_loop.run_app(&mut desktop).expect("event loop failed");
}
}
impl Desktop {
fn new(window: Window, presenter: Presenter, mut app: Box<dyn App>, proxy: EventLoopProxy<Arrived>) -> Self {
#[cfg(any(target_os = "macos", target_os = "ios"))]
let ticker = ticker::Ticker::new(proxy.clone(), &window);
#[cfg(windows)]
exe_icon(&window);
#[cfg(target_os = "linux")]
file_icon(&window);
let gpu = presenter.gpu();
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
let pacing = Pacing::new(&window);
let mut host = Host { bake_workers: true, keyboard_navigation: true, accessibility_on: true, ..Host::default() };
app.init(&mut host);
#[cfg(target_os = "android")]
let size = window.inner_size();
let cpu = (app.rendering_mode() == RenderingModeType::Default).then(|| CpuFrames { gpu: Gpu::raster(), surface: None });
let mut desktop = Desktop {
app,
host,
images: image_workers(proxy.clone()),
bakes: bake_workers(proxy.clone()),
surface: None,
gpu,
cpu,
presenter,
access: None,
access_nodes: Vec::new(),
access_focus: host_access::ROOT,
proxy,
window,
occluded: false,
suspended: false,
#[cfg(any(target_os = "ios", target_os = "android"))]
touch: None,
#[cfg(any(target_os = "ios", target_os = "android"))]
safe_area: crate::Thickness::default(),
#[cfg(target_os = "android")]
insets_read: false,
#[cfg(target_os = "android")]
frame_posted: false,
#[cfg(target_os = "android")]
frame_wanted: false,
#[cfg(target_os = "android")]
ime_open: false,
#[cfg(target_os = "android")]
vsync_ns: None,
#[cfg(target_os = "android")]
vsync: android::VsyncClock::new(),
#[cfg(any(target_os = "macos", target_os = "ios"))]
ticker,
#[cfg(any(target_os = "macos", target_os = "ios"))]
frame_wanted: false,
start: Instant::now(),
wake: None,
resized_at: None,
cursor: (0.0, 0.0),
pressed: None,
modifiers: Modifiers::default(),
ime_composing: false,
stats: Stats::default(),
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
pacing,
#[cfg(target_os = "android")]
size,
};
#[cfg(any(target_os = "ios", target_os = "android"))]
desktop.report_safe_area();
desktop.deliver_assets();
desktop.redraw();
desktop
}
fn access_event(&mut self, event: accesskit_winit::WindowEvent) {
use accesskit_winit::WindowEvent as Access;
match event {
Access::InitialTreeRequested => self.send_access_tree(),
Access::ActionRequested(request) => {
let id = host_access::snapshot_id(request.target_node);
match (request.action, id) {
(accesskit::Action::Click, Some(id)) => self.app.accessibility_activate(id),
(accesskit::Action::Focus, Some(id)) => {
if let Some(previous) = host_access::snapshot_id(self.access_focus) {
self.app.accessibility_focus(previous, false);
}
self.access_focus = request.target_node;
self.app.accessibility_focus(id, true);
self.send_access_tree();
}
(accesskit::Action::Increment, Some(id)) => self.app.accessibility_adjust(id, true),
(accesskit::Action::Decrement, Some(id)) => self.app.accessibility_adjust(id, false),
(accesskit::Action::ScrollIntoView, Some(id)) => self.app.accessibility_scroll_into_view(id),
(accesskit::Action::ScrollDown, Some(id)) => self.app.accessibility_scroll(id, 0.0, 1.0),
(accesskit::Action::ScrollUp, Some(id)) => self.app.accessibility_scroll(id, 0.0, -1.0),
(accesskit::Action::ScrollRight, Some(id)) => self.app.accessibility_scroll(id, 1.0, 0.0),
(accesskit::Action::ScrollLeft, Some(id)) => self.app.accessibility_scroll(id, -1.0, 0.0),
(accesskit::Action::SetValue, Some(id)) => {
if let Some(accesskit::ActionData::NumericValue(value)) = request.data {
self.app.accessibility_set_value(id, value);
}
}
_ => {}
}
}
Access::AccessibilityDeactivated => {}
}
}
fn send_access_tree(&mut self) {
let (title, scale, focus, nodes) = (self.window.title(), self.window.scale_factor(), self.access_focus, &self.access_nodes);
if let Some(access) = &mut self.access {
access.update_if_active(|| host_access::tree(&title, nodes, scale, focus));
}
}
fn redraw(&mut self) {
#[cfg(any(target_os = "macos", target_os = "ios"))]
if let Some(ticker) = &self.ticker {
self.frame_wanted = true;
ticker.run(!self.suspended);
return;
}
#[cfg(target_os = "android")]
{
self.frame_wanted = true;
if !self.frame_posted && !self.suspended {
self.frame_posted = android::post_frame(&self.proxy);
if !self.frame_posted {
self.frame_wanted = false;
self.window.request_redraw();
}
}
return;
}
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
if let Some(at) = self.pacing.hold(Instant::now()) {
self.pacing.due = Some(at);
return;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
self.window.request_redraw();
}
fn surface_size(&self) -> winit::dpi::PhysicalSize<u32> {
#[cfg(target_os = "ios")]
return self.window.outer_size();
#[cfg(not(target_os = "ios"))]
self.window.inner_size()
}
#[cfg(target_os = "android")]
fn report_safe_area(&mut self) {
if self.insets_read {
return;
}
let Some([left, top, right, bottom]) = android::system_insets() else { return };
self.insets_read = true;
let scale = self.window.scale_factor();
let points = |px: i32| (px.max(0) as f64 / scale) as f32;
let insets = crate::Thickness { left: points(left), top: points(top), right: points(right), bottom: points(bottom) };
if insets != self.safe_area {
android::log(&format!("drawnui: safe area {insets:?} points (scale {scale})"));
self.safe_area = insets;
self.app.safe_insets(insets);
self.redraw();
}
}
#[cfg(target_os = "ios")]
fn report_safe_area(&mut self) {
let scale = self.window.scale_factor();
let (Ok(safe), Ok(full)) = (self.window.inner_position(), self.window.outer_position()) else { return };
let (safe_size, full_size) = (self.window.inner_size(), self.window.outer_size());
let left = (safe.x - full.x).max(0) as f64;
let top = (safe.y - full.y).max(0) as f64;
let right = (full_size.width as f64 - safe_size.width as f64 - left).max(0.0);
let bottom = (full_size.height as f64 - safe_size.height as f64 - top).max(0.0);
let insets = crate::Thickness {
left: (left / scale) as f32,
top: (top / scale) as f32,
right: (right / scale) as f32,
bottom: (bottom / scale) as f32,
};
if insets != self.safe_area {
self.safe_area = insets;
self.app.safe_insets(insets);
self.redraw();
}
}
}
fn image_workers(proxy: EventLoopProxy<Arrived>) -> Sender<ImageRequest> {
let (requests, queue) = mpsc::channel::<ImageRequest>();
let queue = Arc::new(Mutex::new(queue));
let spare = std::thread::available_parallelism().map_or(1, |cores| cores.get() - 1);
for _ in 0..spare.clamp(1, Images::MAX_IN_FLIGHT) {
let (queue, proxy) = (queue.clone(), proxy.clone());
std::thread::spawn(move || {
loop {
let Ok(request) = queue.lock().expect("an image worker panicked").recv() else { break };
let path = request.source.split('#').next().unwrap_or_default();
let decoded = match read_app_file(path) {
Ok(bytes) if request.frames => Images::decode_frames(&bytes),
Ok(bytes) => Images::decode(&bytes, request.width, request.height),
Err(e) => {
eprintln!("drawnui: image {path}: {e}");
None
}
};
if proxy.send_event(Arrived::Image(request.id, decoded)).is_err() {
break;
}
}
});
}
requests
}
fn bake_workers(proxy: EventLoopProxy<Arrived>) -> Sender<BakeRequest> {
let (requests, queue) = mpsc::channel::<BakeRequest>();
let queue = Arc::new(Mutex::new(queue));
let cores = std::thread::available_parallelism().map_or(2, |cores| cores.get());
for _ in 0..(cores / 2).clamp(2, 4) {
let (queue, proxy) = (queue.clone(), proxy.clone());
std::thread::spawn(move || {
loop {
let Ok(request) = queue.lock().expect("a bake worker panicked").recv() else { break };
if proxy.send_event(Arrived::Baked(request.id, request.bake())).is_err() {
break;
}
}
});
}
requests
}
impl Desktop {
fn now_ms(&self) -> f64 {
self.start.elapsed().as_secs_f64() * 1000.0
}
fn deliver_assets(&mut self) {
for (id, url) in std::mem::take(&mut self.host.requests) {
match read_app_file(&url) {
Ok(bytes) => self.app.asset(id, bytes),
Err(e) => {
eprintln!("drawnui: asset {url}: {e}");
self.app.asset(id, Vec::new());
}
}
self.redraw();
}
for request in std::mem::take(&mut self.host.images) {
let _ = self.images.send(request);
}
for request in self.host.bakes.drain(..) {
let _ = self.bakes.send(request);
}
}
fn draw(&mut self) {
#[cfg(any(target_os = "macos", target_os = "ios"))]
if self.occluded {
return;
}
if self.suspended {
return;
}
#[cfg(any(target_os = "ios", target_os = "android"))]
self.report_safe_area();
let (size, clock) = (self.surface_size(), self.start);
let Some(surface) = self.presenter.begin(&mut self.gpu, &mut self.surface, size.width.max(1), size.height.max(1)) else {
return;
};
let frame_start = Instant::now();
let time_ms = (frame_start - clock).as_secs_f64() * 1000.0;
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
let time_ms = self.pacing.frame(frame_start).map_or(time_ms, |slot| (slot - clock).as_secs_f64() * 1000.0);
#[cfg(target_os = "android")]
let time_ms = self.vsync_ns.take().map_or(time_ms, |ns| self.vsync.frame_ms(ns, time_ms));
let (width, height, scale) = (size.width as f32, size.height as f32, self.window.scale_factor() as f32);
let more = match &mut self.cpu {
None => self.app.frame(&mut Frame { surface, gpu: &mut self.gpu, width, height, scale, time_ms, host: &mut self.host }),
Some(cpu) => {
let Some((pixels, gpu)) = cpu.frame(surface.width(), surface.height()) else { return };
let more = self.app.frame(&mut Frame { surface: pixels, gpu, width, height, scale, time_ms, host: &mut self.host });
let mut paint = skia::Paint::default();
paint.set_blend_mode(skia::BlendMode::Src);
pixels.draw(surface.canvas(), (0, 0), skia::SamplingOptions::default(), Some(&paint));
more
}
};
self.presenter.end_frame(&mut self.gpu, &mut self.surface);
if let Some(nodes) = self.host.accessibility.take() {
self.access_focus = host_access::refocus(&nodes, self.access_focus, height / scale.max(0.1));
self.access_nodes = nodes;
self.send_access_tree();
}
if self.resized_at.is_some_and(|at| at.elapsed() < RESIZE_SYNC) {
self.gpu.finish();
}
self.stats.frame(frame_start, time_ms);
if self.gpu.lost() {
self.recreate_gpu();
self.redraw();
return;
}
self.presenter.present(&mut self.surface);
self.deliver_assets();
self.apply_input_requests();
let wake = self.host.wake_ms.take().filter(|_| !more);
self.wake = wake.map(|ms| self.start + Duration::from_secs_f64(ms / 1000.0));
if more {
self.redraw();
}
}
}
impl Desktop {
fn recreate_gpu(&mut self) {
eprintln!("drawnui: the GPU context was lost, making a new one");
self.surface = None;
let gpu = self.presenter.gpu_after(&self.window, &mut self.gpu);
self.gpu = gpu;
}
fn apply_input_requests(&mut self) {
for url in self.host.urls.drain(..) {
open_url(&url);
}
if let Some(text) = self.host.clipboard.take() {
clipboard::set(&text);
}
if std::mem::take(&mut self.host.paste)
&& let Some(text) = clipboard::get()
{
self.key(KeyKind::Char, "", &text, false);
}
if let Some(cursor) = self.host.cursor.take() {
self.window.set_cursor(match cursor {
Cursor::Default => CursorIcon::Default,
Cursor::Pointer => CursorIcon::Pointer,
Cursor::Text => CursorIcon::Text,
});
}
if let Some(area) = self.host.text_input.take() {
self.window.set_ime_allowed(area.is_some());
if let Some(r) = area {
self.window.set_ime_cursor_area(LogicalPosition::new(r.left, r.top), LogicalSize::new(r.width(), r.height()));
}
#[cfg(target_os = "android")]
{
self.ime_open = area.is_some();
if self.ime_open {
android::open_text_input();
}
}
}
}
#[cfg(target_os = "android")]
fn take_soft_keyboard_text(&mut self) {
let Some((deleted, typed)) = android::take_text_input() else { return };
for _ in 0..deleted {
self.key(KeyKind::Down, "Backspace", "", false);
self.key(KeyKind::Up, "Backspace", "", false);
}
for (i, line) in typed.split('\n').enumerate() {
if i > 0 {
self.key(KeyKind::Down, "Enter", "", false);
self.key(KeyKind::Up, "Enter", "", false);
}
if !line.is_empty() {
self.key(KeyKind::Char, "", line, false);
}
}
}
fn key(&mut self, kind: KeyKind, key: InputKey, text: &str, repeat: bool) -> bool {
let used = self.app.key(kind, key, text, self.modifiers, repeat);
self.sync_access_focus();
self.redraw();
used
}
fn sync_access_focus(&mut self) {
let focus = self.app.accessibility_focused_id().map_or(host_access::ROOT, host_access::node_id);
if focus != self.access_focus {
self.access_focus = focus;
self.send_access_tree();
}
}
}
#[cfg(windows)]
fn exe_icon(window: &Window) {
use winit::platform::windows::{IconExtWindows, WindowExtWindows};
let icon = |points: f64| {
let px = (points * window.scale_factor()).round() as u32;
winit::window::Icon::from_resource(1, Some(winit::dpi::PhysicalSize::new(px, px))).ok()
};
window.set_window_icon(icon(16.0));
window.set_taskbar_icon(icon(32.0));
}
#[cfg(target_os = "linux")]
fn file_icon(window: &Window) {
let Ok(bytes) = read_app_file("icon.ico") else { return };
let Some(image) = skia::Image::from_encoded(skia::Data::new_copy(&bytes)) else { return };
let (width, height) = (image.width(), image.height());
let info = skia::ImageInfo::new((width, height), skia::ColorType::RGBA8888, skia::AlphaType::Unpremul, None);
let mut rgba = vec![0u8; (width * height * 4) as usize];
if image.read_pixels(&info, &mut rgba, width as usize * 4, (0, 0), skia::image::CachingHint::Disallow) {
window.set_window_icon(winit::window::Icon::from_rgba(rgba, width as u32, height as u32).ok());
}
}
#[cfg(not(target_os = "android"))]
fn read_app_file(path: &str) -> std::io::Result<Vec<u8>> {
let beside_exe = std::env::current_exe().ok().and_then(|exe| Some(exe.parent()?.join(path)));
match beside_exe {
Some(file) if file.is_file() => std::fs::read(file),
_ => std::fs::read(path),
}
}
#[cfg(target_os = "android")]
fn read_app_file(path: &str) -> std::io::Result<Vec<u8>> {
use std::io::{Error, ErrorKind, Read};
let app = android_app().ok_or_else(|| Error::new(ErrorKind::NotFound, "no activity"))?;
let name = std::ffi::CString::new(path).map_err(|e| Error::new(ErrorKind::InvalidInput, e))?;
let mut asset = app.asset_manager().open(&name).ok_or_else(|| Error::new(ErrorKind::NotFound, path.to_owned()))?;
let mut bytes = Vec::new();
asset.read_to_end(&mut bytes)?;
Ok(bytes)
}
#[cfg(target_os = "android")]
mod android {
use std::ffi::{CString, c_char, c_int, c_long, c_void};
use std::sync::OnceLock;
use winit::event_loop::EventLoopProxy;
use winit::platform::android::activity::input::{ImeOptions, InputType, TextInputAction, TextInputState, TextSpan};
#[repr(C)]
struct AChoreographer {
_private: [u8; 0],
}
#[repr(C)]
struct Timespec {
seconds: c_long,
nanoseconds: c_long,
}
type Post64 = unsafe extern "C" fn(*mut AChoreographer, unsafe extern "C" fn(i64, *mut c_void), *mut c_void);
unsafe extern "C" {
fn __android_log_write(priority: c_int, tag: *const c_char, text: *const c_char) -> c_int;
fn AChoreographer_getInstance() -> *mut AChoreographer;
fn AChoreographer_postFrameCallback(choreographer: *mut AChoreographer, callback: unsafe extern "C" fn(c_long, *mut c_void), data: *mut c_void);
fn dlopen(name: *const c_char, flags: c_int) -> *mut c_void;
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
fn clock_gettime(clock: c_int, time: *mut Timespec) -> c_int;
}
thread_local! {
static PROXY: std::cell::OnceCell<EventLoopProxy<super::Arrived>> = const { std::cell::OnceCell::new() };
}
fn on_vsync(nanos: i64) {
PROXY.with(|proxy| {
if let Some(proxy) = proxy.get() {
let _ = proxy.send_event(super::Arrived::Vsync(nanos));
}
});
}
unsafe extern "C" fn on_frame64(nanos: i64, _data: *mut c_void) {
on_vsync(nanos);
}
unsafe extern "C" fn on_frame(nanos: c_long, _data: *mut c_void) {
on_vsync(nanos as i64);
}
pub(super) fn post_frame(proxy: &EventLoopProxy<super::Arrived>) -> bool {
static POST64: OnceLock<Option<Post64>> = OnceLock::new();
let post64 = *POST64.get_or_init(|| unsafe {
let library = dlopen(c"libandroid.so".as_ptr(), 2);
let symbol = if library.is_null() { std::ptr::null_mut() } else { dlsym(library, c"AChoreographer_postFrameCallback64".as_ptr()) };
(!symbol.is_null()).then(|| std::mem::transmute::<*mut c_void, Post64>(symbol))
});
PROXY.with(|cell| {
cell.get_or_init(|| proxy.clone());
});
unsafe {
let choreographer = AChoreographer_getInstance();
if choreographer.is_null() {
return false;
}
match post64 {
Some(post) => post(choreographer, on_frame64, std::ptr::null_mut()),
None => AChoreographer_postFrameCallback(choreographer, on_frame, std::ptr::null_mut()),
}
}
true
}
const PLACEHOLDER: &str = "\u{200B}\u{200B}\u{200B}\u{200B}\u{200B}\u{200B}\u{200B}\u{200B}";
const PLACEHOLDER_LOW: usize = 2;
const FIELD_MAX: usize = 200;
static FIELD: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
pub(super) fn forget_field() {
FIELD.lock().unwrap().clear();
}
fn reset_field(app: &crate::AndroidApp) {
let caret = PLACEHOLDER.chars().count();
app.set_text_input_state(TextInputState {
text: PLACEHOLDER.into(),
selection: TextSpan { start: caret, end: caret },
compose_region: None,
});
*FIELD.lock().unwrap() = PLACEHOLDER.into();
}
pub(super) fn open_text_input() {
let Some(app) = super::android_app() else { return };
let kind = InputType::TYPE_CLASS_TEXT | InputType::TYPE_TEXT_FLAG_MULTI_LINE | InputType::TYPE_TEXT_FLAG_NO_SUGGESTIONS;
app.set_ime_editor_info(kind, TextInputAction::None, ImeOptions::IME_FLAG_NO_FULLSCREEN | ImeOptions::IMG_FLAG_NO_EXTRACT_UI);
reset_field(&app);
}
pub(super) fn take_text_input() -> Option<(usize, String)> {
let app = super::android_app()?;
let state = app.text_input_state();
let mut field = FIELD.lock().unwrap();
if state.compose_region.is_some() || field.is_empty() || state.text == *field {
return None;
}
let common = field.chars().zip(state.text.chars()).take_while(|(a, b)| a == b).count();
let deleted = field.chars().count() - common;
let typed: String = state.text.chars().skip(common).collect();
*field = state.text.clone();
let left = field.chars().take_while(|&c| c == '\u{200B}').count();
let caret_at_end = state.selection.start == state.selection.end && state.selection.end == field.encode_utf16().count();
drop(field);
if left < PLACEHOLDER_LOW || state.text.chars().count() > FIELD_MAX || !caret_at_end {
reset_field(&app);
}
Some((deleted, typed))
}
fn monotonic_ns() -> i64 {
let mut time = Timespec { seconds: 0, nanoseconds: 0 };
unsafe { clock_gettime(1, &mut time) };
time.seconds as i64 * 1_000_000_000 + time.nanoseconds as i64
}
pub(super) struct VsyncClock {
origin_ns: i64,
intervals: [i64; 16],
next: usize,
last_vsync: i64,
last_ms: f64,
}
impl VsyncClock {
pub(super) fn new() -> Self {
Self { origin_ns: monotonic_ns(), intervals: [16_666_667; 16], next: 0, last_vsync: 0, last_ms: 0.0 }
}
pub(super) fn frame_ms(&mut self, vsync_ns: i64, now_ms: f64) -> f64 {
let interval = vsync_ns - self.last_vsync;
if self.last_vsync > 0 && (2_000_000..100_000_000).contains(&interval) {
self.intervals[self.next] = interval;
self.next = (self.next + 1) % self.intervals.len();
}
self.last_vsync = vsync_ns;
let period = *self.intervals.iter().min().unwrap_or(&16_666_667);
let ms = (vsync_ns + period - self.origin_ns) as f64 / 1_000_000.0;
let ms = if (ms - now_ms).abs() > 250.0 { now_ms } else { ms };
self.last_ms = ms.max(self.last_ms);
self.last_ms
}
}
pub(super) fn log(text: &str) {
let (tag, text) = (c"drawnui", CString::new(text.replace('\0', " ")).unwrap_or_default());
unsafe { __android_log_write(4, tag.as_ptr(), text.as_ptr()) };
}
pub(super) fn system_property(name: &str) -> Option<String> {
unsafe extern "C" {
fn __system_property_get(name: *const c_char, value: *mut c_char) -> c_int;
}
let name = CString::new(name).ok()?;
let mut value = [0 as c_char; 92]; let len = unsafe { __system_property_get(name.as_ptr(), value.as_mut_ptr()) };
(len > 0).then(|| unsafe { std::ffi::CStr::from_ptr(value.as_ptr()) }.to_string_lossy().into_owned())
}
pub(super) fn log_panics() {
std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))));
}
pub(super) fn move_task_to_back() {
use jni_sys::{JNIEnv, JavaVM, jobject, jvalue};
use std::ptr::null_mut;
let Some(app) = super::android_app() else { return };
let (vm, activity) = (app.vm_as_ptr() as *mut JavaVM, app.activity_as_ptr() as jobject);
unsafe {
let mut env: *mut JNIEnv = null_mut();
let Some(attach) = (**vm).AttachCurrentThread else { return };
attach(vm, &mut env as *mut *mut JNIEnv as *mut *mut std::ffi::c_void, null_mut());
let e = &**env;
let (Some(get_class), Some(get_method), Some(call), Some(delete)) = (e.GetObjectClass, e.GetMethodID, e.CallBooleanMethodA, e.DeleteLocalRef) else { return };
let class = get_class(env, activity);
let method = get_method(env, class, c"moveTaskToBack".as_ptr(), c"(Z)Z".as_ptr());
delete(env, class);
if !method.is_null() {
call(env, activity, method, [jvalue { z: 1 }].as_ptr());
}
if let (Some(check), Some(clear)) = (e.ExceptionCheck, e.ExceptionClear)
&& check(env) != 0
{
clear(env);
}
}
}
pub(super) fn system_insets() -> Option<[i32; 4]> {
use jni_sys::{JNIEnv, JavaVM, jobject, jvalue};
use std::ptr::{null, null_mut};
let app = super::android_app()?;
let (vm, activity) = (app.vm_as_ptr() as *mut JavaVM, app.activity_as_ptr() as jobject);
unsafe {
let mut env: *mut JNIEnv = null_mut();
((**vm).AttachCurrentThread?)(vm, &mut env as *mut *mut JNIEnv as *mut *mut std::ffi::c_void, null_mut());
let e = &**env;
let failed = |env: *mut JNIEnv| {
let thrown = (e.ExceptionCheck.unwrap())(env) != 0;
if thrown {
(e.ExceptionClear.unwrap())(env);
}
thrown
};
let call_object = |obj: jobject, name: &std::ffi::CStr, signature: &std::ffi::CStr| -> Option<jobject> {
let class = (e.GetObjectClass?)(env, obj);
let method = (e.GetMethodID?)(env, class, name.as_ptr(), signature.as_ptr());
(e.DeleteLocalRef?)(env, class);
if method.is_null() || failed(env) {
return None;
}
let result = (e.CallObjectMethodA?)(env, obj, method, null());
if failed(env) || result.is_null() { None } else { Some(result) }
};
let call_int = |obj: jobject, name: &std::ffi::CStr, signature: &std::ffi::CStr, args: &[jvalue]| -> Option<i32> {
let class = (e.GetObjectClass?)(env, obj);
let method = (e.GetMethodID?)(env, class, name.as_ptr(), signature.as_ptr());
(e.DeleteLocalRef?)(env, class);
if method.is_null() || failed(env) {
return None;
}
let value = (e.CallIntMethodA?)(env, obj, method, args.as_ptr());
if failed(env) { None } else { Some(value) }
};
let window = call_object(activity, c"getWindow", c"()Landroid/view/Window;")?;
let decor = call_object(window, c"getDecorView", c"()Landroid/view/View;");
(e.DeleteLocalRef?)(env, window);
let decor = decor?;
let insets = call_object(decor, c"getRootWindowInsets", c"()Landroid/view/WindowInsets;");
(e.DeleteLocalRef?)(env, decor);
let insets = insets?;
let result = if app.config().sdk_version() >= 30 {
let types = (e.FindClass?)(env, c"android/view/WindowInsets$Type".as_ptr());
let static_int = |name: &std::ffi::CStr| -> Option<i32> {
let method = (e.GetStaticMethodID?)(env, types, name.as_ptr(), c"()I".as_ptr());
if method.is_null() || failed(env) {
return None;
}
let value = (e.CallStaticIntMethodA?)(env, types, method, null());
if failed(env) { None } else { Some(value) }
};
let mask = static_int(c"systemBars").zip(static_int(c"displayCutout")).map(|(a, b)| a | b);
(e.DeleteLocalRef?)(env, types);
let rect = mask.and_then(|mask| {
let class = (e.GetObjectClass?)(env, insets);
let method = (e.GetMethodID?)(env, class, c"getInsets".as_ptr(), c"(I)Landroid/graphics/Insets;".as_ptr());
(e.DeleteLocalRef?)(env, class);
if method.is_null() || failed(env) {
return None;
}
let rect = (e.CallObjectMethodA?)(env, insets, method, [jvalue { i: mask }].as_ptr());
if failed(env) || rect.is_null() { None } else { Some(rect) }
});
rect.and_then(|rect| {
let class = (e.GetObjectClass?)(env, rect);
let field = |name: &std::ffi::CStr| -> Option<i32> {
let id = (e.GetFieldID?)(env, class, name.as_ptr(), c"I".as_ptr());
if id.is_null() || failed(env) {
return None;
}
Some((e.GetIntField?)(env, rect, id))
};
let sides = [field(c"left"), field(c"top"), field(c"right"), field(c"bottom")];
(e.DeleteLocalRef?)(env, class);
(e.DeleteLocalRef?)(env, rect);
Some([sides[0]?, sides[1]?, sides[2]?, sides[3]?])
})
} else {
let side = |name: &std::ffi::CStr| call_int(insets, name, c"()I", &[]);
Some([
side(c"getSystemWindowInsetLeft")?,
side(c"getSystemWindowInsetTop")?,
side(c"getSystemWindowInsetRight")?,
side(c"getSystemWindowInsetBottom")?,
])
};
(e.DeleteLocalRef?)(env, insets);
result
}
}
}
fn open_url(url: &str) {
#[cfg(windows)]
let mut command = std::process::Command::new("rundll32.exe");
#[cfg(windows)]
command.args(["url.dll,FileProtocolHandler", url]);
#[cfg(target_os = "macos")]
let mut command = std::process::Command::new("open");
#[cfg(target_os = "macos")]
command.arg(url);
#[cfg(not(any(windows, target_os = "macos")))]
let mut command = std::process::Command::new("xdg-open");
#[cfg(not(any(windows, target_os = "macos")))]
command.arg(url);
if let Err(e) = command.spawn() {
eprintln!("drawnui: could not open {url}: {e}");
}
}
mod clipboard {
#[cfg(target_os = "linux")]
fn with<T>(f: impl FnOnce(&mut arboard::Clipboard) -> Option<T>) -> Option<T> {
static CLIPBOARD: std::sync::Mutex<Option<arboard::Clipboard>> = std::sync::Mutex::new(None);
let mut clipboard = CLIPBOARD.lock().ok()?;
if clipboard.is_none() {
*clipboard = arboard::Clipboard::new().ok();
}
f(clipboard.as_mut()?)
}
#[cfg(target_os = "linux")]
pub fn set(text: &str) -> bool {
with(|clipboard| clipboard.set_text(text).ok()).is_some()
}
#[cfg(target_os = "linux")]
pub fn get() -> Option<String> {
with(|clipboard| clipboard.get_text().ok())
}
#[cfg(windows)]
pub fn set(text: &str) -> bool {
const GMEM_MOVEABLE: u32 = 2;
let wide: Vec<u16> = text.encode_utf16().chain([0]).collect();
unsafe {
if win::OpenClipboard(std::ptr::null_mut()) == 0 {
return false;
}
win::EmptyClipboard();
let memory = win::GlobalAlloc(GMEM_MOVEABLE, wide.len() * 2);
let mut done = false;
if !memory.is_null() {
let to = win::GlobalLock(memory) as *mut u16;
if !to.is_null() {
std::ptr::copy_nonoverlapping(wide.as_ptr(), to, wide.len());
win::GlobalUnlock(memory);
done = !win::SetClipboardData(win::CF_UNICODETEXT, memory).is_null();
}
if !done {
win::GlobalFree(memory);
}
}
win::CloseClipboard();
done
}
}
#[cfg(windows)]
pub fn get() -> Option<String> {
unsafe {
if win::OpenClipboard(std::ptr::null_mut()) == 0 {
return None;
}
let memory = win::GetClipboardData(win::CF_UNICODETEXT);
let from = if memory.is_null() { std::ptr::null() } else { win::GlobalLock(memory) as *const u16 };
let text = (!from.is_null()).then(|| {
let len = (0..).take_while(|i| *from.add(*i) != 0).count();
String::from_utf16_lossy(std::slice::from_raw_parts(from, len))
});
if !from.is_null() {
win::GlobalUnlock(memory);
}
win::CloseClipboard();
text
}
}
#[cfg(windows)]
#[allow(non_snake_case)]
mod win {
use std::ffi::c_void;
pub const CF_UNICODETEXT: u32 = 13;
#[link(name = "user32")]
unsafe extern "system" {
pub fn OpenClipboard(owner: *mut c_void) -> i32;
pub fn CloseClipboard() -> i32;
pub fn EmptyClipboard() -> i32;
pub fn GetClipboardData(format: u32) -> *mut c_void;
pub fn SetClipboardData(format: u32, memory: *mut c_void) -> *mut c_void;
}
#[link(name = "kernel32")]
unsafe extern "system" {
pub fn GlobalAlloc(flags: u32, bytes: usize) -> *mut c_void;
pub fn GlobalLock(memory: *mut c_void) -> *mut c_void;
pub fn GlobalUnlock(memory: *mut c_void) -> i32;
pub fn GlobalFree(memory: *mut c_void) -> *mut c_void;
}
}
#[cfg(not(any(windows, target_os = "linux")))]
pub fn set(_text: &str) -> bool {
false
}
#[cfg(not(any(windows, target_os = "linux")))]
pub fn get() -> Option<String> {
None
}
}
macro_rules! desktop_key_name {
($($code:ident),*) => {
fn key_name(code: KeyCode) -> InputKey {
match code {
$(KeyCode::$code => stringify!($code),)*
KeyCode::SuperLeft => "MetaLeft",
KeyCode::SuperRight => "MetaRight",
_ => "Unknown",
}
}
};
}
with_key_codes!(desktop_key_name);
fn mouse_button(button: WinitButton) -> Option<MouseButton> {
match button {
WinitButton::Left => Some(MouseButton::Left),
WinitButton::Right => Some(MouseButton::Right),
WinitButton::Middle => Some(MouseButton::Middle),
WinitButton::Back => Some(MouseButton::Back),
WinitButton::Forward => Some(MouseButton::Forward),
WinitButton::Other(_) => None,
}
}
impl ApplicationHandler<Arrived> for Desktop {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.access.is_none() && self.window.is_visible() != Some(true) {
self.access = Some(accesskit_winit::Adapter::with_event_loop_proxy(event_loop, &self.window, self.proxy.clone()));
}
self.window.set_visible(true);
if self.suspended {
#[cfg(target_os = "android")]
{
self.presenter.resume(&self.window);
self.surface = None;
self.insets_read = false;
}
self.suspended = false;
self.app.visibility(true);
self.redraw();
}
}
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
self.suspended = true;
#[cfg(any(target_os = "macos", target_os = "ios"))]
if let Some(ticker) = &self.ticker {
ticker.run(false);
}
self.app.visibility(false);
#[cfg(target_os = "android")]
{
self.surface = None;
self.presenter.suspend();
}
}
fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
if matches!(cause, StartCause::ResumeTimeReached { .. }) {
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
if self.pacing.due.is_some_and(|at| at <= Instant::now()) {
self.pacing.due = None;
self.window.request_redraw();
return;
}
self.redraw();
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
#[cfg(target_os = "android")]
if self.ime_open {
self.take_soft_keyboard_text();
}
let wake = self.wake;
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
let wake = match (wake, self.pacing.due) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
};
event_loop.set_control_flow(wake.map_or(ControlFlow::Wait, ControlFlow::WaitUntil));
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, arrived: Arrived) {
match arrived {
Arrived::Image(id, decoded) => {
let started = Instant::now();
self.app.image(id, decoded);
self.stats.image_ms.push(started.elapsed().as_secs_f32() * 1000.0);
}
Arrived::Baked(id, image) => self.app.baked(id, image),
Arrived::Access(event) => self.access_event(event.window_event),
#[cfg(any(target_os = "macos", target_os = "ios"))]
Arrived::Tick => {
if !self.suspended && std::mem::take(&mut self.frame_wanted) {
self.draw();
}
if !self.frame_wanted
&& let Some(ticker) = &self.ticker
{
ticker.run(false);
}
return;
}
#[cfg(target_os = "android")]
Arrived::Vsync(ns) => {
self.frame_posted = false;
if !self.suspended && std::mem::take(&mut self.frame_wanted) {
self.frame_posted = android::post_frame(&self.proxy);
self.vsync_ns = Some(ns);
self.window.request_redraw();
}
return;
}
}
self.redraw();
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
if let Some(access) = &mut self.access {
access.process_event(&self.window, &event);
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
#[cfg(target_os = "android")]
let kept = std::mem::replace(&mut self.size, size) == size;
#[cfg(not(target_os = "android"))]
let kept = self.surface.as_ref().is_some_and(|s| (s.width() as u32, s.height() as u32) == (size.width, size.height));
if !kept {
self.resized_at = Some(Instant::now());
self.surface = None;
self.presenter.resize(&self.window, size.width.max(1), size.height.max(1));
}
#[cfg(target_os = "android")]
{
self.insets_read = false;
}
#[cfg(any(target_os = "ios", target_os = "android"))]
self.report_safe_area();
self.redraw();
}
WindowEvent::CursorMoved { position, .. } => {
self.cursor = (position.x as f32, position.y as f32);
let (kind, button) = self.pressed.map_or((PointerKind::Hover, MouseButton::Left), |b| (PointerKind::Move, b));
self.app.pointer_button(kind, button, self.cursor.0, self.cursor.1, self.now_ms());
self.redraw();
}
WindowEvent::CursorLeft { .. } if self.pressed.is_none() => {
self.app.pointer_button(PointerKind::Leave, MouseButton::Left, self.cursor.0, self.cursor.1, self.now_ms());
self.redraw();
}
WindowEvent::MouseInput { state, button, .. } => {
let Some(button) = mouse_button(button) else { return };
let (x, y, now) = (self.cursor.0, self.cursor.1, self.now_ms());
if state == ElementState::Pressed {
self.pressed.get_or_insert(button);
self.app.pointer_button(PointerKind::Down, button, x, y, now);
} else {
if self.pressed == Some(button) {
self.pressed = None;
}
self.app.pointer_button(PointerKind::Up, button, x, y, now);
if button == MouseButton::Right {
self.app.context_menu(x, y, ContextMenuSource::Mouse, now);
}
}
self.redraw();
}
WindowEvent::Focused(false) => {
if let Some(button) = self.pressed.take() {
self.app.pointer_button(PointerKind::Cancel, button, self.cursor.0, self.cursor.1, self.now_ms());
}
self.app.blur();
self.redraw();
}
WindowEvent::Occluded(occluded) => {
self.occluded = occluded;
self.app.visibility(!occluded);
self.redraw();
}
WindowEvent::ModifiersChanged(modifiers) => {
let m = modifiers.state();
self.modifiers = Modifiers { shift: m.shift_key(), ctrl: m.control_key(), alt: m.alt_key(), meta: m.super_key() };
}
WindowEvent::KeyboardInput { event, .. } => {
if self.ime_composing || event.logical_key == Key::Named(NamedKey::Process) {
return;
}
#[cfg(target_os = "android")]
if event.logical_key == Key::Named(NamedKey::BrowserBack) {
let down = event.state == ElementState::Pressed;
let used = self.key(if down { KeyKind::Down } else { KeyKind::Up }, "BrowserBack", "", event.repeat);
if down && !event.repeat && !used && !self.app.back() {
android::move_task_to_back();
}
self.redraw();
return;
}
let key = match event.physical_key {
PhysicalKey::Code(code) => key_name(code),
#[cfg(target_os = "ios")]
PhysicalKey::Unidentified(_) if matches!(&event.logical_key, Key::Character(c) if c == "\n" || c == "\r") => "Enter",
PhysicalKey::Unidentified(_) => "Unknown",
};
let down = event.state == ElementState::Pressed;
let used = self.key(if down { KeyKind::Down } else { KeyKind::Up }, key, "", event.repeat);
let m = self.modifiers;
let plain = !(m.ctrl || m.alt || m.meta) || (m.ctrl && m.alt && !m.meta);
if let Some(text) = event.text.as_deref().filter(|t| down && plain && !t.chars().any(char::is_control)) {
self.key(KeyKind::Char, "", text, event.repeat);
}
if down && !used && key == "ContextMenu" {
let now = self.now_ms();
self.app.context_menu(self.cursor.0, self.cursor.1, ContextMenuSource::Keyboard, now);
}
}
WindowEvent::Ime(Ime::Commit(text)) => {
self.ime_composing = false;
self.key(KeyKind::Char, "", &text, false);
}
WindowEvent::Ime(Ime::Preedit(text, _)) => self.ime_composing = !text.is_empty(),
WindowEvent::Ime(Ime::Disabled) => self.ime_composing = false,
WindowEvent::MouseWheel { delta, .. } => {
let (x, y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x, y),
MouseScrollDelta::PixelDelta(p) => {
let notch = 100.0 * self.window.scale_factor();
((p.x / notch) as f32, (p.y / notch) as f32)
}
};
let (dominant, horizontal) = if y.abs() >= x.abs() { (y, false) } else { (x, true) };
if self.app.wheel(self.cursor.0, self.cursor.1, dominant, horizontal, self.now_ms()) {
self.redraw();
}
}
#[cfg(any(target_os = "ios", target_os = "android"))]
WindowEvent::Touch(touch) => {
use winit::event::TouchPhase;
let (x, y, now) = (touch.location.x as f32, touch.location.y as f32, self.now_ms());
let kind = match touch.phase {
TouchPhase::Started if self.touch.is_none() => {
self.touch = Some(touch.id);
self.pressed = Some(MouseButton::Left);
PointerKind::Down
}
_ if self.touch != Some(touch.id) => return,
TouchPhase::Started | TouchPhase::Moved => PointerKind::Move,
TouchPhase::Ended => PointerKind::Up,
TouchPhase::Cancelled => PointerKind::Cancel,
};
if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
(self.touch, self.pressed) = (None, None);
}
self.cursor = (x, y);
self.app.pointer_touch(kind, x, y, now);
self.redraw();
}
WindowEvent::RedrawRequested => self.draw(),
_ => {}
}
}
}
#[cfg(target_os = "android")]
mod android_gpu {
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes};
use super::{android, gl, vulkan};
use crate::GpuBackend;
use crate::{Gpu, skia};
pub(super) enum Presenter {
Gl(gl::Presenter),
Vulkan(vulkan::Presenter),
}
impl Presenter {
pub(super) fn new_active(event_loop: &ActiveEventLoop, attributes: WindowAttributes, backend: GpuBackend) -> (Window, Self) {
let backend = match android::system_property("debug.drawnui.gpu").as_deref() {
Some("gl" | "opengl") => GpuBackend::OpenGl,
Some("vulkan" | "vk") => GpuBackend::Vulkan,
_ => backend,
};
if backend == GpuBackend::OpenGl {
android::log("drawnui: OpenGL ES");
let (window, presenter) = gl::Presenter::new_active(event_loop, attributes);
return (window, Self::Gl(presenter));
}
let window = event_loop.create_window(attributes).expect("no window");
match vulkan::Presenter::new(&window) {
Ok(presenter) => {
android::log(&format!("drawnui: Vulkan {}", presenter.describe()));
(window, Self::Vulkan(presenter))
}
Err(e) => {
android::log(&format!("drawnui: no Vulkan ({e}), OpenGL ES"));
let (window, presenter) = gl::Presenter::for_window(event_loop, window);
(window, Self::Gl(presenter))
}
}
}
pub(super) fn suspend(&mut self) {
match self {
Self::Gl(p) => p.suspend(),
Self::Vulkan(p) => p.suspend(),
}
}
pub(super) fn resume(&mut self, window: &Window) {
match self {
Self::Gl(p) => p.resume(window),
Self::Vulkan(p) => p.resume(window),
}
}
pub(super) fn gpu(&self) -> Gpu {
match self {
Self::Gl(p) => p.gpu(),
Self::Vulkan(p) => p.gpu(None),
}
}
pub(super) fn gpu_after(&mut self, window: &Window, lost: &mut Gpu) -> Gpu {
match self {
Self::Gl(p) => p.gpu_after(window, lost),
Self::Vulkan(p) => p.gpu(Some(lost)),
}
}
pub(super) fn begin<'a>(
&mut self,
gpu: &mut Gpu,
surface: &'a mut Option<skia::Surface>,
width: u32,
height: u32,
) -> Option<&'a mut skia::Surface> {
match self {
Self::Gl(p) => p.begin(gpu, surface, width, height),
Self::Vulkan(p) => p.begin(gpu, surface, width, height),
}
}
pub(super) fn end_frame(&mut self, gpu: &mut Gpu, surface: &mut Option<skia::Surface>) {
match self {
Self::Gl(p) => p.end_frame(gpu, surface),
Self::Vulkan(p) => p.end_frame(gpu, surface),
}
}
pub(super) fn present(&mut self, surface: &mut Option<skia::Surface>) {
match self {
Self::Gl(p) => p.present(surface),
Self::Vulkan(p) => p.present(surface),
}
}
pub(super) fn resize(&mut self, window: &Window, width: u32, height: u32) {
match self {
Self::Gl(p) => p.resize(window, width, height),
Self::Vulkan(p) => p.resize(width, height),
}
}
pub(super) fn wait_idle(&mut self) {
if let Self::Vulkan(p) = self {
p.wait_idle();
}
}
}
}
#[cfg(target_os = "android")]
mod vulkan {
use std::ffi::{c_char, c_void};
use ash::khr;
use ash::vk::{self, Handle};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use winit::window::Window;
use crate::{Gpu, skia};
pub(super) struct Presenter {
entry: ash::Entry,
instance: ash::Instance,
version: u32,
physical: vk::PhysicalDevice,
device: ash::Device,
family: u32,
queue: vk::Queue,
surface_fn: khr::surface::Instance,
android_fn: khr::android_surface::Instance,
swapchain_fn: khr::swapchain::Device,
surface: vk::SurfaceKHR,
swapchain: vk::SwapchainKHR,
rgba: bool,
usage: vk::ImageUsageFlags,
extent: vk::Extent2D,
images: Vec<vk::Image>,
surfaces: Vec<Option<skia::Surface>>,
acquire: Vec<vk::Semaphore>,
rendered: Vec<vk::Semaphore>,
next: usize,
frame: Option<(u32, bool)>,
stale: bool,
}
impl Presenter {
pub(super) fn new(window: &Window) -> Result<Self, String> {
unsafe {
let entry = ash::Entry::load().map_err(|e| format!("no libvulkan: {e}"))?;
let loader = entry.try_enumerate_instance_version().ok().flatten().unwrap_or(vk::API_VERSION_1_0);
let app = vk::ApplicationInfo::default().api_version(loader.min(vk::API_VERSION_1_3));
let extensions = [khr::surface::NAME.as_ptr(), khr::android_surface::NAME.as_ptr()];
let info = vk::InstanceCreateInfo::default().application_info(&app).enabled_extension_names(&extensions);
let instance = entry.create_instance(&info, None).map_err(|e| format!("instance: {e}"))?;
let surface_fn = khr::surface::Instance::new(&entry, &instance);
let android_fn = khr::android_surface::Instance::new(&entry, &instance);
let surface = match window_surface(&android_fn, window) {
Ok(surface) => surface,
Err(e) => {
instance.destroy_instance(None);
return Err(e);
}
};
let found = instance.enumerate_physical_devices().unwrap_or_default().into_iter().find_map(|p| {
let families = instance.get_physical_device_queue_family_properties(p);
families.iter().enumerate().find_map(|(i, f)| {
let present = surface_fn.get_physical_device_surface_support(p, i as u32, surface).unwrap_or(false);
(f.queue_flags.contains(vk::QueueFlags::GRAPHICS) && present).then_some((p, i as u32))
})
});
let Some((physical, family)) = found else {
surface_fn.destroy_surface(surface, None);
instance.destroy_instance(None);
return Err("no device draws to the window".into());
};
let version = instance.get_physical_device_properties(physical).api_version.min(app.api_version);
let priorities = [1.0];
let queues = [vk::DeviceQueueCreateInfo::default().queue_family_index(family).queue_priorities(&priorities)];
let extensions = [khr::swapchain::NAME.as_ptr()];
let info = vk::DeviceCreateInfo::default().queue_create_infos(&queues).enabled_extension_names(&extensions);
let device = match instance.create_device(physical, &info, None) {
Ok(device) => device,
Err(e) => {
surface_fn.destroy_surface(surface, None);
instance.destroy_instance(None);
return Err(format!("device: {e}"));
}
};
let queue = device.get_device_queue(family, 0);
let swapchain_fn = khr::swapchain::Device::new(&instance, &device);
Ok(Self {
entry,
instance,
version,
physical,
device,
family,
queue,
surface_fn,
android_fn,
swapchain_fn,
surface,
swapchain: vk::SwapchainKHR::null(),
rgba: true,
usage: vk::ImageUsageFlags::COLOR_ATTACHMENT,
extent: vk::Extent2D::default(),
images: Vec::new(),
surfaces: Vec::new(),
acquire: Vec::new(),
rendered: Vec::new(),
next: 0,
frame: None,
stale: true,
})
}
}
pub(super) fn describe(&self) -> String {
let properties = unsafe { self.instance.get_physical_device_properties(self.physical) };
let name = properties.device_name_as_c_str().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let v = self.version;
format!("{}.{} on {name}", vk::api_version_major(v), vk::api_version_minor(v))
}
pub(super) fn gpu(&self, lost: Option<&mut Gpu>) -> Gpu {
let entry = self.entry.clone();
let device_proc_addr = self.instance.fp_v1_0().get_device_proc_addr;
let instance_proc = move |instance: u64, name: *const c_char| -> *const c_void {
unsafe { entry.get_instance_proc_addr(vk::Instance::from_raw(instance), name) }.map_or(std::ptr::null(), |f| f as *const c_void)
};
let device_proc = move |device: u64, name: *const c_char| -> *const c_void {
unsafe { device_proc_addr(vk::Device::from_raw(device), name) }.map_or(std::ptr::null(), |f| f as *const c_void)
};
unsafe {
Gpu::new_vulkan(
self.instance.handle().as_raw(),
self.physical.as_raw(),
self.device.handle().as_raw(),
self.queue.as_raw(),
self.family,
self.version,
&instance_proc,
&device_proc,
lost,
)
}
.expect("no Vulkan GPU context")
}
pub(super) fn suspend(&mut self) {
self.wait_idle();
self.destroy_swapchain();
unsafe { self.surface_fn.destroy_surface(self.surface, None) };
self.surface = vk::SurfaceKHR::null();
}
pub(super) fn resume(&mut self, window: &Window) {
match window_surface(&self.android_fn, window) {
Ok(surface) => self.surface = surface,
Err(e) => super::android::log(&format!("drawnui: Vulkan: {e}")),
}
self.stale = true;
}
pub(super) fn resize(&mut self, width: u32, height: u32) {
if (width, height) != (self.extent.width, self.extent.height) {
self.stale = true;
}
}
pub(super) fn wait_idle(&mut self) {
let _ = unsafe { self.device.device_wait_idle() };
}
pub(super) fn begin<'a>(
&mut self,
gpu: &mut Gpu,
surface: &'a mut Option<skia::Surface>,
width: u32,
height: u32,
) -> Option<&'a mut skia::Surface> {
if self.surface.is_null() {
return None;
}
if self.stale || (width, height) != (self.extent.width, self.extent.height) {
self.make_swapchain(width, height)?;
}
let index = match self.acquire_image() {
Some(index) => index,
None => {
self.make_swapchain(width, height)?;
self.acquire_image()?
}
};
let (w, h) = (self.extent.width as i32, self.extent.height as i32);
let (raw, rgba, usage) = (self.images[index as usize].as_raw(), self.rgba, self.usage.as_raw());
let slot = &mut self.surfaces[index as usize];
if slot.is_none() {
*slot = unsafe { gpu.vulkan_surface(raw, rgba, usage, w, h) };
}
let mut image = slot.clone()?;
unsafe { gpu.vulkan_wait(&mut image, self.acquire[self.next].as_raw()) };
self.frame = Some((index, false));
*surface = Some(image);
surface.as_mut()
}
pub(super) fn end_frame(&mut self, gpu: &mut Gpu, surface: &mut Option<skia::Surface>) {
if let (Some(image), Some((index, _))) = (surface.as_mut(), self.frame) {
let signaled = unsafe { gpu.vulkan_flush(image, self.rendered[index as usize].as_raw()) };
self.frame = Some((index, signaled));
}
}
pub(super) fn present(&mut self, surface: &mut Option<skia::Surface>) {
*surface = None;
let Some((index, signaled)) = self.frame.take() else { return };
let waits = [self.rendered[index as usize]];
let (swapchains, indices) = ([self.swapchain], [index]);
let mut info = vk::PresentInfoKHR::default().swapchains(&swapchains).image_indices(&indices);
if signaled {
info = info.wait_semaphores(&waits);
}
match unsafe { self.swapchain_fn.queue_present(self.queue, &info) } {
Ok(suboptimal) => self.stale |= suboptimal,
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => self.stale = true,
Err(e) => super::android::log(&format!("drawnui: Vulkan present: {e}")),
}
self.next = (self.next + 1) % self.acquire.len().max(1);
}
fn acquire_image(&mut self) -> Option<u32> {
match unsafe { self.swapchain_fn.acquire_next_image(self.swapchain, u64::MAX, self.acquire[self.next], vk::Fence::null()) } {
Ok((index, suboptimal)) => {
self.stale |= suboptimal;
Some(index)
}
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => None,
Err(e) => {
super::android::log(&format!("drawnui: Vulkan acquire: {e}"));
None
}
}
}
fn make_swapchain(&mut self, width: u32, height: u32) -> Option<()> {
self.wait_idle();
self.surfaces.clear();
unsafe {
let caps = self.surface_fn.get_physical_device_surface_capabilities(self.physical, self.surface).ok()?;
let formats = self.surface_fn.get_physical_device_surface_formats(self.physical, self.surface).ok()?;
let format = [vk::Format::R8G8B8A8_UNORM, vk::Format::B8G8R8A8_UNORM]
.into_iter()
.find_map(|wanted| formats.iter().find(|f| f.format == wanted))
.copied()?;
let extent = if caps.current_extent.width != u32::MAX {
caps.current_extent
} else {
vk::Extent2D {
width: width.clamp(caps.min_image_extent.width, caps.max_image_extent.width),
height: height.clamp(caps.min_image_extent.height, caps.max_image_extent.height),
}
};
let mut count = caps.min_image_count.max(3);
if caps.max_image_count > 0 {
count = count.min(caps.max_image_count);
}
let optional = vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED;
let usage = vk::ImageUsageFlags::COLOR_ATTACHMENT | (caps.supported_usage_flags & optional);
let transform = if caps.supported_transforms.contains(vk::SurfaceTransformFlagsKHR::IDENTITY) {
vk::SurfaceTransformFlagsKHR::IDENTITY
} else {
caps.current_transform
};
let alpha = [
vk::CompositeAlphaFlagsKHR::OPAQUE,
vk::CompositeAlphaFlagsKHR::INHERIT,
vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED,
vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED,
]
.into_iter()
.find(|a| caps.supported_composite_alpha.contains(*a))
.unwrap_or(vk::CompositeAlphaFlagsKHR::OPAQUE);
let old = self.swapchain;
let info = vk::SwapchainCreateInfoKHR::default()
.surface(self.surface)
.min_image_count(count)
.image_format(format.format)
.image_color_space(format.color_space)
.image_extent(extent)
.image_array_layers(1)
.image_usage(usage)
.image_sharing_mode(vk::SharingMode::EXCLUSIVE)
.pre_transform(transform)
.composite_alpha(alpha)
.present_mode(vk::PresentModeKHR::FIFO)
.clipped(true)
.old_swapchain(old);
let made = self.swapchain_fn.create_swapchain(&info, None);
self.destroy_swapchain();
let swapchain = match made {
Ok(swapchain) => swapchain,
Err(e) => {
super::android::log(&format!("drawnui: Vulkan swapchain: {e}"));
return None;
}
};
self.swapchain = swapchain;
self.images = self.swapchain_fn.get_swapchain_images(swapchain).ok()?;
self.surfaces = self.images.iter().map(|_| None).collect();
let device = &self.device;
let semaphore = || device.create_semaphore(&vk::SemaphoreCreateInfo::default(), None).ok();
self.acquire = (0..=self.images.len()).filter_map(|_| semaphore()).collect();
self.rendered = (0..self.images.len()).filter_map(|_| semaphore()).collect();
self.rgba = format.format == vk::Format::R8G8B8A8_UNORM;
(self.usage, self.extent, self.next, self.stale) = (usage, extent, 0, false);
}
Some(())
}
fn destroy_swapchain(&mut self) {
self.surfaces.clear();
self.images.clear();
unsafe {
for semaphore in self.acquire.drain(..).chain(self.rendered.drain(..)) {
self.device.destroy_semaphore(semaphore, None);
}
if !self.swapchain.is_null() {
self.swapchain_fn.destroy_swapchain(self.swapchain, None);
}
}
self.swapchain = vk::SwapchainKHR::null();
}
}
impl Drop for Presenter {
fn drop(&mut self) {
self.wait_idle();
self.destroy_swapchain();
unsafe {
if !self.surface.is_null() {
self.surface_fn.destroy_surface(self.surface, None);
}
self.device.destroy_device(None);
self.instance.destroy_instance(None);
}
}
}
fn window_surface(android_fn: &khr::android_surface::Instance, window: &Window) -> Result<vk::SurfaceKHR, String> {
let handle = window.window_handle().map_err(|e| e.to_string())?.as_raw();
let RawWindowHandle::AndroidNdk(handle) = handle else { return Err("not an Android window".into()) };
let info = vk::AndroidSurfaceCreateInfoKHR::default().window(handle.a_native_window.as_ptr().cast());
unsafe { android_fn.create_android_surface(&info, None) }.map_err(|e| format!("surface: {e}"))
}
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
mod gl {
use std::{ffi::CString, num::NonZeroU32};
use glutin::{
config::{Config as GlConfigHandle, ConfigTemplateBuilder, GlConfig},
context::{ContextApi, ContextAttributesBuilder, NotCurrentContext, PossiblyCurrentContext, Robustness},
display::{GetGlDisplay, GlDisplay},
prelude::{GlSurface, NotCurrentGlContext},
surface::{Surface as GlSurfaceHandle, SurfaceAttributesBuilder, SwapInterval, WindowSurface},
};
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
#[cfg(not(target_os = "android"))]
use winit::event_loop::EventLoop;
use winit::window::{Window, WindowAttributes};
#[cfg(not(target_os = "android"))]
use super::Arrived;
use crate::{Gpu, skia};
macro_rules! display {
($event_loop:expr, $attributes:expr) => {
DisplayBuilder::new()
.with_window_attributes($attributes.into())
.build($event_loop, ConfigTemplateBuilder::new().with_alpha_size(8).with_stencil_size(8), |configs| {
configs.reduce(|best, c| if c.num_samples() < best.num_samples() { c } else { best }).unwrap()
})
.expect("no GL config")
};
}
pub(super) struct Presenter {
gl_surface: Option<GlSurfaceHandle<WindowSurface>>,
gl_context: PossiblyCurrentContext,
gl_config: GlConfigHandle,
samples: usize,
stencil: usize,
}
impl Presenter {
#[cfg(not(target_os = "android"))]
pub(super) fn new(event_loop: &EventLoop<Arrived>, attributes: WindowAttributes) -> (Window, Self) {
let (window, gl_config) = display!(event_loop, attributes);
Self::with(window, gl_config)
}
#[cfg(target_os = "android")]
pub(super) fn new_active(event_loop: &winit::event_loop::ActiveEventLoop, attributes: WindowAttributes) -> (Window, Self) {
let (window, gl_config) = display!(event_loop, attributes);
Self::with(window, gl_config)
}
#[cfg(target_os = "android")]
pub(super) fn for_window(event_loop: &winit::event_loop::ActiveEventLoop, window: Window) -> (Window, Self) {
let (_, gl_config) = DisplayBuilder::new()
.build(event_loop, ConfigTemplateBuilder::new().with_alpha_size(8).with_stencil_size(8), |configs| {
configs.reduce(|best, c| if c.num_samples() < best.num_samples() { c } else { best }).unwrap()
})
.expect("no GL config");
Self::with(Some(window), gl_config)
}
fn with(window: Option<Window>, gl_config: GlConfigHandle) -> (Window, Self) {
let window = window.expect("no window");
let raw_handle = window.window_handle().expect("no window handle").as_raw();
let context = gl_context_for(&gl_config, raw_handle);
let gl_surface = window_surface(&gl_config, &window);
let gl_context = context.make_current(&gl_surface).expect("GL context not current");
if let Err(e) = gl_surface.set_swap_interval(&gl_context, SwapInterval::Wait(NonZeroU32::new(1).unwrap())) {
eprintln!("drawnui: vsync not set: {e}");
}
let samples = gl_config.num_samples() as usize;
let stencil = gl_config.stencil_size() as usize;
(window, Self { gl_surface: Some(gl_surface), gl_context, gl_config, samples, stencil })
}
#[cfg(target_os = "android")]
pub(super) fn suspend(&mut self) {
self.gl_surface = None;
}
#[cfg(target_os = "android")]
pub(super) fn resume(&mut self, window: &Window) {
use glutin::prelude::PossiblyCurrentGlContext;
let gl_surface = window_surface(&self.gl_config, window);
self.gl_context.make_current(&gl_surface).expect("GL context not current");
self.gl_surface = Some(gl_surface);
}
pub(super) fn gpu(&self) -> Gpu {
Gpu::new_gl(gl_interface(&self.gl_config)).expect("no GPU context")
}
pub(super) fn gpu_after(&mut self, window: &Window, lost: &mut Gpu) -> Gpu {
let raw_handle = window.window_handle().expect("no window handle").as_raw();
let surface = self.gl_surface.as_ref().expect("a window surface");
self.gl_context = gl_context_for(&self.gl_config, raw_handle).make_current(surface).expect("GL context not current");
Gpu::new_gl_after(gl_interface(&self.gl_config), lost).expect("no GPU context")
}
pub(super) fn begin<'a>(
&mut self,
gpu: &mut Gpu,
surface: &'a mut Option<skia::Surface>,
width: u32,
height: u32,
) -> Option<&'a mut skia::Surface> {
if surface.is_none() {
*surface = Some(gpu.window_surface(width as i32, height as i32, self.samples, self.stencil).expect("no window surface"));
}
surface.as_mut()
}
pub(super) fn end_frame(&mut self, gpu: &mut Gpu, surface: &mut Option<skia::Surface>) {
if let Some(surface) = surface {
gpu.end_frame(surface);
}
}
pub(super) fn present(&mut self, _surface: &mut Option<skia::Surface>) {
if let Some(gl_surface) = &self.gl_surface {
gl_surface.swap_buffers(&self.gl_context).expect("swap failed");
}
}
pub(super) fn resize(&mut self, _window: &Window, width: u32, height: u32) {
if let Some(gl_surface) = &self.gl_surface {
gl_surface.resize(&self.gl_context, NonZeroU32::new(width).unwrap(), NonZeroU32::new(height).unwrap());
}
}
}
fn window_surface(gl_config: &GlConfigHandle, window: &Window) -> GlSurfaceHandle<WindowSurface> {
let raw_handle = window.window_handle().expect("no window handle").as_raw();
let (width, height): (u32, u32) = window.inner_size().into();
let surface_attributes = SurfaceAttributesBuilder::<WindowSurface>::new().build(
raw_handle,
NonZeroU32::new(width.max(1)).unwrap(),
NonZeroU32::new(height.max(1)).unwrap(),
);
unsafe { gl_config.display().create_window_surface(gl_config, &surface_attributes).expect("no GL surface") }
}
fn gl_context_for(gl_config: &GlConfigHandle, raw_handle: raw_window_handle::RawWindowHandle) -> NotCurrentContext {
let display = gl_config.display();
let robust = ContextAttributesBuilder::new().with_robustness(Robustness::RobustLoseContextOnReset).build(Some(raw_handle));
let plain = ContextAttributesBuilder::new().build(Some(raw_handle));
let gles = ContextAttributesBuilder::new().with_context_api(ContextApi::Gles(None)).build(Some(raw_handle));
unsafe {
display
.create_context(gl_config, &robust)
.or_else(|_| display.create_context(gl_config, &plain))
.or_else(|_| display.create_context(gl_config, &gles))
.expect("no GL context")
}
}
fn gl_interface(gl_config: &GlConfigHandle) -> skia::gpu::gl::Interface {
let display = gl_config.display();
skia::gpu::gl::Interface::new_load_with(|name| {
if name == "eglGetCurrentDisplay" {
return std::ptr::null();
}
display.get_proc_address(CString::new(name).unwrap().as_c_str())
})
.expect("no GL interface")
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
mod metal {
use objc2::{
rc::{Retained, autoreleasepool},
runtime::ProtocolObject,
};
#[cfg(target_os = "macos")]
use objc2_app_kit::NSView;
use objc2_core_foundation::CGSize;
#[cfg(target_os = "ios")]
use objc2_ui_kit::UIView;
use objc2_metal::{MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable, MTLPixelFormat};
use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use winit::window::Window;
use crate::{Gpu, skia, skia::gpu::mtl};
pub(super) struct Presenter {
drawable: Option<Retained<ProtocolObject<dyn CAMetalDrawable>>>,
layer: Retained<CAMetalLayer>,
queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
device: Retained<ProtocolObject<dyn MTLDevice>>,
#[cfg(target_os = "ios")]
view: Retained<UIView>,
}
impl Presenter {
pub(super) fn new(window: Window, background: skia::Color) -> (Window, Self) {
#[cfg(not(target_os = "ios"))]
let _ = background;
let device = MTLCreateSystemDefaultDevice().expect("no Metal device");
let queue = device.newCommandQueue().expect("no Metal command queue");
let layer = CAMetalLayer::new();
layer.setDevice(Some(&device));
layer.setPixelFormat(MTLPixelFormat::BGRA8Unorm);
layer.setFramebufferOnly(false);
layer.setPresentsWithTransaction(false);
#[cfg(target_os = "macos")]
let presenter = {
let RawWindowHandle::AppKit(handle) = window.window_handle().expect("no window handle").as_raw() else {
panic!("not an AppKit window");
};
let view = unsafe { &*(handle.ns_view.as_ptr() as *const NSView) };
view.setWantsLayer(true);
view.setLayer(Some(&layer.clone().into_super()));
Self { drawable: None, layer, queue, device }
};
#[cfg(target_os = "ios")]
let presenter = {
let RawWindowHandle::UiKit(handle) = window.window_handle().expect("no window handle").as_raw() else {
panic!("not a UIKit window");
};
let view = unsafe { Retained::retain(handle.ui_view.as_ptr() as *mut UIView) }.expect("no view");
view.layer().addSublayer(&layer);
if background.a() > 0 {
let f = |c: u8| c as f64 / 255.0;
let color = objc2_ui_kit::UIColor::colorWithRed_green_blue_alpha(
f(background.r()),
f(background.g()),
f(background.b()),
f(background.a()),
);
view.setBackgroundColor(Some(&color));
if let Some(window) = view.window() {
window.setBackgroundColor(Some(&color));
}
}
Self { drawable: None, layer, queue, device, view }
};
#[cfg(target_os = "ios")]
let size = window.outer_size();
#[cfg(not(target_os = "ios"))]
let size = window.inner_size();
presenter.resize(&window, size.width.max(1), size.height.max(1));
(window, presenter)
}
fn handles(&self) -> (mtl::Handle, mtl::Handle) {
(Retained::as_ptr(&self.device) as mtl::Handle, Retained::as_ptr(&self.queue) as mtl::Handle)
}
pub(super) fn gpu(&self) -> Gpu {
let (device, queue) = self.handles();
unsafe { Gpu::new_metal(device, queue) }.expect("no GPU context")
}
pub(super) fn gpu_after(&mut self, _window: &Window, lost: &mut Gpu) -> Gpu {
let (device, queue) = self.handles();
unsafe { Gpu::new_metal_after(device, queue, lost) }.expect("no GPU context")
}
pub(super) fn begin<'a>(
&mut self,
gpu: &mut Gpu,
surface: &'a mut Option<skia::Surface>,
_width: u32,
_height: u32,
) -> Option<&'a mut skia::Surface> {
let drawable = self.layer.nextDrawable()?;
let size = self.layer.drawableSize();
let texture = drawable.texture();
*surface = unsafe { gpu.drawable_surface(Retained::as_ptr(&texture) as mtl::Handle, size.width as i32, size.height as i32) };
self.drawable = Some(drawable);
surface.as_mut()
}
pub(super) fn end_frame(&mut self, gpu: &mut Gpu, surface: &mut Option<skia::Surface>) {
if let Some(surface) = surface {
gpu.end_frame(surface);
}
}
pub(super) fn present(&mut self, surface: &mut Option<skia::Surface>) {
*surface = None;
let Some(drawable) = self.drawable.take() else { return };
autoreleasepool(|_| {
let Some(commands) = self.queue.commandBuffer() else { return };
let drawable: Retained<ProtocolObject<dyn MTLDrawable>> = (&drawable).into();
commands.presentDrawable(&drawable);
commands.commit();
});
}
pub(super) fn resize(&self, window: &Window, width: u32, height: u32) {
#[cfg(target_os = "ios")]
self.layer.setFrame(self.view.bounds());
self.layer.setContentsScale(window.scale_factor());
self.layer.setDrawableSize(CGSize::new(width as f64, height as f64));
}
}
}
#[cfg(any(target_os = "ios", target_os = "android"))]
mod ios {
use winit::{
application::ApplicationHandler,
event::{StartCause, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::{WindowAttributes, WindowId},
};
use super::{Arrived, Desktop, Presenter};
use crate::App;
pub(super) struct Launch {
pub attributes: Option<WindowAttributes>,
pub make: fn() -> Box<dyn App>,
pub proxy: EventLoopProxy<Arrived>,
pub desktop: Option<Desktop>,
}
impl ApplicationHandler<Arrived> for Launch {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
match (&mut self.desktop, self.attributes.take()) {
(Some(desktop), _) => desktop.resumed(event_loop),
(None, Some(attributes)) => {
let app = (self.make)();
#[cfg(target_os = "ios")]
let (window, presenter) = Presenter::new(event_loop.create_window(attributes).expect("no window"), app.background());
#[cfg(target_os = "android")]
let (window, presenter) = Presenter::new_active(event_loop, attributes, app.gpu_backend());
self.desktop = Some(Desktop::new(window, presenter, app, self.proxy.clone()));
if let Some(desktop) = &mut self.desktop {
desktop.resumed(event_loop);
}
}
(None, None) => {}
}
}
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
if let Some(desktop) = &mut self.desktop {
desktop.suspended(event_loop);
}
}
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
if let Some(desktop) = &mut self.desktop {
desktop.new_events(event_loop, cause);
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
if let Some(desktop) = &mut self.desktop {
desktop.about_to_wait(event_loop);
}
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, arrived: Arrived) {
if let Some(desktop) = &mut self.desktop {
desktop.user_event(event_loop, arrived);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
if let Some(desktop) = &mut self.desktop {
desktop.window_event(event_loop, id, event);
}
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
struct Pacing {
period: Duration,
on: Option<bool>,
intervals: Vec<f32>,
last_start: Option<Instant>,
slot: Option<Instant>,
due: Option<Instant>,
}
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
impl Pacing {
fn new(window: &Window) -> Self {
let millihertz = window.current_monitor().and_then(|monitor| monitor.refresh_rate_millihertz()).filter(|&mhz| mhz >= 20_000);
let period = Duration::from_secs_f64(1000.0 / millihertz.unwrap_or(60_000) as f64);
Self { period, on: None, intervals: Vec::with_capacity(60), last_start: None, slot: None, due: None }
}
fn frame(&mut self, start: Instant) -> Option<Instant> {
let previous = self.last_start.replace(start);
if self.on == Some(true) {
let next = self.slot.map(|slot| slot + self.period);
let slot = match next {
Some(next) if start >= next && start - next < self.period => next,
_ => start,
};
self.slot = Some(slot);
return Some(slot);
}
if self.on.is_some() {
return None;
}
let interval = (start - previous?).as_secs_f32() * 1000.0;
if interval > 100.0 {
self.intervals.clear();
return None;
}
self.intervals.push(interval);
if self.intervals.len() == 60 {
self.intervals.sort_by(f32::total_cmp);
let (median, period_ms) = (self.intervals[30], self.period.as_secs_f32() * 1000.0);
let ignored = median < period_ms / 2.0;
if ignored {
eprintln!("drawnui: the driver does not wait for vsync (frames {median:.1} ms apart): frames paced at {:.0} Hz", 1000.0 / period_ms);
}
self.on = Some(ignored);
}
None
}
fn hold(&self, now: Instant) -> Option<Instant> {
if self.on != Some(true) {
return None;
}
let at = self.slot.or(self.last_start)? + self.period;
(at > now).then_some(at)
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
mod ticker {
use std::cell::Cell;
use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, define_class, msg_send, rc::Retained, sel};
#[cfg(target_os = "macos")]
use objc2_app_kit::NSView;
#[cfg(target_os = "macos")]
use objc2_foundation::NSObjectProtocol;
use objc2_foundation::{NSObject, NSRunLoop, NSRunLoopCommonModes};
use objc2_quartz_core::{CADisplayLink, CAFrameRateRange};
#[cfg(target_os = "ios")]
use objc2_ui_kit::UIScreen;
use winit::{event_loop::EventLoopProxy, window::Window};
use super::Arrived;
pub(crate) struct Target {
proxy: EventLoopProxy<Arrived>,
}
define_class!(
#[unsafe(super(NSObject))]
#[thread_kind = MainThreadOnly]
#[name = "DrawnUiTicker"]
#[ivars = Target]
pub(crate) struct TickTarget;
impl TickTarget {
#[unsafe(method(tick:))]
fn tick(&self, _link: &CADisplayLink) {
let _ = self.ivars().proxy.send_event(Arrived::Tick);
}
}
);
pub(crate) struct Ticker {
link: Retained<CADisplayLink>,
_target: Retained<TickTarget>,
#[cfg(target_os = "macos")]
view: Retained<NSView>,
#[cfg(target_os = "ios")]
display_fps: u32,
applied: Cell<u32>,
}
impl Ticker {
pub fn new(proxy: EventLoopProxy<Arrived>, window: &Window) -> Option<Self> {
let mtm = MainThreadMarker::new().expect("not on the main thread");
let target = mtm.alloc::<TickTarget>().set_ivars(Target { proxy });
let target: Retained<TickTarget> = unsafe { msg_send![super(target), init] };
#[cfg(target_os = "ios")]
let _ = window;
#[cfg(target_os = "ios")]
let link = unsafe { CADisplayLink::displayLinkWithTarget_selector(&target, sel!(tick:)) };
#[cfg(target_os = "macos")]
let (link, view) = {
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
let RawWindowHandle::AppKit(handle) = window.window_handle().ok()?.as_raw() else { return None };
let view = unsafe { Retained::retain(handle.ns_view.as_ptr() as *mut NSView) }?;
if !view.respondsToSelector(sel!(displayLinkWithTarget:selector:)) {
return None;
}
(unsafe { view.displayLinkWithTarget_selector(&target, sel!(tick:)) }, view)
};
link.setPaused(true);
unsafe { link.addToRunLoop_forMode(&NSRunLoop::mainRunLoop(), NSRunLoopCommonModes) };
Some(Self {
link,
_target: target,
#[cfg(target_os = "macos")]
view,
#[cfg(target_os = "ios")]
display_fps: {
#[allow(deprecated)]
let fps = UIScreen::mainScreen(mtm).maximumFramesPerSecond();
fps.max(30) as u32
},
applied: Cell::new(0),
})
}
fn display_fps(&self) -> u32 {
#[cfg(target_os = "ios")]
return self.display_fps;
#[cfg(target_os = "macos")]
self.view.window().and_then(|w| w.screen()).map_or(60, |s| s.maximumFramesPerSecond().max(30) as u32)
}
pub fn run(&self, on: bool) {
if on {
let display = self.display_fps();
let fps = match crate::max_fps() {
0 => display,
cap if cap >= display => display,
cap => display / display.div_ceil(cap),
};
if fps != self.applied.get() {
self.applied.set(fps);
let fps = fps as f32;
self.link.setPreferredFrameRateRange(CAFrameRateRange { minimum: fps, maximum: fps, preferred: fps });
}
}
if self.link.isPaused() == on {
self.link.setPaused(!on);
}
}
}
impl Drop for Ticker {
fn drop(&mut self) {
self.link.invalidate();
}
}
}
#[derive(Default)]
struct Stats {
cpu_ms: Vec<f32>,
interval_ms: Vec<f32>,
step_ms: Vec<f32>,
image_ms: Vec<f32>,
last_start: Option<Instant>,
last_time_ms: f64,
}
impl Stats {
fn frame(&mut self, start: Instant, time_ms: f64) {
self.cpu_ms.push(start.elapsed().as_secs_f32() * 1000.0);
if let Some(last) = self.last_start.replace(start) {
let interval = (start - last).as_secs_f32() * 1000.0;
self.interval_ms.push(interval);
if interval < 100.0 {
self.step_ms.push((time_ms - self.last_time_ms) as f32);
}
}
self.last_time_ms = time_ms;
if self.cpu_ms.len() == 300 {
let (cpu, interval) = (summary(&mut self.cpu_ms), summary(&mut self.interval_ms));
let mut line = format!("frames 300 | cpu ms {cpu} | interval ms {interval}");
if self.step_ms.len() > 10 {
let steps = &mut self.step_ms;
steps.sort_by(f32::total_cmp);
let at = |p: f32| steps[((steps.len() - 1) as f32 * p) as usize];
line += &format!(" | step ms p5 {:.2} p50 {:.2} p95 {:.2} (spread {:.2})", at(0.05), at(0.5), at(0.95), at(0.95) - at(0.05));
}
if !self.image_ms.is_empty() {
line += &format!(" | {} images, ms each {}", self.image_ms.len(), summary(&mut self.image_ms));
}
#[cfg(target_os = "android")]
android::log(&line);
#[cfg(not(target_os = "android"))]
println!("{line}");
self.cpu_ms.clear();
self.interval_ms.clear();
self.step_ms.clear();
self.image_ms.clear();
}
}
}
fn summary(values: &mut [f32]) -> String {
values.sort_by(f32::total_cmp);
let at = |p: f32| values[((values.len() - 1) as f32 * p) as usize];
format!("p50 {:.2} p95 {:.2} p99 {:.2} max {:.2}", at(0.5), at(0.95), at(0.99), at(1.0))
}