menu-ui 0.2.0

Android egui 内置菜单框架:EGL/GL 渲染、JNI 输入桥接、原生文本编辑框与图片选择
use std::ffi::c_void;
use std::sync::atomic::Ordering;

use jni::errors::{Error as JniError, Result as JniResult};
use jni::objects::{JClass, JObject, JString};
use jni::sys::{jboolean, jfloat, jint, jlong};
use jni::NativeMethod;
use jni::EnvUnowned;
use ndk::native_window::NativeWindow;

use crate::{bridge_class_name, jni_static};
use crate::renderer::render_loop;
use crate::state::{TouchAction, TouchEvent, app_state, init_app_state};

/// 构建 native 方法描述表:方法名、JNI 类型签名、对应的 Rust 函数指针。
///
/// 每个函数指针都必须与签名严格匹配(静态方法形态):
/// `fn(EnvUnowned, JClass, 参数...) -> 返回值`。
pub fn build_native_methods() -> Vec<NativeMethod<'static>> {
    vec![
        unsafe {
            NativeMethod::from_raw_parts(
                jni_static("nativeAttach"),
                // (Activity, Surface, int width, int height, float density) -> boolean
                jni_static("(Landroid/app/Activity;Landroid/view/Surface;IIF)Z"),
                native_attach as *mut c_void,
            )
        },
        unsafe {
            NativeMethod::from_raw_parts(
                jni_static("nativeOnTouch"),
                // (float x, float y, int action) -> boolean
                jni_static("(FFI)Z"),
                native_on_touch as *mut c_void,
            )
        },
        unsafe {
            NativeMethod::from_raw_parts(
                jni_static("nativeOnTextEditResult"),
                // (long requestId, String text) -> void
                jni_static("(JLjava/lang/String;)V"),
                native_on_text_edit_result as *mut c_void,
            )
        },
        unsafe {
            NativeMethod::from_raw_parts(
                jni_static("nativeOnTextEditCancelled"),
                // () -> void
                jni_static("()V"),
                native_on_text_edit_cancelled as *mut c_void,
            )
        },
        unsafe {
            NativeMethod::from_raw_parts(
                jni_static("nativeOnImagePicked"),
                // (long requestId, String savedPath) -> void(savedPath 为 null 表示取消)
                jni_static("(JLjava/lang/String;)V"),
                native_on_image_picked as *mut c_void,
            )
        },
        unsafe {
            NativeMethod::from_raw_parts(
                jni_static("nativeStop"),
                // () -> void
                jni_static("()V"),
                native_stop as *mut c_void,
            )
        },
    ]
}

// ---------------------------------------------------------------------------
// JNI 入口函数(Kotlin 侧对应 `EguiBridge` 的外部方法)
// ---------------------------------------------------------------------------

