waterui-ffi 0.3.0

FFI bindings for the WaterUI cross-platform UI framework
//! JNI navigation functions for WaterUI Android.
//!
//! This module provides JNI functions for navigation controllers that are NOT
//! generated by macros. All type IDs and forceAs functions are auto-generated
//! by the `ffi_view!` and `ffi_metadata!` macros in their respective modules.

extern crate alloc;
extern crate std;

use alloc::boxed::Box;
use jni::objects::{Global, JClass, JObject, JValue};
use jni::sys::{jboolean, jint, jlong, jobject};
use jni::{Env, EnvUnowned, jni_sig, jni_str};
use std::sync::Arc;

/// Data for the JNI navigation controller callback.
struct JniNavigationCallbackData {
    jvm: Arc<jni::JavaVM>,
    callback: Global<JObject<'static>>,
}

impl JniNavigationCallbackData {
    fn with_env<R>(&self, f: impl FnOnce(&mut Env) -> R) -> R {
        super::with_attached_env(&self.jvm, f)
            .expect("navigation callback failed to attach its JVM thread")
    }
}

/// C-compatible transaction callback that calls into Java.
unsafe extern "C" fn jni_navigation_apply(
    data: *mut (),
    transaction: crate::components::navigation::WuiNavigationTransaction,
) {
    let data = unsafe { &*(data as *const JniNavigationCallbackData) };
    data.with_env(|env| {
        let view_class = env
            .find_class(jni_str!("dev/waterui/android/runtime/NavigationViewStruct"))
            .expect("NavigationViewStruct class not found");
        let inserted = env
            .new_object_array(
                super::array_len(transaction.inserted.len()),
                view_class,
                JObject::null(),
            )
            .expect("failed to allocate navigation transaction destination array");
        for (index, view) in transaction.inserted.iter().enumerate() {
            let view = crate::jni::convert::struct_to_java(env, view);
            inserted
                .set_element(env, index, view)
                .expect("failed to insert navigation transaction destination");
        }
        env.call_method(
            data.callback.as_obj(),
            jni_str!("onApply"),
            jni_sig!("(JII[Ldev/waterui/android/runtime/NavigationViewStruct;)V"),
            &[
                JValue::Long(transaction.id as jlong),
                JValue::Int(
                    i32::try_from(transaction.retained_prefix)
                        .expect("navigation retained prefix must fit Java Int"),
                ),
                JValue::Int(
                    i32::try_from(transaction.removed)
                        .expect("navigation removal count must fit Java Int"),
                ),
                JValue::Object(&inserted),
            ],
        )
        .expect("jni_navigation_apply: failed to call callback.onApply(...)");
        transaction.inserted.consume();
    });
}

/// C-compatible drop callback to clean up the JNI data.
unsafe extern "C" fn jni_navigation_drop(data: *mut ()) {
    let _ = unsafe { Box::from_raw(data as *mut JniNavigationCallbackData) };
}

// ============================================================================
// Navigation Controller Functions
// ============================================================================

/// Installs a navigation controller with a Java callback into the environment.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_envInstallNavigationController<
    'local,
>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    env_ptr: jlong,
    callback: JObject<'local>,
) {
    super::with_env(&mut env, |env| {
        let global_callback = env
            .new_global_ref(callback)
            .expect("Failed to create global ref for navigation callback");
        let data = Box::new(JniNavigationCallbackData {
            jvm: Arc::new(
                env.get_java_vm()
                    .expect("navigation controller failed to access JavaVM"),
            ),
            callback: global_callback,
        });
        let data_ptr = Box::into_raw(data) as *mut ();
        unsafe {
            crate::components::navigation::waterui_env_install_navigation_controller(
                env_ptr as *mut crate::WuiEnv,
                data_ptr,
                jni_navigation_apply,
                jni_navigation_drop,
            )
        }
    });
}

/// Checks if the environment has a navigation controller installed.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_envHasNavigationController<'local>(
    _env: EnvUnowned<'local>,
    _class: JClass<'local>,
    env_ptr: jlong,
) -> jboolean {
    let wui_env = env_ptr as *const crate::WuiEnv;
    unsafe { crate::components::navigation::waterui_env_has_navigation_controller(wui_env) }
}

