menu-ui 0.2.0

Android egui 内置菜单框架:EGL/GL 渲染、JNI 输入桥接、原生文本编辑框与图片选择
//! 全局共享状态模块。
//!
//! 应用运行在两类线程上,它们通过 [`AppState`] 交换数据:
//! - **Android UI 线程**:JNI 入口(`nativeAttach` / `nativeOnTouch` / `nativeStop`)
//!   在这里被调用,负责接收 Surface、投递触摸事件、接收原生编辑框结果;
//! - **Rust 渲染线程**:负责 EGL / egui 帧循环,每帧取走 UI 线程投递的
//!   触摸事件,并向 Kotlin 发起原生编辑框请求。
//!
//! 因此 [`AppState`] 的所有字段都是同步原语(`Mutex` / `AtomicBool`)。

use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, LazyLock, Mutex, OnceLock};
use std::thread::JoinHandle;
use std::time::Instant;

use jni::objects::Global;
use jni::objects::JClass;
use jni::JavaVM;
use ndk::native_window::NativeWindow;

/// 进程级全局“原生编辑框桥接”状态(唯一副本,任何线程可访问)。
///
/// - `native_text_edit`(渲染线程):点击时写入 `pending_request`、
///   从 `results` 消费本控件的结果;
/// - Kotlin 结果回调(`push_text_result`):写入 `results`、复位 `dialog_open`;
/// - `maybe_request_native_editor`(渲染线程):取走 `pending_request` 弹窗。
///
/// 所有访问都是短临界区(不再由渲染线程持锁跨整个 UI 帧)。
pub(crate) static TEXT_BRIDGE: LazyLock<Mutex<TextBridgeState>> =
    LazyLock::new(|| Mutex::new(TextBridgeState::default()));

/// 进程级全局「选图(系统文件选择器)」桥接状态(唯一副本,任何线程可访问)。
///
/// 与 [`TEXT_BRIDGE`] 同构,但流程更简单:
/// - 渲染线程(`build_ui` 内点击图片)直接发起请求 → Kotlin 启动
///   `ImagePickerActivity`(透明代理 Activity)→ 系统文件选择器;
/// - 选择完成后 Kotlin 把文件**拷贝**到调用方指定的目标路径,
///   再经 `nativeOnImagePicked` 回写 `(request_id, 目标路径)`;
/// - 渲染线程下一帧用 [`crate::take_image_pick_result`] 取回结果。
pub(crate) static IMAGE_PICKER: LazyLock<Mutex<ImagePickerState>> =
    LazyLock::new(|| Mutex::new(ImagePickerState::default()));

/// 进程级单例:在 `nativeAttach` 首次被调用时创建(`OnceLock` 保证线程安全且只创建一次)。
static APP: OnceLock<Arc<AppState>> = OnceLock::new();

/// 整个应用共享的状态,UI 线程与渲染线程持有同一个 `Arc<AppState>`。
pub(crate) struct AppState {
    /// 缓存的 `JavaVM`,渲染线程回调 Kotlin 时用它挂载到 JVM。
    pub(crate) java_vm: Mutex<Option<JavaVM>>,
    /// `EguiBridge` 类的全局引用(`Global` 引用跨线程有效)。
    ///
    /// 为什么缓存类而不是每次 `find_class`:Android 上从“原生线程”
    /// (渲染线程)调用 `FindClass` 找不到应用类,所以要在 `nativeAttach`
    /// (UI 线程的 JNI 调用,类加载器上下文正确)里拿到类并转成全局引用,
    /// 渲染线程直接用这个引用调用 Kotlin 静态方法。
    pub(crate) egui_class: Mutex<Option<Global<JClass<'static>>>>,
    /// Android 屏幕密度(px 与 egui point 的换算系数,由 Kotlin 传入)。
    pub(crate) density: Mutex<f32>,
    /// Surface 的物理像素尺寸(宽 x 高),由 `surfaceChanged` 传入。
    pub(crate) size_px: Mutex<(u32, u32)>,
    /// Android `Surface` 对应的原生窗口句柄;渲染线程每帧克隆它来创建 EGL 表面。
    pub(crate) window: Mutex<Option<NativeWindow>>,
    /// 渲染线程是否应该继续运行(`false` 表示 Surface 已销毁或应用退出)。
    pub(crate) running: AtomicBool,
    /// 「当前会话需要重建」请求(Surface 被替换 / 回到前台主动重连)。
    ///
    /// 与 `running` 的区别:`running=false` 表示**整个渲染线程该退出**;
    /// 本标志只表示**这一个会话作废**——渲染线程在会话内检测到它就把当前会话
    /// 正常收尾(释放 EGL),然后在循环顶部用**最新的** `window` 重开会话。
    /// 这样 UI 线程(`nativeAttach`)永远不需要 join 渲染线程。
    pub(crate) rebind: AtomicBool,
    /// UI 线程投递的触摸事件队列(egui point 坐标),渲染线程每帧取空。
    pub(crate) touches: Mutex<VecDeque<TouchEvent>>,
    /// 共享的 egui 上下文:渲染线程每帧 `run_ui`,UI 线程用它的
    /// `egui_wants_pointer_input()` / `egui_wants_keyboard_input()` 与
    /// `layer_id_at()` 做触摸判定(决定“egui 消费”还是“透传给兄弟控件”)。
    pub(crate) egui_ctx: Mutex<Option<egui::Context>>,
    /// 额外合成事件队列(目前用于原生编辑框打开后给 egui 发一个 `Escape`
    /// 清除文本框焦点,避免 `egui_wants_keyboard_input()` 一直为真)。
    pub(crate) extra_events: Mutex<Vec<egui::Event>>,
    /// 上一渲染会话结束时保存的 egui 内存(窗口位置/大小/折叠状态等)。
    ///
    /// 会话开始时恢复进新的 `egui::Context`,保证退后台再回前台后
    /// 悬浮窗停留在离开时的位置,而不是回到 `default_pos`。
    ///
    /// 注意:恢复必须在 `set_fonts` **之后**调用——`egui::Memory` 里保存的
    /// `new_font_definitions` 会随整体恢复被清除,因此先恢复内存再设置字体
    /// 的顺序会让新会话退化为默认字体(中文不渲染)。
    pub(crate) egui_memory: Mutex<Option<egui::Memory>>,
    /// 渲染线程句柄,Surface 重建/销毁时需要 join 旧线程。
    pub(crate) render_thread: Mutex<Option<JoinHandle<()>>>,
    /// 渲染线程空闲时挂起等待的条件变量。
    pub(crate) wake: Condvar,
    /// 与 [`Self::wake`] 配套的互斥锁。
    pub(crate) wake_lock: Mutex<()>,
    /// egui 请求的“定时重绘”截止时间,由重绘回调写入、渲染线程消费。
    pub(crate) repaint_deadline: Mutex<Option<Instant>>,
}