/// `nativeAttach(activity, surface, width, height, density)`
///
/// 由 `SurfaceHolder.Callback.surfaceChanged` 与 `onActivityResumed` 调用:
/// 1. 缓存 JavaVM 与 `EguiBridge` 类的全局引用(供渲染线程回调 Kotlin);
/// 2. 由 Android `Surface` 创建原生窗口(`ANativeWindow`);
/// 3. **非阻塞**地把渲染线程切到新 Surface:已有存活线程 → 只置「重开会话」
///    请求(渲染线程在会话内自行收尾并用最新 window 重开);线程不存在 →
///    新建。**绝不 join**(见下)。
///
/// # 为什么不能在这里 join 旧渲染线程(实测 ANR 13 秒)
///
/// 退后台期间(选图器/系统 UI 覆盖)`SurfaceView` 不可见,SurfaceFlinger
/// 不再回收缓冲 ⇒ 渲染线程阻塞在 `eglSwapBuffers`。它要等窗口**重新可见**
/// 才能返回,而窗口重新可见的前提正是主线程跑完 `onResume` —— 若在这里
/// join,主线程与渲染线程互等死锁(ANR 栈:
/// `onActivityResumed → nativeAttach → pthread_join`)。
pub extern "system" fn native_attach<'caller>(
    mut unowned: EnvUnowned<'caller>,
    _class: JClass<'caller>,
    _activity: JObject<'caller>,
    surface: JObject<'caller>,
    width: jint,
    height: jint,
    density: jfloat,
) -> jboolean {
    unowned
        .with_env(|env| -> JniResult<jboolean> {
            let java_vm = env.get_java_vm()?.clone();
            // 在 UI 线程(类加载器上下文正确)缓存类全局引用。
            let class = env.find_class(jni::strings::JNIString::from(bridge_class_name()))?;
            let class = env.new_global_ref(class)?;
            // 由 Java Surface 创建 ANativeWindow(ndk 0.9 的 NativeWindow 拥有释放语义)。
            let window = unsafe {
                let raw_env: *mut jni_sys_03::JNIEnv =
                    env.get_raw() as *mut jni_sys_03::JNIEnv;
                NativeWindow::from_surface(raw_env, surface.as_raw())
                    .ok_or_else(|| JniError::NullPtr("ANativeWindow_fromSurface"))?
            };

            let state = init_app_state();

            // 写入新 Surface 的信息并清空旧事件队列。
            {
                *state.java_vm.lock().unwrap() = Some(java_vm);
                *state.egui_class.lock().unwrap() = Some(class);
                *state.density.lock().unwrap() = density;
                *state.size_px.lock().unwrap() = (width.max(1) as u32, height.max(1) as u32);
                *state.window.lock().unwrap() = Some(window);
                state.touches.lock().unwrap().clear();
                state.extra_events.lock().unwrap().clear();
                // 复位原生编辑框桥接状态:退后台时若编辑框还开着,
                // dialog_open 会残留为 true,回前台后所有触摸都被它吞掉。
                crate::state::TEXT_BRIDGE.lock().unwrap().dialog_open = false;
                // 同理复位选图桥接状态(退后台时选择器可能还开着)。
                crate::state::IMAGE_PICKER.lock().unwrap().open = false;
            }

            // 非阻塞地把渲染线程切到新 Surface:
            // - 已有存活线程 → 只请求「重开会话」(它在会话内自行结束,回到
            //   循环顶部用最新 window 重开);
            // - 线程已退出/不存在 → 新建。
            // 持有 `render_thread` 锁完成「判断 + 置位」,与渲染线程退出路径
            // (同一把锁下复查 `running`)互斥,避免两者同时认为「对方还在」。
            {
                let mut guard = state.render_thread.lock().unwrap();
                if guard.as_ref().is_some_and(|h| h.is_finished()) {
                    guard.take(); // 旧线程已退出:清掉句柄,下面新建
                }
                state.running.store(true, Ordering::SeqCst);
                match guard.as_ref() {
                    Some(_) => state.request_rebind(),
                    None => {
                        let thread_state = state.clone();
                        *guard = Some(std::thread::spawn(move || render_loop(thread_state)));
                    }
                }
            }
            Ok(jni::sys::JNI_TRUE)
        })
        .resolve::<jni::errors::LogErrorAndDefault>()
}

/// `nativeOnTouch(x, y, action) -> boolean`
///
/// 由 `TouchRouter.dispatchTouchEvent` 在每次手势开始时调用,决定本次触摸
/// 是“egui 消费”还是“透传给兄弟控件”。判定完全基于 egui 自身的输入状态,
/// 不使用手写几何矩形:
///
/// - `egui_wants_pointer_input()`:egui 正在使用指针(拖动等)或指针悬停于 egui 区域;
/// - `egui_wants_keyboard_input()`:egui 有控件持有键盘焦点;
/// - `layer_id_at(pos)`:egui 自身 layer 系统对按下坐标的位置判定(即
///   `is_pointer_over_egui()` 的内部逻辑,但传入真实按下坐标,避免 egui
///   记录的指针位置滞后一帧)。
///
/// 返回 true:事件被消费(兄弟控件收不到);返回 false:事件继续派发。
pub extern "system" fn native_on_touch<'caller>(
    mut unowned: EnvUnowned<'caller>,
    _class: JClass<'caller>,
    x_px: jfloat,
    y_px: jfloat,
    action: jint,
) -> jboolean {
    unowned
        .with_env(|_env| -> JniResult<jboolean> {
            let state = app_state();
            let density = *state.density.lock().unwrap();
            if density <= 0.0 {
                return Ok(jni::sys::JNI_FALSE);
            }
            let touch_action = match action {
                0 => TouchAction::Down,
                1 => TouchAction::Up,
                2 => TouchAction::Move,
                _ => return Ok(jni::sys::JNI_FALSE),
            };

            // px 转 egui point。
            let pos = egui::pos2(x_px / density, y_px / density);

            // 原生编辑框打开期间:消费所有触摸(编辑框本身在最上层),
            // 避免触发 SurfaceView 后面的兄弟控件。
            if crate::state::TEXT_BRIDGE.lock().unwrap().dialog_open {
                return Ok(jni::sys::JNI_TRUE);
            }

            let consume = if touch_action == TouchAction::Down {
                // 只有 DOWN 需要做位置判定;MOVE/UP 跟随手势。
                match state.egui_ctx.lock().unwrap().clone() {
                    Some(ctx) => {
                        let wants_pointer = ctx.egui_wants_pointer_input();
                        let wants_keyboard = ctx.egui_wants_keyboard_input();
                        let over_egui = ctx
                            .layer_id_at(pos)
                            .is_some_and(|layer| layer.order != egui::Order::Background);
                        let (_px, _py) = (pos.x, pos.y);
                        wants_pointer || wants_keyboard || over_egui
                    }
                    // 渲染上下文尚未就绪:不消费,避免误吞触摸。
                    None => false,
                }
            } else {
                true
            };
            if !consume {
                return Ok(jni::sys::JNI_FALSE);
            }

            // 投递给渲染线程(持锁写入并唤醒空闲中的渲染线程)。
            state.push_touch(TouchEvent {
                x: pos.x,
                y: pos.y,
                action: touch_action,
            });
            Ok(jni::sys::JNI_TRUE)
        })
        .resolve::<jni::errors::LogErrorAndDefault>()
}