/// Requests a user/system pop before Android begins a non-interactive transition.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_navigationRequestPop<'local>(
    _env: EnvUnowned<'local>,
    _class: JClass<'local>,
    env_ptr: jlong,
    count: jint,
) {
    let count = usize::try_from(count).expect("Android navigation pop count must be positive");
    assert!(count != 0, "Android navigation pop count must be non-zero");
    let env = unsafe { crate::borrow_ffi(env_ptr as *const crate::WuiEnv) };
    let controller = env
        .get::<waterui_navigation::NavigationController>()
        .expect("Android requested navigation pop without an installed controller");
    controller.request_pop(count);
}

/// Commits an Android predictive pop already completed by the native stack.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_navigationCompleteNativePop<'local>(
    _env: EnvUnowned<'local>,
    _class: JClass<'local>,
    env_ptr: jlong,
    count: jint,
) {
    let count = usize::try_from(count).expect("Android navigation pop count must be positive");
    assert!(count != 0, "Android navigation pop count must be non-zero");
    let env = unsafe { crate::borrow_ffi(env_ptr as *const crate::WuiEnv) };
    let controller = env
        .get::<waterui_navigation::NavigationController>()
        .expect("Android completed navigation pop without an installed controller");
    controller.complete_native_pop(count);
}

/// Acknowledges successful completion of the current Android transaction.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_navigationTransitionCompleted<'local>(
    _env: EnvUnowned<'local>,
    _class: JClass<'local>,
    env_ptr: jlong,
    id: jlong,
) -> jboolean {
    let id = u64::try_from(id).expect("Android navigation transaction id must be positive");
    let env = unsafe { crate::borrow_ffi(env_ptr as *const crate::WuiEnv) };
    let controller = env
        .get::<waterui_navigation::NavigationController>()
        .expect("Android completed navigation transaction without an installed controller");
    controller.transition_completed(id)
}

/// Acknowledges cancellation of the current Android transaction.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_navigationTransitionCancelled<'local>(
    _env: EnvUnowned<'local>,
    _class: JClass<'local>,
    env_ptr: jlong,
    id: jlong,
) -> jboolean {
    let id = u64::try_from(id).expect("Android navigation transaction id must be positive");
    let env = unsafe { crate::borrow_ffi(env_ptr as *const crate::WuiEnv) };
    let controller = env
        .get::<waterui_navigation::NavigationController>()
        .expect("Android cancelled navigation transaction without an installed controller");
    controller.transition_cancelled(id)
}

// ============================================================================
// Tab Content Function
// ============================================================================

/// Extracts the NavigationView content from a tab.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_tabContent<'local>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    content_ptr: jlong,
    env_ptr: jlong,
) -> jobject {
    use crate::components::navigation::WuiTabContent;

    let content = content_ptr as *mut WuiTabContent;
    let wui_env = env_ptr as *const crate::WuiEnv;
    let nav_view = unsafe { crate::components::navigation::waterui_tab_content(content, wui_env) };
    super::with_env(&mut env, |env| {
        crate::jni::convert::struct_to_java(env, &nav_view).into_raw()
    })
}

/// Resolves split detail content from a selected identifier.
#[unsafe(no_mangle)]
extern "system" fn Java_dev_waterui_android_ffi_WatcherJni_splitNavigationDetailContent<'local>(
    mut env: EnvUnowned<'local>,
    _class: JClass<'local>,
    detail_ptr: jlong,
    selected_id: jint,
    env_ptr: jlong,
) -> jobject {
    use crate::components::navigation::WuiNavigationSplitDetail;

    let detail = detail_ptr as *mut WuiNavigationSplitDetail;
    let wui_env = env_ptr as *const crate::WuiEnv;
    let nav_view = unsafe {
        crate::components::navigation::waterui_split_navigation_detail_content(
            detail,
            crate::id::WuiId { inner: selected_id },
            wui_env,
        )
    };
    super::with_env(&mut env, |env| {
        crate::jni::convert::struct_to_java(env, &nav_view).into_raw()
    })
}