mod kotlin;
mod native;
mod renderer;
mod state;
mod text;
pub use egui;
pub use jni;
pub use crate::text::{
parse_unity_rich_text, unity_rich_text, NativeNumber, NativeNumberInput, NativeTextEdit,
TextInputMode, UnityTextSegment,
};
use jni::signature::RuntimeMethodSignature;
use jni::strings::{JNIStr, JNIString};
use std::sync::Mutex;
use jni::errors::Result as JniResult;
use jni::objects::{JObject, JString};
use jni::JavaVM;
use crate::native::build_native_methods;
fn pkg() -> &'static str {
"com.wuyan.egui"
}
fn bridge_class_name() -> String {
format!("{}/{}", pkg(), "EguiBridge").replace('.', "/")
}
pub(crate) fn jni_static(s: &'static str) -> &'static JNIStr {
let c = std::ffi::CString::new(s).expect("JNI string contains NUL");
let leaked: &'static std::ffi::CStr = Box::leak(c.into_boxed_c_str());
unsafe { JNIStr::from_cstr_unchecked(leaked) }
}
pub type UiRenderFn = Box<dyn FnMut(&mut egui::Ui) + Send + 'static>;
pub(crate) static UI_RENDERER: Mutex<Option<UiRenderFn>> = Mutex::new(None);
pub fn set_ui_renderer(render_fn: UiRenderFn) {
*UI_RENDERER.lock().unwrap() = Some(render_fn);
}
pub fn init(vm: *mut jni::sys::JavaVM, build_ui: UiRenderFn) {
let result = unsafe { JavaVM::from_raw(vm) }.attach_current_thread(|env| -> JniResult<()> {
match fetch_external_files_dir(env) {
Ok(path) => {
*ROOT_PATH.lock().unwrap() = path;
}
Err(e) => log::warn!("获取外部存储目录失败(不影响启动) {e}"),
}
let class = env.find_class(jni::strings::JNIString::from(bridge_class_name()))?;
let methods = build_native_methods();
unsafe { env.register_native_methods(&class, &methods)? };
Ok(())
});
match result {
Ok(()) => {}
Err(e) => log::error!("menu-ui init failed: {e}"),
}
set_ui_renderer(build_ui);
}
static ROOT_PATH: Mutex<String> = Mutex::new(String::new());
pub fn root_path() -> String {
ROOT_PATH.lock().unwrap().clone()
}
pub fn request_image_pick(request_id: u64, dest_path: &str, title: &str) -> bool {
match crate::state::try_app_state() {
Some(state) => crate::kotlin::request_image_picker(&state, request_id, dest_path, title),
None => {
log::error!("request_image_pick: 渲染会话尚未初始化(nativeAttach 未调用)");
false
}
}
}
pub fn take_image_pick_result(request_id: u64) -> Option<Option<String>> {
let mut picker = crate::state::IMAGE_PICKER.lock().unwrap();
if let Some(pos) = picker
.results
.iter()
.position(|(id, _)| *id == request_id)
{
return picker.results.remove(pos).map(|(_, path)| path);
}
None
}
pub fn image_picker_open() -> bool {
crate::state::IMAGE_PICKER.lock().unwrap().open
}
fn fetch_external_files_dir(env: &mut jni::Env) -> JniResult<String> {
let activity_thread_class =
env.find_class(jni::strings::JNIString::from("android/app/ActivityThread"))?;
let activity_thread = env.call_static_method(
activity_thread_class,
JNIString::from("currentActivityThread"),
RuntimeMethodSignature::from_str("()Landroid/app/ActivityThread;")?.method_signature(),
&[],
)?
.into_object()?;
let application = env.call_method(
activity_thread,
JNIString::from("getApplication"),
RuntimeMethodSignature::from_str("()Landroid/app/Application;")?.method_signature(),
&[],
)?
.into_object()?;
if application.is_null() {
return Err(jni::errors::Error::NullPtr("Application is null"));
}
let file = env.call_method(
application,
JNIString::from("getExternalFilesDir"),
RuntimeMethodSignature::from_str("(Ljava/lang/String;)Ljava/io/File;")?.method_signature(),
&[(&JObject::null()).into()],
)?
.into_object()?;
if file.is_null() {
return Err(jni::errors::Error::NullPtr("getExternalFilesDir returned null"));
}
let path = env.call_method(
file,
JNIString::from("getAbsolutePath"),
RuntimeMethodSignature::from_str("()Ljava/lang/String;")?.method_signature(),
&[],
)?
.into_object()?;
Ok(JString::cast_local(env, path)?.to_string())
}