/// “egui 文本编辑框 -> Android 原生编辑框”的桥接状态机。
#[derive(Default)]
pub(crate) struct TextBridgeState {
    /// 待弹出原生编辑框的请求;由渲染线程在文本控件被点击的下一帧消费。
    pub(crate) pending_request: Option<TextEditRequest>,
    /// 原生编辑框当前是否打开。打开期间:
    /// - 触摸不再投递给 egui(原生编辑框在最上层);
    /// - 渲染线程不会发起新的弹出请求。
    pub(crate) dialog_open: bool,
    /// 原生编辑框返回的结果队列:`(widget_id, 新文本)`。
    /// 点“完成”时 Kotlin 写入,下一帧由对应 id 的 `native_text_edit`
    /// 控件在 `build_ui` 里自行取回并回填。
    pub(crate) results: VecDeque<(u64, String)>,
}

/// “点击图片 -> Android 系统文件选择器”的桥接状态机。
#[derive(Default)]
pub(crate) struct ImagePickerState {
    /// 选择器当前是否打开。打开期间不重复发起请求;Surface 重建时复位。
    pub(crate) open: bool,
    /// 选择结果队列:`(request_id, 拷贝到本地的绝对路径)`;
    /// `None` 表示用户取消 / 拷贝失败。
    pub(crate) results: VecDeque<(u64, Option<String>)>,
}

/// 一次“弹出 Android 原生编辑框”请求的全部参数。
#[derive(Clone)]
pub(crate) struct TextEditRequest {
    /// 对应 egui 文本控件的 id(结果回填时按它分发)。
    pub(crate) widget_id: u64,
    /// 回显到原生编辑框的初始文本。
    pub(crate) initial_text: String,
    /// 原生编辑框对话框标题。
    pub(crate) title: String,
    /// 原生编辑框输入框的占位提示(hint)。
    pub(crate) hint: String,
    /// Android `InputType` 取值:任意文本 / 仅整数 / 小数。
    pub(crate) input_type: i32,
}

/// 一个触摸事件(坐标已由 px 换算成 egui point)。
#[derive(Clone, Copy)]
pub(crate) struct TouchEvent {
    /// egui 坐标(point)。
    pub(crate) x: f32,
    /// egui 坐标(point)。
    pub(crate) y: f32,
    /// 触摸动作(按下/抬起/移动)。
    pub(crate) action: TouchAction,
}

/// 触摸动作,与 Kotlin 侧 `MotionEvent` 的映射约定:
/// `0 = Down`、`1 = Up/Cancel`、`2 = Move`。
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum TouchAction {
    Down,
    Up,
    Move,
}

/// 获取进程级单例状态;必须在 `nativeAttach` 之后调用。
pub(crate) fn app_state() -> Arc<AppState> {
    APP.get()
        .expect("nativeAttach must be called before touching app state")
        .clone()
}

