menu-ui 0.2.0

Android egui 内置菜单框架:EGL/GL 渲染、JNI 输入桥接、原生文本编辑框与图片选择
//! 渲染线程向 Kotlin 发起回调的模块。
//!
//! 目前只有一个用途:egui 文本编辑控件被点击后,通知 Kotlin 在 Android UI
//! 线程弹出原生文本编辑框。调用链:
//!
//! ```text
//! [渲染线程] maybe_request_native_editor
//!     -> attach_current_thread(若线程尚未挂载到 JVM,先挂载)
//!     -> 用缓存的 EguiBridge 类全局引用调用静态方法 showNativeTextEditor
//!     -> Kotlin: activity.runOnUiThread { 弹出 AlertDialog }
//! ```
//!
//! 注意:这里不能使用 `Env::find_class` 查找应用类——Android 的原生线程
//! 上 `FindClass` 找不到应用类,因此类引用必须在 `nativeAttach`(UI 线程)
//! 中提前获取并缓存为全局引用(见 [`crate::state::AppState::egui_class`])。

use std::sync::Arc;

use jni::errors::Result as JniResult;
use jni::objects::JString;
use jni::signature::{MethodSignature, RuntimeMethodSignature};
use jni::sys::jlong;

use crate::state::AppState;
use crate::jni_static;

/// 请求 Kotlin 启动系统文件选择器(`ACTION_OPEN_DOCUMENT`)。
///
/// 由宿主 UI(渲染线程,`build_ui` 内)在图片被点击时调用:
///
/// 1. 若已有选择器打开则直接返回 `false`(避免重复弹窗);
/// 2. 挂载当前线程到 JVM(渲染线程尚未挂载)并调用
///    `EguiBridge.pickImage(requestId, destPath, title)`;
/// 3. Kotlin 侧用透明代理 Activity 启动系统选图,选中后把文件拷贝到
///    `destPath`,再经 `nativeOnImagePicked` 把结果写回队列。
///
/// `dest_path` 必须是**绝对路径**(Kotlin 直接按它写入文件)。
/// 返回值只表示“请求是否已发出”,不代表用户选择结果。
pub(crate) fn request_image_picker(state: &Arc<AppState>, request_id: u64, dest_path: &str, title: &str) -> bool {
    // 已有选择器打开:忽略新请求。
    {
        let picker = crate::state::IMAGE_PICKER.lock().unwrap();
        if picker.open {
            return false;
        }
    }
    let vm = state.java_vm.lock().unwrap().clone();
    let class_guard = state.egui_class.lock().unwrap();
    let (Some(vm), Some(class)) = (vm, class_guard.as_ref()) else {
        log::error!("JVM/class not initialized, cannot pick image");
        return false;
    };

    let result = vm.attach_current_thread(|env| -> JniResult<()> {
        // 目标路径 / 标题都是普通文本:按 JNI Modified UTF-8 转字符串。
        let jdest = JString::from_str(env, dest_path)?;
        let jtitle = JString::from_str(env, title)?;
        let args = [
            jni::objects::JValue::Long(request_id as jlong),
            jni::objects::JValue::Object(&*jdest),
            jni::objects::JValue::Object(&*jtitle),
        ];
        // EguiBridge.pickImage(long requestId, String destPath, String title)
        let runtime_sig = RuntimeMethodSignature::from_str(
            "(JLjava/lang/String;Ljava/lang/String;)V",
        )?;
        let sig = MethodSignature::from(&runtime_sig);
        env.call_static_method(class, jni_static("pickImage"), sig, &args)?;
        Ok(())
    });
    match result {
        Ok(()) => {
            crate::state::IMAGE_PICKER.lock().unwrap().open = true;
            true
        }
        Err(e) => {
            log::error!("failed to call Kotlin pickImage: {e}");
            false
        }
    }
}

/// 如果本帧有文本控件被点击且当前没有打开的对话框,则请求 Kotlin 弹出
/// Android 原生文本编辑框,并把 `dialog_open` 置为 true。
///
/// 弹出成功后会向 egui 注入一个 `Escape` 按键事件,清除文本框的键盘焦点。
/// 如果不清除,`egui_wants_keyboard_input()` 会一直返回 true,导致后续所有
/// 触摸都被当作“egui 要消费”,透明区域就无法再透传给兄弟控件。
pub(crate) fn maybe_request_native_editor(state: &Arc<AppState>) {
    // 从全局桥接状态取走待处理的请求(若对话框已打开则忽略新请求)。
    let request = {
        let mut bridge = crate::state::TEXT_BRIDGE.lock().unwrap();
        if bridge.dialog_open {
            return;
        }
        bridge.pending_request.take()
    };
    let Some(request) = request else {
        return;
    };

    // 渲染线程调用 Kotlin:需要 JavaVM + EguiBridge 类的全局引用。
    let vm = state.java_vm.lock().unwrap().clone();
    let class_guard = state.egui_class.lock().unwrap();
    if let (Some(vm), Some(class)) = (vm, class_guard.as_ref()) {
        let result = vm.attach_current_thread(|env| -> JniResult<()> {
            let jstr = JString::from_str(env, &request.initial_text)?;
            let jtitle = JString::from_str(env, &request.title)?;
            let jhint = JString::from_str(env, &request.hint)?;
            let args = [
                jni::objects::JValue::Long(request.widget_id as jlong),
                jni::objects::JValue::Object(&*jstr),
                jni::objects::JValue::Object(&*jtitle),
                jni::objects::JValue::Object(&*jhint),
                jni::objects::JValue::Int(request.input_type),
            ];
            // 调用 Kotlin 静态方法:
            // showNativeTextEditor(long requestId, String initialText, String title, String hint, int inputType)。
            // 签名用结构化类型传入(jni 0.22 的 call_static_method 不接受裸 `&str` 签名)。
            let runtime_sig = RuntimeMethodSignature::from_str(
                "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V",
            )?;
            let sig = MethodSignature::from(&runtime_sig);
            env.call_static_method(
                class,
                jni_static("showNativeTextEditor"),
                sig,
                &args,
            )?;
            Ok(())
        });
        if let Err(e) = result {
            log::error!("failed to call Kotlin showNativeTextEditor: {e}");
        } else {
            crate::state::TEXT_BRIDGE.lock().unwrap().dialog_open = true;
            // 注入 Escape 清除 egui 文本框焦点(见函数顶部说明)。
            state.push_extra_event(egui::Event::Key {
                key: egui::Key::Escape,
                physical_key: None,
                pressed: true,
                repeat: false,
                modifiers: egui::Modifiers::default(),
            });
        }
    } else {
        log::error!("JVM/class not initialized, cannot show native editor");
    }
}