From a0dd9776bb14a39108915a38a5df6ee34b2c9c30 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Mon, 28 Oct 2024 12:14:44 -0700 Subject: [PATCH] Experimental Android support for eframe --- Cargo.lock | 38 ++++++++++++++++ crates/eframe/src/epi.rs | 16 +++++++ crates/eframe/src/native/run.rs | 11 +++++ examples/hello_android/Cargo.toml | 32 +++++++++++++ examples/hello_android/README.md | 23 ++++++++++ examples/hello_android/screenshot.png | 3 ++ examples/hello_android/src/lib.rs | 65 +++++++++++++++++++++++++++ 7 files changed, 188 insertions(+) create mode 100644 examples/hello_android/Cargo.toml create mode 100644 examples/hello_android/README.md create mode 100644 examples/hello_android/screenshot.png create mode 100644 examples/hello_android/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 000165018c6..05f8f3fd163 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -1460,6 +1477,16 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -1989,6 +2016,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hello_android" +version = "0.1.0" +dependencies = [ + "android_logger", + "eframe", + "egui_extras", + "log", + "winit", +] + [[package]] name = "hello_world" version = "0.1.0" diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index f567507c4da..f03128fdac5 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -364,6 +364,16 @@ pub struct NativeOptions { /// /// Defaults to true. pub dithering: bool, + + /// Android application for `winit`'s event loop. + /// + /// This value is required on Android to correctly create the event loop. See + /// [`EventLoopBuilder::build`] and [`with_android_app`] for details. + /// + /// [`EventLoopBuilder::build`]: winit::event_loop::EventLoopBuilder::build + /// [`with_android_app`]: winit::platform::android::EventLoopBuilderExtAndroid::with_android_app + #[cfg(target_os = "android")] + pub android_app: Option, } #[cfg(not(target_arch = "wasm32"))] @@ -383,6 +393,9 @@ impl Clone for NativeOptions { persistence_path: self.persistence_path.clone(), + #[cfg(target_os = "android")] + android_app: self.android_app.clone(), + ..*self } } @@ -424,6 +437,9 @@ impl Default for NativeOptions { persistence_path: None, dithering: true, + + #[cfg(target_os = "android")] + android_app: None, } } } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 43dd07ee0da..219edd56625 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -17,9 +17,20 @@ use crate::{ // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { + #[cfg(target_os = "android")] + use winit::platform::android::EventLoopBuilderExtAndroid as _; + crate::profile_function!(); let mut builder = winit::event_loop::EventLoop::with_user_event(); + #[cfg(target_os = "android")] + let mut builder = + builder.with_android_app(native_options.android_app.take().ok_or_else(|| { + crate::Error::AppCreation(Box::from( + "`NativeOptions` is missing required `android_app`", + )) + })?); + if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { hook(&mut builder); } diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml new file mode 100644 index 00000000000..dcb0a5a5c01 --- /dev/null +++ b/examples/hello_android/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "hello_android" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +# `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error. +# [lints] +# workspace = true + +[lib] +crate-type = ["cdylib"] + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "android-native-activity", +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +log = { workspace = true } +winit = { workspace = true } +android_logger = "0.14" + +[package.metadata.android] +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android" ] diff --git a/examples/hello_android/README.md b/examples/hello_android/README.md new file mode 100644 index 00000000000..5f5cfa7c98e --- /dev/null +++ b/examples/hello_android/README.md @@ -0,0 +1,23 @@ +Hello world example for Android. + +Use `cargo-apk` to build and run. Requires a patch to workaround [an upstream bug](https://github.com/rust-mobile/cargo-subcommand/issues/29). + +One-time setup: + +```sh +rustup target add aarch64-linux-android +rustup target add armv7-linux-androideabi + +cargo install \ + --git https://github.com/parasyte/cargo-apk.git \ + --rev 282639508eeed7d73f2e1eaeea042da2716436d5 \ + cargo-apk +``` + +Build and run: + +```sh +cargo apk run -p hello_android +``` + +![](screenshot.png) diff --git a/examples/hello_android/screenshot.png b/examples/hello_android/screenshot.png new file mode 100644 index 00000000000..91179fa2f41 --- /dev/null +++ b/examples/hello_android/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7add91d7d6b73f48e98f20d84cba3bd3a950cf97aa31f5e9fa93da9af98e876c +size 120019 diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs new file mode 100644 index 00000000000..adda66ca5f7 --- /dev/null +++ b/examples/hello_android/src/lib.rs @@ -0,0 +1,65 @@ +#![cfg(target_os = "android")] +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use android_logger::Config; +use eframe::egui; +use log::LevelFilter; +use winit::platform::android::activity::AndroidApp; + +#[no_mangle] +fn android_main(app: AndroidApp) { + // Log to android output + android_logger::init_once(Config::default().with_max_level(LevelFilter::Info)); + + let options = eframe::NativeOptions { + android_app: Some(app), + ..Default::default() + }; + eframe::run_native( + "My egui App", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ) + .unwrap() +} + +struct MyApp { + name: String, + age: u32, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("My egui Application"); + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + + ui.image(egui::include_image!( + "../../../crates/egui/assets/ferris.png" + )); + }); + } +}