/// 非 panic 版本的 [`app_state`]:未初始化(尚未 `nativeAttach`)时返回 `None`。
///
/// 供「UI 之外可能被提前调用」的公开 API 使用(如选图请求),
/// 避免在渲染线程上 panic 直接终止整个渲染。
pub(crate) fn try_app_state() -> Option<Arc<AppState>> {
    APP.get().cloned()
}

/// 在 `nativeAttach` 中首次创建单例(`OnceLock::get_or_init`)。
pub(crate) fn init_app_state() -> Arc<AppState> {
    APP.get_or_init(|| Arc::new(AppState::new())).clone()
}

impl AppState {
    /// 创建全空的共享状态;各字段在 `nativeAttach` 中被逐一填充。
    fn new() -> Self {
        Self {
            java_vm: Mutex::new(None),
            egui_class: Mutex::new(None),
            density: Mutex::new(1.0),
            size_px: Mutex::new((0, 0)),
            window: Mutex::new(None),
            running: AtomicBool::new(false),
            rebind: AtomicBool::new(false),
            touches: Mutex::new(VecDeque::new()),
            egui_ctx: Mutex::new(None),
            extra_events: Mutex::new(Vec::new()),
            egui_memory: Mutex::new(None),
            render_thread: Mutex::new(None),
            wake: Condvar::new(),
            wake_lock: Mutex::new(()),
            repaint_deadline: Mutex::new(None),
        }
    }

    /// 唤醒空闲中的渲染线程(新输入、重绘请求、停止请求时调用)。
    pub(crate) fn wake_renderer(&self) {
        self.wake.notify_all();
    }

    /// 请求渲染线程停止:持锁修改 `running` 并唤醒等待中的线程。
    ///
    /// **非阻塞**:渲染线程会在自己退出会话后于循环顶部检查 `running` 并自行
    /// 结束(句柄也由它自己清除)。调用方(UI 线程)绝不能 join —— 退后台
    /// 期间渲染线程可能阻塞在 `eglSwapBuffers`(Surface 不可见时系统不回收
    /// 缓冲),它要等窗口重新可见才能返回,UI 线程 join 就会与它互等死锁。
    pub(crate) fn request_stop(&self) {
        let _guard = self.wake_lock.lock().unwrap();
        self.running.store(false, Ordering::SeqCst);
        self.wake.notify_all();
    }

    /// 请求「作废当前渲染会话、用最新 Surface 重开」(不阻塞调用方)。
    ///
    /// 持 `wake_lock` 置位并唤醒:渲染线程在**同一把锁**保护下做
    /// 「检查(含 rebind)+ 挂起」,不持锁会丢失唤醒(置位发生在检查之后、
    /// 挂起之前时,线程会一直睡到下一次无关输入)。
    pub(crate) fn request_rebind(&self) {
        let _guard = self.wake_lock.lock().unwrap();
        self.rebind.store(true, Ordering::SeqCst);
        self.wake.notify_all();
    }

    /// 取走「需要重开会话」请求(渲染线程调用,取走即清)。
    pub(crate) fn take_rebind(&self) -> bool {
        self.rebind.swap(false, Ordering::SeqCst)
    }

    /// 持锁写入一个触摸事件并唤醒渲染线程。
    ///
    /// 持 `wake_lock` 写入是 Condvar 正确性的关键:渲染线程在 `wake_lock`
    /// 保护下检查“是否有输入”,若写入与检查不加锁,可能丢失唤醒导致
    /// 渲染线程永远睡下去。
    pub(crate) fn push_touch(&self, event: TouchEvent) {
        let _guard = self.wake_lock.lock().unwrap();
        self.touches.lock().unwrap().push_back(event);
        self.wake.notify_one();
    }

    /// 持锁写入一个原生编辑框结果并唤醒渲染线程(同时复位“对话框已打开”)。
    pub(crate) fn push_text_result(&self, request_id: u64, text: String) {
        let _guard = self.wake_lock.lock().unwrap();
        let mut bridge = TEXT_BRIDGE.lock().unwrap();
        bridge.dialog_open = false;
        bridge.results.push_back((request_id, text));
        self.wake.notify_one();
    }

    /// 持锁写入一个选图结果并唤醒渲染线程(同时复位“选择器已打开”)。
    pub(crate) fn push_image_result(&self, request_id: u64, path: Option<String>) {
        let _guard = self.wake_lock.lock().unwrap();
        let mut picker = IMAGE_PICKER.lock().unwrap();
        picker.open = false;
        picker.results.push_back((request_id, path));
        self.wake.notify_one();
    }

    /// 持锁写入一个合成事件(如清除焦点的 Escape)并唤醒渲染线程。
    pub(crate) fn push_extra_event(&self, event: egui::Event) {
        let _guard = self.wake_lock.lock().unwrap();
        self.extra_events.lock().unwrap().push(event);
        self.wake.notify_one();
    }
}