950 lines
39 KiB
C
950 lines
39 KiB
C
/*
|
|
* el_android.c — Android JNI backend for the el native widget system.
|
|
*
|
|
* This file implements the Android widget layer that el_seed.c calls through
|
|
* to when EL_TARGET_ANDROID is defined. It is the exact Android counterpart
|
|
* to el_appkit.m and presents the same C API surface.
|
|
*
|
|
* Architecture:
|
|
* el program (el code)
|
|
* → __widget_* C builtins in el_seed.c
|
|
* → el_android_* C-callable functions declared here
|
|
* → ElBridge static methods in Java via JNI
|
|
* → android.view.View subclasses on the UI thread
|
|
*
|
|
* Widget handles: every widget (window root, view, control) is assigned an
|
|
* int64_t slot index into view_slots[]. The el program holds these as opaque
|
|
* Int values. Slot 0 is never valid (null handle = -1 convention).
|
|
*
|
|
* Threading: Android requires all UI operations to run on the main (UI) thread.
|
|
* Every JNI call that mutates a View is dispatched through
|
|
* Activity.runOnUiThread(Runnable) if the current thread is not the UI thread.
|
|
* el_android_run_loop is a no-op — Android lifecycle is driven by the Activity.
|
|
*
|
|
* Callback mechanism: when a widget fires an event Java calls
|
|
* nativeOnClick / nativeOnChange / nativeOnSubmit
|
|
* The C side looks up the registered El function name for that slot, then:
|
|
* dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string)
|
|
* This matches the __thread_create pattern in el_seed.c exactly.
|
|
*
|
|
* Compile / link (as part of libelruntime.so):
|
|
* Compiled by the Android Gradle NDK build system with -DEL_TARGET_ANDROID.
|
|
* Link flags: -landroid -llog -ldl
|
|
*
|
|
* Java companion: ElBridge.java in the same directory must be compiled into
|
|
* the Android application's APK (package com.neuron.el).
|
|
*/
|
|
|
|
#ifdef EL_TARGET_ANDROID
|
|
|
|
#include <jni.h>
|
|
#include <android/log.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <dlfcn.h>
|
|
#include "el_runtime.h"
|
|
|
|
/* ── Logging ─────────────────────────────────────────────────────────────── */
|
|
|
|
#define EL_TAG "ElAndroid"
|
|
#define EL_LOGI(...) __android_log_print(ANDROID_LOG_INFO, EL_TAG, __VA_ARGS__)
|
|
#define EL_LOGW(...) __android_log_print(ANDROID_LOG_WARN, EL_TAG, __VA_ARGS__)
|
|
#define EL_LOGE(...) __android_log_print(ANDROID_LOG_ERROR, EL_TAG, __VA_ARGS__)
|
|
|
|
/* ── JNI global state ────────────────────────────────────────────────────── */
|
|
|
|
static JavaVM *g_jvm = NULL;
|
|
static jobject g_activity = NULL; /* global ref to Activity */
|
|
static jclass g_bridge_class = NULL; /* global ref to ElBridge class */
|
|
|
|
/* Cached method IDs on ElBridge — filled in el_android_init(). */
|
|
static jmethodID g_mid_createLinearLayout = NULL;
|
|
static jmethodID g_mid_createFrameLayout = NULL;
|
|
static jmethodID g_mid_createScrollView = NULL;
|
|
static jmethodID g_mid_createTextView = NULL;
|
|
static jmethodID g_mid_createButton = NULL;
|
|
static jmethodID g_mid_createEditText = NULL;
|
|
static jmethodID g_mid_createImageView = NULL;
|
|
static jmethodID g_mid_setContentView = NULL;
|
|
static jmethodID g_mid_setTitle = NULL;
|
|
static jmethodID g_mid_addChild = NULL;
|
|
static jmethodID g_mid_removeChild = NULL;
|
|
static jmethodID g_mid_destroyView = NULL;
|
|
static jmethodID g_mid_setText = NULL;
|
|
static jmethodID g_mid_getText = NULL;
|
|
static jmethodID g_mid_setTextColor = NULL;
|
|
static jmethodID g_mid_setBackgroundColor = NULL;
|
|
static jmethodID g_mid_setFont = NULL;
|
|
static jmethodID g_mid_setPadding = NULL;
|
|
static jmethodID g_mid_setWidth = NULL;
|
|
static jmethodID g_mid_setHeight = NULL;
|
|
static jmethodID g_mid_setFlex = NULL;
|
|
static jmethodID g_mid_setCornerRadius = NULL;
|
|
static jmethodID g_mid_setEnabled = NULL;
|
|
static jmethodID g_mid_setVisibility = NULL;
|
|
static jmethodID g_mid_setOnClickListener = NULL;
|
|
static jmethodID g_mid_setOnChangeListener = NULL;
|
|
static jmethodID g_mid_setOnSubmitListener = NULL;
|
|
static jmethodID g_mid_runOnUiThread = NULL; /* Activity.runOnUiThread */
|
|
|
|
/* ── Widget table ─────────────────────────────────────────────────────────── */
|
|
|
|
#define EL_ANDROID_MAX_WIDGETS 4096
|
|
|
|
typedef enum {
|
|
EL_WIDGET_FREE = 0,
|
|
EL_WIDGET_WINDOW = 1,
|
|
EL_WIDGET_VSTACK = 2,
|
|
EL_WIDGET_HSTACK = 3,
|
|
EL_WIDGET_ZSTACK = 4,
|
|
EL_WIDGET_SCROLL = 5,
|
|
EL_WIDGET_LABEL = 6,
|
|
EL_WIDGET_BUTTON = 7,
|
|
EL_WIDGET_TEXTFIELD = 8,
|
|
EL_WIDGET_TEXTAREA = 9,
|
|
EL_WIDGET_IMAGE = 10,
|
|
} ElWidgetKind;
|
|
|
|
typedef struct {
|
|
ElWidgetKind kind;
|
|
jint slot; /* Java-side slot index (matches C index) */
|
|
char *cb_click; /* El function name for click / submit events */
|
|
char *cb_change; /* El function name for value-change events */
|
|
} ElWidget;
|
|
|
|
static ElWidget _el_widgets[EL_ANDROID_MAX_WIDGETS];
|
|
|
|
static int64_t el_widget_alloc(ElWidgetKind kind, jint slot) {
|
|
for (int i = 1; i < EL_ANDROID_MAX_WIDGETS; i++) {
|
|
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
|
|
_el_widgets[i].kind = kind;
|
|
_el_widgets[i].slot = slot;
|
|
_el_widgets[i].cb_click = NULL;
|
|
_el_widgets[i].cb_change = NULL;
|
|
return (int64_t)i;
|
|
}
|
|
}
|
|
EL_LOGE("el_widget_alloc: slot table full");
|
|
return -1;
|
|
}
|
|
|
|
static ElWidget *el_widget_get(int64_t handle) {
|
|
if (handle <= 0 || handle >= EL_ANDROID_MAX_WIDGETS) return NULL;
|
|
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
|
|
return &_el_widgets[handle];
|
|
}
|
|
|
|
static void el_widget_free(int64_t handle) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
w->kind = EL_WIDGET_FREE;
|
|
w->slot = -1;
|
|
free(w->cb_click); w->cb_click = NULL;
|
|
free(w->cb_change); w->cb_change = NULL;
|
|
}
|
|
|
|
/* ── JNI environment helpers ─────────────────────────────────────────────── */
|
|
|
|
/*
|
|
* Obtain a JNIEnv for the calling thread. Attaches the thread to the JVM if
|
|
* needed (detaches in el_jni_detach_if_attached — call in pairs).
|
|
*/
|
|
static int g_was_attached = 0; /* thread-local would be cleaner but this is
|
|
safe for single-threaded el programs */
|
|
|
|
static JNIEnv *el_jni_env(void) {
|
|
if (!g_jvm) return NULL;
|
|
JNIEnv *env = NULL;
|
|
jint rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_6);
|
|
if (rc == JNI_OK) { g_was_attached = 0; return env; }
|
|
if (rc == JNI_EDETACHED) {
|
|
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) == JNI_OK) {
|
|
g_was_attached = 1;
|
|
return env;
|
|
}
|
|
}
|
|
EL_LOGE("el_jni_env: failed to obtain JNIEnv");
|
|
return NULL;
|
|
}
|
|
|
|
static void el_jni_detach_if_attached(void) {
|
|
if (g_was_attached && g_jvm) {
|
|
(*g_jvm)->DetachCurrentThread(g_jvm);
|
|
g_was_attached = 0;
|
|
}
|
|
}
|
|
|
|
/* ── UI-thread dispatch ──────────────────────────────────────────────────── */
|
|
/*
|
|
* Most ElBridge static methods already dispatch to the UI thread internally
|
|
* (they call Activity.runOnUiThread). The helper below is available for
|
|
* cases where the caller needs to be sure the call has completed before
|
|
* returning (ElBridge methods marked "sync" use a CountDownLatch internally).
|
|
*
|
|
* For the current implementation we call ElBridge methods directly; ElBridge
|
|
* itself marshals to the UI thread via Activity.runOnUiThread + latch.
|
|
* This keeps the C side simple and mirrors the AppKit dispatch_sync pattern.
|
|
*/
|
|
|
|
/* ── JNI_OnLoad ──────────────────────────────────────────────────────────── */
|
|
|
|
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
|
|
(void)reserved;
|
|
g_jvm = vm;
|
|
EL_LOGI("JNI_OnLoad: el Android bridge loaded");
|
|
return JNI_VERSION_1_6;
|
|
}
|
|
|
|
/* ── el_android_init ─────────────────────────────────────────────────────── */
|
|
/*
|
|
* Called from __native_init(). The Activity must have already called
|
|
* ElBridge.registerActivity(activity) from Java before this runs, which sets
|
|
* g_activity via the nativeRegisterActivity JNI method below.
|
|
*
|
|
* Caches all method IDs used later so individual widget calls avoid repeated
|
|
* FindClass / GetStaticMethodID lookups.
|
|
*/
|
|
void el_android_init(void) {
|
|
static int done = 0;
|
|
if (done) return;
|
|
done = 1;
|
|
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env) { EL_LOGE("el_android_init: no JNIEnv"); return; }
|
|
|
|
jclass cls = (*env)->FindClass(env, "com/neuron/el/ElBridge");
|
|
if (!cls) { EL_LOGE("el_android_init: ElBridge class not found"); return; }
|
|
g_bridge_class = (*env)->NewGlobalRef(env, cls);
|
|
(*env)->DeleteLocalRef(env, cls);
|
|
|
|
#define CACHE_STATIC(var, name, sig) \
|
|
var = (*env)->GetStaticMethodID(env, g_bridge_class, name, sig); \
|
|
if (!var) EL_LOGW("el_android_init: method not found: %s %s", name, sig)
|
|
|
|
CACHE_STATIC(g_mid_createLinearLayout, "createLinearLayout", "(II)I");
|
|
CACHE_STATIC(g_mid_createFrameLayout, "createFrameLayout", "()I");
|
|
CACHE_STATIC(g_mid_createScrollView, "createScrollView", "()I");
|
|
CACHE_STATIC(g_mid_createTextView, "createTextView", "(Ljava/lang/String;)I");
|
|
CACHE_STATIC(g_mid_createButton, "createButton", "(Ljava/lang/String;)I");
|
|
CACHE_STATIC(g_mid_createEditText, "createEditText", "(Ljava/lang/String;Z)I");
|
|
CACHE_STATIC(g_mid_createImageView, "createImageView", "(Ljava/lang/String;)I");
|
|
CACHE_STATIC(g_mid_setContentView, "setContentView", "(I)V");
|
|
CACHE_STATIC(g_mid_setTitle, "setTitle", "(Ljava/lang/String;)V");
|
|
CACHE_STATIC(g_mid_addChild, "addChild", "(II)V");
|
|
CACHE_STATIC(g_mid_removeChild, "removeChild", "(II)V");
|
|
CACHE_STATIC(g_mid_destroyView, "destroyView", "(I)V");
|
|
CACHE_STATIC(g_mid_setText, "setText", "(ILjava/lang/String;)V");
|
|
CACHE_STATIC(g_mid_getText, "getText", "(I)Ljava/lang/String;");
|
|
CACHE_STATIC(g_mid_setTextColor, "setTextColor", "(IFFFF)V");
|
|
CACHE_STATIC(g_mid_setBackgroundColor, "setBackgroundColor", "(IFFFF)V");
|
|
CACHE_STATIC(g_mid_setFont, "setFont", "(ILjava/lang/String;IZ)V");
|
|
CACHE_STATIC(g_mid_setPadding, "setPadding", "(IIIII)V");
|
|
CACHE_STATIC(g_mid_setWidth, "setWidth", "(II)V");
|
|
CACHE_STATIC(g_mid_setHeight, "setHeight", "(II)V");
|
|
CACHE_STATIC(g_mid_setFlex, "setFlex", "(II)V");
|
|
CACHE_STATIC(g_mid_setCornerRadius, "setCornerRadius", "(IF)V");
|
|
CACHE_STATIC(g_mid_setEnabled, "setEnabled", "(IZ)V");
|
|
CACHE_STATIC(g_mid_setVisibility, "setVisibility", "(IZ)V");
|
|
CACHE_STATIC(g_mid_setOnClickListener, "setOnClickListener", "(I)V");
|
|
CACHE_STATIC(g_mid_setOnChangeListener, "setOnChangeListener", "(I)V");
|
|
CACHE_STATIC(g_mid_setOnSubmitListener, "setOnSubmitListener", "(I)V");
|
|
#undef CACHE_STATIC
|
|
|
|
el_jni_detach_if_attached();
|
|
EL_LOGI("el_android_init: complete");
|
|
}
|
|
|
|
/* ── JNI: Activity registration ─────────────────────────────────────────── */
|
|
|
|
/*
|
|
* Called from Java: ElBridge.registerActivity(activity) calls back here.
|
|
* Stores a global reference to the Activity so C code can dispatch to it.
|
|
*/
|
|
JNIEXPORT void JNICALL
|
|
Java_com_neuron_el_ElBridge_nativeRegisterActivity(JNIEnv *env, jclass cls,
|
|
jobject activity) {
|
|
(void)cls;
|
|
if (g_activity) {
|
|
(*env)->DeleteGlobalRef(env, g_activity);
|
|
g_activity = NULL;
|
|
}
|
|
if (activity) {
|
|
g_activity = (*env)->NewGlobalRef(env, activity);
|
|
EL_LOGI("nativeRegisterActivity: activity registered");
|
|
}
|
|
}
|
|
|
|
/* ── El callback invocation ──────────────────────────────────────────────── */
|
|
/*
|
|
* Invoke an El callback by symbol name.
|
|
* Signature matches AppKit: fn(handle: Int, data: String) -> Void
|
|
* compiled to: void fn(el_val_t handle, el_val_t data)
|
|
*/
|
|
typedef void (*ElCb2)(int64_t handle, int64_t data);
|
|
|
|
static void el_android_invoke_cb(const char *fn_name, int64_t handle,
|
|
const char *data) {
|
|
if (!fn_name || !*fn_name) return;
|
|
void *sym = dlsym(RTLD_DEFAULT, fn_name);
|
|
if (!sym) { EL_LOGW("invoke_cb: symbol not found: %s", fn_name); return; }
|
|
ElCb2 fn = (ElCb2)sym;
|
|
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
|
|
}
|
|
|
|
/* ── JNI: callbacks from Java → C ───────────────────────────────────────── */
|
|
|
|
JNIEXPORT void JNICALL
|
|
Java_com_neuron_el_ElBridge_nativeOnClick(JNIEnv *env, jclass cls, jint slot) {
|
|
(void)env; (void)cls;
|
|
int64_t handle = (int64_t)slot;
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (w && w->cb_click) {
|
|
el_android_invoke_cb(w->cb_click, handle, "");
|
|
}
|
|
}
|
|
|
|
JNIEXPORT void JNICALL
|
|
Java_com_neuron_el_ElBridge_nativeOnChange(JNIEnv *env, jclass cls,
|
|
jint slot, jstring text) {
|
|
(void)cls;
|
|
int64_t handle = (int64_t)slot;
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (w && w->cb_change) {
|
|
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
|
|
el_android_invoke_cb(w->cb_change, handle, ctext);
|
|
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
|
|
}
|
|
}
|
|
|
|
JNIEXPORT void JNICALL
|
|
Java_com_neuron_el_ElBridge_nativeOnSubmit(JNIEnv *env, jclass cls,
|
|
jint slot, jstring text) {
|
|
(void)cls;
|
|
int64_t handle = (int64_t)slot;
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (w && w->cb_click) { /* submit stored in cb_click, same as AppKit */
|
|
const char *ctext = text ? (*env)->GetStringUTFChars(env, text, NULL) : "";
|
|
el_android_invoke_cb(w->cb_click, handle, ctext);
|
|
if (text) (*env)->ReleaseStringUTFChars(env, text, ctext);
|
|
}
|
|
}
|
|
|
|
/* ── Helper: jstring from C string ──────────────────────────────────────── */
|
|
|
|
static jstring el_jstr(JNIEnv *env, const char *s) {
|
|
return (*env)->NewStringUTF(env, s ? s : "");
|
|
}
|
|
|
|
/* ── Window ──────────────────────────────────────────────────────────────── */
|
|
|
|
/*
|
|
* el_android_window_create — on Android a "window" is the root LinearLayout
|
|
* set as the Activity's content view. We create a vertical LinearLayout and
|
|
* store it. el_android_window_show calls setContentView on the Activity.
|
|
*/
|
|
int64_t el_android_window_create(const char *title, int width, int height,
|
|
int min_width, int min_height) {
|
|
(void)width; (void)height; (void)min_width; (void)min_height;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
|
|
/* VERTICAL LinearLayout with no spacing (spacing added via margins in Java) */
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createLinearLayout,
|
|
(jint)1 /* VERTICAL */, (jint)0);
|
|
if ((*env)->ExceptionCheck(env)) {
|
|
(*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1;
|
|
}
|
|
|
|
/* Set activity title */
|
|
if (g_mid_setTitle && title) {
|
|
jstring jtitle = el_jstr(env, title);
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
|
|
(*env)->DeleteLocalRef(env, jtitle);
|
|
}
|
|
|
|
int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return handle;
|
|
}
|
|
|
|
void el_android_window_show(int64_t handle) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w || w->kind != EL_WIDGET_WINDOW) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setContentView,
|
|
(jint)w->slot);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_window_set_title(int64_t handle, const char *title) {
|
|
(void)handle;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
jstring jtitle = el_jstr(env, title);
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTitle, jtitle);
|
|
(*env)->DeleteLocalRef(env, jtitle);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
/* ── Layout containers ───────────────────────────────────────────────────── */
|
|
|
|
int64_t el_android_vstack_create(int spacing) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createLinearLayout,
|
|
(jint)1 /* VERTICAL */, (jint)spacing);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_VSTACK, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
int64_t el_android_hstack_create(int spacing) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createLinearLayout,
|
|
(jint)0 /* HORIZONTAL */, (jint)spacing);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_HSTACK, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
int64_t el_android_zstack_create(void) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createFrameLayout);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_ZSTACK, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
int64_t el_android_scroll_create(void) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createScrollView);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_SCROLL, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
/* ── Widget factories ─────────────────────────────────────────────────────── */
|
|
|
|
int64_t el_android_label_create(const char *text) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jstring jt = el_jstr(env, text);
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createTextView, jt);
|
|
(*env)->DeleteLocalRef(env, jt);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_LABEL, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
int64_t el_android_button_create(const char *label) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jstring jl = el_jstr(env, label);
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createButton, jl);
|
|
(*env)->DeleteLocalRef(env, jl);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
int64_t el_android_text_field_create(const char *placeholder) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jstring jp = el_jstr(env, placeholder);
|
|
/* singleLine = true */
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createEditText, jp, (jboolean)JNI_TRUE);
|
|
(*env)->DeleteLocalRef(env, jp);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
int64_t el_android_text_area_create(const char *placeholder) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jstring jp = el_jstr(env, placeholder);
|
|
/* singleLine = false → multiline EditText */
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createEditText, jp, (jboolean)JNI_FALSE);
|
|
(*env)->DeleteLocalRef(env, jp);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
int64_t el_android_image_create(const char *path) {
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return -1;
|
|
jstring jp = el_jstr(env, path);
|
|
jint slot = (*env)->CallStaticIntMethod(env, g_bridge_class,
|
|
g_mid_createImageView, jp);
|
|
(*env)->DeleteLocalRef(env, jp);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return -1; }
|
|
int64_t h = el_widget_alloc(EL_WIDGET_IMAGE, (int)slot);
|
|
el_jni_detach_if_attached();
|
|
return h;
|
|
}
|
|
|
|
/* ── Widget property setters ─────────────────────────────────────────────── */
|
|
|
|
void el_android_widget_set_text(int64_t handle, const char *text) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
jstring jt = el_jstr(env, text);
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setText,
|
|
(jint)w->slot, jt);
|
|
(*env)->DeleteLocalRef(env, jt);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
const char *el_android_widget_get_text(int64_t handle) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return "";
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return "";
|
|
jstring js = (jstring)(*env)->CallStaticObjectMethod(env, g_bridge_class,
|
|
g_mid_getText,
|
|
(jint)w->slot);
|
|
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionClear(env); el_jni_detach_if_attached(); return ""; }
|
|
const char *result = "";
|
|
if (js) {
|
|
const char *cstr = (*env)->GetStringUTFChars(env, js, NULL);
|
|
result = cstr ? strdup(cstr) : "";
|
|
if (cstr) (*env)->ReleaseStringUTFChars(env, js, cstr);
|
|
(*env)->DeleteLocalRef(env, js);
|
|
}
|
|
el_jni_detach_if_attached();
|
|
return result;
|
|
}
|
|
|
|
void el_android_widget_set_color(int64_t handle, float r, float g, float b, float a) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setTextColor,
|
|
(jint)w->slot, (jfloat)r, (jfloat)g,
|
|
(jfloat)b, (jfloat)a);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setBackgroundColor,
|
|
(jint)w->slot, (jfloat)r, (jfloat)g,
|
|
(jfloat)b, (jfloat)a);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_font(int64_t handle, const char *family, int size, int bold) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
jstring jfam = el_jstr(env, family);
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFont,
|
|
(jint)w->slot, jfam, (jint)size,
|
|
(jboolean)(bold ? JNI_TRUE : JNI_FALSE));
|
|
(*env)->DeleteLocalRef(env, jfam);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setPadding,
|
|
(jint)w->slot, (jint)top, (jint)right,
|
|
(jint)bottom, (jint)left);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_width(int64_t handle, int width) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setWidth,
|
|
(jint)w->slot, (jint)width);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_height(int64_t handle, int height) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setHeight,
|
|
(jint)w->slot, (jint)height);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_flex(int64_t handle, int flex) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setFlex,
|
|
(jint)w->slot, (jint)flex);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_corner_radius(int64_t handle, int radius) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setCornerRadius,
|
|
(jint)w->slot, (jfloat)radius);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_disabled(int64_t handle, int disabled) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setEnabled,
|
|
(jint)w->slot,
|
|
(jboolean)(disabled ? JNI_FALSE : JNI_TRUE));
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_set_hidden(int64_t handle, int hidden) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
/* visible=true means NOT hidden */
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setVisibility,
|
|
(jint)w->slot,
|
|
(jboolean)(hidden ? JNI_FALSE : JNI_TRUE));
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
/* ── Child management ─────────────────────────────────────────────────────── */
|
|
|
|
void el_android_widget_add_child(int64_t parent, int64_t child) {
|
|
ElWidget *pw = el_widget_get(parent);
|
|
ElWidget *cw = el_widget_get(child);
|
|
if (!pw || !cw) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_addChild,
|
|
(jint)pw->slot, (jint)cw->slot);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_remove_child(int64_t parent, int64_t child) {
|
|
ElWidget *pw = el_widget_get(parent);
|
|
ElWidget *cw = el_widget_get(child);
|
|
if (!pw || !cw) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_removeChild,
|
|
(jint)pw->slot, (jint)cw->slot);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
/* ── Event registration ───────────────────────────────────────────────────── */
|
|
|
|
void el_android_widget_on_click(int64_t handle, const char *fn_name) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
free(w->cb_click);
|
|
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
|
if (!w->cb_click) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnClickListener,
|
|
(jint)w->slot);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_on_change(int64_t handle, const char *fn_name) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
free(w->cb_change);
|
|
w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
|
if (!w->cb_change) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnChangeListener,
|
|
(jint)w->slot);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
void el_android_widget_on_submit(int64_t handle, const char *fn_name) {
|
|
/* Submit stored in cb_click, same as AppKit. */
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
free(w->cb_click);
|
|
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
|
|
if (!w->cb_click) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (!env || !g_bridge_class) return;
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_setOnSubmitListener,
|
|
(jint)w->slot);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
/* ── Widget destroy ───────────────────────────────────────────────────────── */
|
|
|
|
void el_android_widget_destroy(int64_t handle) {
|
|
ElWidget *w = el_widget_get(handle);
|
|
if (!w) return;
|
|
JNIEnv *env = el_jni_env();
|
|
if (env && g_bridge_class) {
|
|
(*env)->CallStaticVoidMethod(env, g_bridge_class, g_mid_destroyView,
|
|
(jint)w->slot);
|
|
if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);
|
|
}
|
|
el_widget_free(handle);
|
|
el_jni_detach_if_attached();
|
|
}
|
|
|
|
/* ── Manifest reader ─────────────────────────────────────────────────────── */
|
|
/*
|
|
* __manifest_read: parse the app{} block from a manifest file.
|
|
* Returns the raw file contents as an el_val_t (const char* cast).
|
|
* The caller (el program) parses the returned string.
|
|
* Reads from the filesystem; for APK assets use the AssetManager path instead.
|
|
*/
|
|
static char *el_read_file(const char *path) {
|
|
if (!path || !*path) return NULL;
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return NULL;
|
|
fseek(f, 0, SEEK_END);
|
|
long len = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (len <= 0) { fclose(f); return NULL; }
|
|
char *buf = (char *)malloc((size_t)len + 1);
|
|
if (!buf) { fclose(f); return NULL; }
|
|
fread(buf, 1, (size_t)len, f);
|
|
buf[len] = '\0';
|
|
fclose(f);
|
|
return buf;
|
|
}
|
|
|
|
el_val_t el_android_manifest_read(const char *path) {
|
|
char *contents = el_read_file(path);
|
|
if (!contents) return (el_val_t)(uintptr_t)"";
|
|
return (el_val_t)(uintptr_t)contents; /* caller owns allocation */
|
|
}
|
|
|
|
/* ── __widget_* C API (called from el_seed.c) ────────────────────────────── */
|
|
/*
|
|
* These are the functions declared in el_native_target.h under EL_TARGET_ANDROID.
|
|
* They forward to the el_android_* internal functions above.
|
|
*
|
|
* The el_val_t / int64_t ABI matches the AppKit functions exactly:
|
|
* - Integer params passed as int64_t, extracted with (int)
|
|
* - String params passed as int64_t, extracted with (const char*)(uintptr_t)
|
|
* - Float params (r,g,b,a) passed as int64_t bit-cast from double; extracted
|
|
* with el_to_float / bit-cast union
|
|
*/
|
|
|
|
static inline float el_val_to_float(el_val_t v) {
|
|
union { double d; int64_t i; } u;
|
|
u.i = v;
|
|
return (float)u.d;
|
|
}
|
|
|
|
void __native_init(void) {
|
|
el_android_init();
|
|
}
|
|
|
|
void __native_run_loop(void) {
|
|
/* No-op on Android — lifecycle is driven by the Activity. */
|
|
}
|
|
|
|
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
|
|
el_val_t min_width, el_val_t min_height) {
|
|
return (el_val_t)el_android_window_create(
|
|
(const char *)(uintptr_t)title,
|
|
(int)width, (int)height, (int)min_width, (int)min_height);
|
|
}
|
|
|
|
void __window_show(el_val_t handle) {
|
|
el_android_window_show((int64_t)handle);
|
|
}
|
|
|
|
void __window_set_title(el_val_t handle, el_val_t title) {
|
|
el_android_window_set_title((int64_t)handle,
|
|
(const char *)(uintptr_t)title);
|
|
}
|
|
|
|
el_val_t __vstack_create(el_val_t spacing) {
|
|
return (el_val_t)el_android_vstack_create((int)spacing);
|
|
}
|
|
|
|
el_val_t __hstack_create(el_val_t spacing) {
|
|
return (el_val_t)el_android_hstack_create((int)spacing);
|
|
}
|
|
|
|
el_val_t __zstack_create(void) {
|
|
return (el_val_t)el_android_zstack_create();
|
|
}
|
|
|
|
el_val_t __scroll_create(void) {
|
|
return (el_val_t)el_android_scroll_create();
|
|
}
|
|
|
|
el_val_t __label_create(el_val_t text) {
|
|
return (el_val_t)el_android_label_create((const char *)(uintptr_t)text);
|
|
}
|
|
|
|
el_val_t __button_create(el_val_t label) {
|
|
return (el_val_t)el_android_button_create((const char *)(uintptr_t)label);
|
|
}
|
|
|
|
el_val_t __text_field_create(el_val_t placeholder) {
|
|
return (el_val_t)el_android_text_field_create((const char *)(uintptr_t)placeholder);
|
|
}
|
|
|
|
el_val_t __text_area_create(el_val_t placeholder) {
|
|
return (el_val_t)el_android_text_area_create((const char *)(uintptr_t)placeholder);
|
|
}
|
|
|
|
el_val_t __image_create(el_val_t path_or_name) {
|
|
return (el_val_t)el_android_image_create((const char *)(uintptr_t)path_or_name);
|
|
}
|
|
|
|
void __widget_set_text(el_val_t handle, el_val_t text) {
|
|
el_android_widget_set_text((int64_t)handle,
|
|
(const char *)(uintptr_t)text);
|
|
}
|
|
|
|
el_val_t __widget_get_text(el_val_t handle) {
|
|
return (el_val_t)(uintptr_t)el_android_widget_get_text((int64_t)handle);
|
|
}
|
|
|
|
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
|
|
el_val_t b, el_val_t a) {
|
|
el_android_widget_set_color((int64_t)handle,
|
|
el_val_to_float(r), el_val_to_float(g),
|
|
el_val_to_float(b), el_val_to_float(a));
|
|
}
|
|
|
|
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
|
|
el_val_t b, el_val_t a) {
|
|
el_android_widget_set_bg_color((int64_t)handle,
|
|
el_val_to_float(r), el_val_to_float(g),
|
|
el_val_to_float(b), el_val_to_float(a));
|
|
}
|
|
|
|
void __widget_set_font(el_val_t handle, el_val_t family,
|
|
el_val_t size, el_val_t bold) {
|
|
el_android_widget_set_font((int64_t)handle,
|
|
(const char *)(uintptr_t)family,
|
|
(int)size, (int)bold);
|
|
}
|
|
|
|
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
|
|
el_val_t bottom, el_val_t left) {
|
|
el_android_widget_set_padding((int64_t)handle,
|
|
(int)top, (int)right, (int)bottom, (int)left);
|
|
}
|
|
|
|
void __widget_set_width(el_val_t handle, el_val_t width) {
|
|
el_android_widget_set_width((int64_t)handle, (int)width);
|
|
}
|
|
|
|
void __widget_set_height(el_val_t handle, el_val_t height) {
|
|
el_android_widget_set_height((int64_t)handle, (int)height);
|
|
}
|
|
|
|
void __widget_set_flex(el_val_t handle, el_val_t flex) {
|
|
el_android_widget_set_flex((int64_t)handle, (int)flex);
|
|
}
|
|
|
|
void __widget_set_corner_radius(el_val_t handle, el_val_t radius) {
|
|
el_android_widget_set_corner_radius((int64_t)handle, (int)radius);
|
|
}
|
|
|
|
void __widget_set_disabled(el_val_t handle, el_val_t disabled) {
|
|
el_android_widget_set_disabled((int64_t)handle, (int)disabled);
|
|
}
|
|
|
|
void __widget_set_hidden(el_val_t handle, el_val_t hidden) {
|
|
el_android_widget_set_hidden((int64_t)handle, (int)hidden);
|
|
}
|
|
|
|
void __widget_add_child(el_val_t parent, el_val_t child) {
|
|
el_android_widget_add_child((int64_t)parent, (int64_t)child);
|
|
}
|
|
|
|
void __widget_remove_child(el_val_t parent, el_val_t child) {
|
|
el_android_widget_remove_child((int64_t)parent, (int64_t)child);
|
|
}
|
|
|
|
void __widget_destroy(el_val_t handle) {
|
|
el_android_widget_destroy((int64_t)handle);
|
|
}
|
|
|
|
void __widget_on_click(el_val_t handle, el_val_t fn_name) {
|
|
el_android_widget_on_click((int64_t)handle,
|
|
(const char *)(uintptr_t)fn_name);
|
|
}
|
|
|
|
void __widget_on_change(el_val_t handle, el_val_t fn_name) {
|
|
el_android_widget_on_change((int64_t)handle,
|
|
(const char *)(uintptr_t)fn_name);
|
|
}
|
|
|
|
void __widget_on_submit(el_val_t handle, el_val_t fn_name) {
|
|
el_android_widget_on_submit((int64_t)handle,
|
|
(const char *)(uintptr_t)fn_name);
|
|
}
|
|
|
|
el_val_t __manifest_read(el_val_t path) {
|
|
return el_android_manifest_read((const char *)(uintptr_t)path);
|
|
}
|
|
|
|
#endif /* EL_TARGET_ANDROID */
|