/// `nativeOnTextEditResult(requestId, text)`
///
/// Kotlin 侧点击原生编辑框的“完成”按钮后回调:把新文本放入全局结果队列,
/// 渲染线程下一帧会应用到对应的 egui 文本控件,并关闭“对话框已打开”标记。
pub extern "system" fn native_on_text_edit_result<'caller>(
    mut unowned: EnvUnowned<'caller>,
    _class: JClass<'caller>,
    request_id: jlong,
    text: JString<'caller>,
) {
    unowned
        .with_env(|env| -> JniResult<()> {
            let new_text = text.mutf8_chars(env)?.to_str().into_owned();
            // 持锁写入结果并唤醒渲染线程(内部同时复位 dialog_open)。
            app_state().push_text_result(request_id as u64, new_text);
            Ok(())
        })
        .resolve::<jni::errors::LogErrorAndDefault>()
}

/// `nativeOnTextEditCancelled()`
///
/// Kotlin 侧点击“取消”或按返回键关闭编辑框后回调:只清除“对话框已打开”
/// 标记,不写入任何结果(egui 文本保持不变)。
pub extern "system" fn native_on_text_edit_cancelled<'caller>(
    mut unowned: EnvUnowned<'caller>,
    _class: JClass<'caller>,
) {
    unowned
        .with_env(|_env| -> JniResult<()> {
            crate::state::TEXT_BRIDGE.lock().unwrap().dialog_open = false;
            Ok(())
        })
        .resolve::<jni::errors::LogErrorAndDefault>()
}

/// `nativeOnImagePicked(requestId, savedPath)`
///
/// Kotlin 侧 `ImagePickerActivity` 在系统文件选择器返回后调用:
/// 把「拷贝到本地后的绝对路径」放入全局结果队列;`savedPath` 为 null
/// (用户取消 / 拷贝失败)时写入 `None`。渲染线程下一帧取回结果。
pub extern "system" fn native_on_image_picked<'caller>(
    mut unowned: EnvUnowned<'caller>,
    _class: JClass<'caller>,
    request_id: jlong,
    saved_path: JString<'caller>,
) {
    unowned
        .with_env(|env| -> JniResult<()> {
            let path = if saved_path.is_null() {
                None
            } else {
                Some(saved_path.mutf8_chars(env)?.to_str().into_owned())
            };
            app_state().push_image_result(request_id as u64, path);
            Ok(())
        })
        .resolve::<jni::errors::LogErrorAndDefault>()
}

/// `nativeStop()`
///
/// Surface 销毁或 Activity 销毁时调用:停止渲染线程并释放原生窗口。
pub extern "system" fn native_stop<'caller>(
    mut unowned: EnvUnowned<'caller>,
    _class: JClass<'caller>,
) {
    unowned
        .with_env(|_env| -> JniResult<()> {
            let state = app_state();
            // 非阻塞:只置停止标志 + 唤醒 + 丢弃窗口。渲染线程会在会话收尾后
            // 自行退出并清除自己的句柄;**不 join**(理由同 `native_attach`:
            // 可能与阻塞在 eglSwapBuffers 的渲染线程互等死锁)。
            state.request_stop();
            *state.window.lock().unwrap() = None;
            Ok(())
        })
        .resolve::<jni::errors::LogErrorAndDefault>()
}