diff --git a/lang/el-compiler/runtime/el_android.c b/lang/el-compiler/runtime/el_android.c new file mode 100644 index 0000000..7e3e765 --- /dev/null +++ b/lang/el-compiler/runtime/el_android.c @@ -0,0 +1,949 @@ +/* + * 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 +#include +#include +#include +#include +#include +#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 */ diff --git a/lang/el-compiler/runtime/el_appkit.m b/lang/el-compiler/runtime/el_appkit.m new file mode 100644 index 0000000..9eac7a6 --- /dev/null +++ b/lang/el-compiler/runtime/el_appkit.m @@ -0,0 +1,1018 @@ +/* + * el_appkit.m — AppKit backend for the el native widget system. + * + * This file implements the macOS AppKit widget layer that el_seed.c calls + * through to when EL_TARGET_MACOS is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_appkit_* C-callable functions declared here + * → NSView/NSWindow/NSStackView/NSButton/etc. via ObjC + * + * Widget handles: every widget (window, view, control) is assigned an int64_t + * slot index into _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved as null handle = -1 convention). + * + * Threading: All AppKit calls MUST run on the main thread. el_appkit_dispatch_main + * bounces work to the main thread synchronously if called from a background thread. + * el_appkit_run_loop MUST be called from the main thread; it never returns. + * + * Callback mechanism: when a widget fires an event (button click, text change), + * the bridge looks up the registered callback function name, then calls + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * This resolves the El function symbol at runtime, matching the __thread_create + * pattern already established in el_seed.c. + * + * Compile (MRC — no ARC; manual retain/release needed for struct-embedded id): + * clang -std=gnu11 -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m -o el_appkit.o + * Then link el_appkit.o alongside el_seed.c. + * + * Do NOT use -fobjc-arc: the widget table stores id in a plain C struct. ARC + * forbids explicit retain/release on struct-embedded object pointers and cannot + * insert automatic retain/release through C struct boundaries. MRC is the + * correct choice for this kind of C/ObjC bridge. + * + * Link flags required: + * -framework Cocoa + */ + +#import +#include +#include +#include +#include +#include + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_APPKIT_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, + EL_WIDGET_DIVIDER = 11, + EL_WIDGET_SPACER = 12, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + id obj; /* NSWindow* or NSView* subclass, retained */ + char* cb_click; /* El function name for click/submit events */ + char* cb_change; /* El function name for value-change events */ + char* user_data; /* custom data string passed to click callbacks */ +} ElWidget; + +static ElWidget _el_widgets[EL_APPKIT_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* Allocate a slot and return its index. Returns -1 if full. */ +static int64_t el_widget_alloc(ElWidgetKind kind, id obj) { + for (int i = 1; i < EL_APPKIT_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].obj = [obj retain]; + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + _el_widgets[i].user_data = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_APPKIT_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->obj release]; + w->obj = nil; + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; + free(w->user_data); w->user_data = NULL; +} + +/* ── Dispatch helpers ─────────────────────────────────────────────────────── */ + +/* Run a block on the main thread, waiting for completion. Safe to call from + * any thread including the main thread. */ +static void el_appkit_sync_main(void (^block)(void)) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. The El function must have the + * signature: fn handler(handle: Int, data: String) -> Void + * which compiles to: void fn_name(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_appkit_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) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── ObjC delegate for button clicks ─────────────────────────────────────── */ + +@interface ElButtonTarget : NSObject +@property int64_t widgetHandle; +@end + +@implementation ElButtonTarget +- (void)buttonClicked:(id)sender { + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + el_appkit_invoke_cb(w->cb_click, self.widgetHandle, w->user_data ? w->user_data : ""); + } +} +@end + +/* ── ObjC delegate for text field changes / enter key ───────────────────── */ + +@interface ElTextFieldDelegate : NSObject +@property int64_t widgetHandle; +@end + +@implementation ElTextFieldDelegate +- (void)controlTextDidChange:(NSNotification*)notif { + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w) return; + NSTextField* tf = (NSTextField*)notif.object; + const char* val = [[tf stringValue] UTF8String]; + if (w->cb_change) { + el_appkit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); + } +} +- (void)controlTextDidEndEditing:(NSNotification*)notif { + /* Enter key (or tab) submits. The userInfo value is an NSNumber, not NSEvent. */ + NSNumber* movement = [[notif userInfo] objectForKey:@"NSTextMovement"]; + if (movement && [movement intValue] == NSTextMovementReturn) { + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w) return; + NSTextField* tf = (NSTextField*)notif.object; + const char* val = [[tf stringValue] UTF8String]; + if (w->cb_click) { /* cb_click = on_submit for text fields */ + el_appkit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : ""); + } + } +} +@end + +/* We hold strong references to our ObjC delegate objects in a side table so + * ARC/MRC don't collect them. Simple parallel array keyed by widget slot. */ +static id _el_delegates[EL_APPKIT_MAX_WIDGETS]; + +/* ── Flipped wrapper for NSScrollView document views ───────────────────────── + * NSScrollView's clip view uses the document view's coordinate system. When + * the document view is non-flipped (origin at bottom-left), content stacks + * from the bottom. This subclass overrides isFlipped so content starts at the + * visual top-left, matching expected list/feed behaviour. */ +@interface ElFlippedView : NSView +@end +@implementation ElFlippedView +- (BOOL)isFlipped { return YES; } +@end + +/* ── ElResizableContentView ─────────────────────────────────────────────────── + * Used as NSWindow's content view instead of a plain NSView. + * Overrides setFrameSize: to propagate the new size directly to the single + * root child without going through the AL constraint graph. + * + * Why: with TAMIC:NO on the content view and 4-edge AL constraints to the + * child, NSWindow calls [contentView fittingSize] to compute its minimum + * resize floor. fittingSize walks the constraint graph and returns the + * largest minimum it finds — locking horizontal resize to the initial width. + * By removing the AL constraints and propagating via setFrameSize: instead, + * NSWindow has no constraint-based minimum and respects setContentMinSize: only. */ +@interface ElResizableContentView : NSView +@end +@implementation ElResizableContentView +- (NSSize)fittingSize { return NSMakeSize(1.0, 1.0); } +- (NSSize)intrinsicContentSize { return NSMakeSize(NSViewNoIntrinsicMetric, NSViewNoIntrinsicMetric); } +@end + +/* ElWindow subclass — overrides minSize so the AL constraint graph cannot + * inflate the resize floor beyond the manifest-specified minimum. + * NSWindow normally sets minSize from the larger of the explicit setMinSize: + * call and the AL-computed minimum content size; the override replaces that + * with the explicit minimum only. */ +@interface ElWindow : NSWindow +@property (nonatomic) NSSize elMinFrameSize; +@property (nonatomic) NSSize elInitialContentSize; /* designed width/height from manifest */ +@end +@implementation ElWindow +- (NSSize)minSize { return _elMinFrameSize; } +@end + +/* ── NSApplication setup ──────────────────────────────────────────────────── */ + +@interface ElAppDelegate : NSObject +@end + +@implementation ElAppDelegate +- (void)applicationDidFinishLaunching:(NSNotification*)notif { + /* Nothing needed — windows are created programmatically before run loop. */ +} +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app { + return YES; +} +@end + +static ElAppDelegate* _el_app_delegate = nil; + +/* ── Public C API (called from el_seed.c) ─────────────────────────────────── */ + +/* + * el_appkit_init — initialize NSApplication. Must be called once on the main + * thread before any other el_appkit_* function. Idempotent. + */ +void el_appkit_init(void) { + static int done = 0; + if (done) return; + done = 1; + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + _el_app_delegate = [[ElAppDelegate alloc] init]; + [NSApp setDelegate:_el_app_delegate]; +} + +/* + * el_appkit_window_create — create an NSWindow and return its handle. + * title, width, height, min_width, min_height come from manifest.el app{} block. + */ +int64_t el_appkit_window_create(const char* title, int width, int height, + int min_width, int min_height) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSRect frame = NSMakeRect(0, 0, (CGFloat)width, (CGFloat)height); + NSWindowStyleMask style = + NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskResizable; + ElWindow* win = [[ElWindow alloc] + initWithContentRect:frame + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; + [win setTitle:[NSString stringWithUTF8String:title ? title : ""]]; + /* Store the manifest minimum as our explicit frame floor. */ + NSRect minContentRect = NSMakeRect(0, 0, (CGFloat)min_width, (CGFloat)min_height); + NSSize minFrame = [win frameRectForContentRect:minContentRect].size; + win.elMinFrameSize = minFrame; + [win setContentMinSize:NSMakeSize((CGFloat)min_width, (CGFloat)min_height)]; + win.elInitialContentSize = NSMakeSize((CGFloat)width, (CGFloat)height); + [win center]; + + ElResizableContentView* root = [[ElResizableContentView alloc] initWithFrame:frame]; + [win setContentView:root]; + + handle = el_widget_alloc(EL_WIDGET_WINDOW, win); + [win release]; + [root release]; + }); + return handle; +} + +/* + * el_appkit_window_show — make the window visible and bring it to front. + */ +void el_appkit_window_show(int64_t handle) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + NSWindow* win = (NSWindow*)w->obj; + /* AL may have shrunk the window during the build phase. + * Restore the designed content size before showing — the window + * is not yet visible so the user never sees the shrunken state. + * After this, AL runs within the correct frame and required + * 4-edge constraints keep the child filling on every resize. */ + if ([win isKindOfClass:[ElWindow class]]) { + ElWindow* elWin = (ElWindow*)win; + NSSize cs = elWin.elInitialContentSize; + if (cs.width > 0) { + [win setContentSize:cs]; + [win center]; + } + } + [win makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + }); +} + +/* + * el_appkit_window_set_title — update window title at runtime. + */ +void el_appkit_window_set_title(int64_t handle, const char* title) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + NSWindow* win = (NSWindow*)w->obj; + [win setTitle:[NSString stringWithUTF8String:title ? title : ""]]; + }); +} + +/* + * el_appkit_run_loop — start the NSApplication run loop. Never returns. + * Must be called from the main thread as the last step of main(). + */ +void el_appkit_run_loop(void) { + [NSApp run]; +} + +/* ── Layout container factories ───────────────────────────────────────────── */ + +static int64_t make_stack(NSUserInterfaceLayoutOrientation orient, + CGFloat spacing) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSStackView* sv = [[NSStackView alloc] initWithFrame:NSZeroRect]; + [sv setOrientation:orient]; + /* Alignment must match the stack's cross-axis to avoid NSStackView + * silently flipping orientation. For a vertical stack, align children + * to the leading edge (left); for a horizontal stack, align to top. + * Using NSLayoutAttributeLeading on a horizontal stack flips it vertical. */ + NSLayoutAttribute align = (orient == NSUserInterfaceLayoutOrientationVertical) + ? NSLayoutAttributeLeading : NSLayoutAttributeTop; + [sv setAlignment:align]; + [sv setSpacing:spacing]; + [sv setEdgeInsets:NSEdgeInsetsZero]; + [sv setDistribution:NSStackViewDistributionFill]; + [sv setTranslatesAutoresizingMaskIntoConstraints:NO]; + ElWidgetKind kind = (orient == NSUserInterfaceLayoutOrientationVertical) + ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; + handle = el_widget_alloc(kind, sv); + [sv release]; + }); + return handle; +} + +int64_t el_appkit_vstack_create(int spacing) { + return make_stack(NSUserInterfaceLayoutOrientationVertical, (CGFloat)spacing); +} + +int64_t el_appkit_hstack_create(int spacing) { + return make_stack(NSUserInterfaceLayoutOrientationHorizontal, (CGFloat)spacing); +} + +int64_t el_appkit_zstack_create(void) { + /* ZStack = NSView with manual child positioning (no stack manager). */ + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSView* v = [[NSView alloc] initWithFrame:NSZeroRect]; + [v setTranslatesAutoresizingMaskIntoConstraints:NO]; + handle = el_widget_alloc(EL_WIDGET_ZSTACK, v); + [v release]; + }); + return handle; +} + +int64_t el_appkit_scroll_create(void) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSScrollView* sv = [[NSScrollView alloc] initWithFrame:NSZeroRect]; + [sv setHasVerticalScroller:YES]; + [sv setHasHorizontalScroller:NO]; + [sv setAutohidesScrollers:YES]; + [sv setTranslatesAutoresizingMaskIntoConstraints:NO]; + handle = el_widget_alloc(EL_WIDGET_SCROLL, sv); + [sv release]; + }); + return handle; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_appkit_label_create(const char* text) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSTextField* lbl = [NSTextField labelWithString: + [NSString stringWithUTF8String:text ? text : ""]]; + [lbl setEditable:NO]; + [lbl setBordered:NO]; + [lbl setDrawsBackground:NO]; + [lbl setTranslatesAutoresizingMaskIntoConstraints:NO]; + handle = el_widget_alloc(EL_WIDGET_LABEL, lbl); + }); + return handle; +} + +int64_t el_appkit_button_create(const char* label) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSButton* btn = [[NSButton alloc] initWithFrame:NSZeroRect]; + [btn setButtonType:NSButtonTypeMomentaryLight]; + [btn setBezelStyle:NSBezelStyleShadowlessSquare]; + [btn setTitle:[NSString stringWithUTF8String:label ? label : ""]]; + [btn setAlignment:NSTextAlignmentLeft]; + [btn setTranslatesAutoresizingMaskIntoConstraints:NO]; + + /* Create action target. */ + ElButtonTarget* target = [[ElButtonTarget alloc] init]; + /* handle assigned below after alloc — set after */ + [btn setTarget:target]; + [btn setAction:@selector(buttonClicked:)]; + + int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn); + target.widgetHandle = h; + _el_delegates[h] = target; /* retain */ + handle = h; + [btn release]; + }); + return handle; +} + +int64_t el_appkit_text_field_create(const char* placeholder) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSTextField* tf = [[NSTextField alloc] initWithFrame:NSZeroRect]; + [tf setTranslatesAutoresizingMaskIntoConstraints:NO]; + if (placeholder && *placeholder) { + [tf setPlaceholderString:[NSString stringWithUTF8String:placeholder]]; + } + + ElTextFieldDelegate* del = [[ElTextFieldDelegate alloc] init]; + [tf setDelegate:del]; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf); + del.widgetHandle = h; + _el_delegates[h] = del; + handle = h; + [tf release]; + }); + return handle; +} + +int64_t el_appkit_text_area_create(const char* placeholder) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSScrollView* scroll = [[NSScrollView alloc] initWithFrame:NSZeroRect]; + [scroll setHasVerticalScroller:YES]; + [scroll setAutohidesScrollers:YES]; + [scroll setTranslatesAutoresizingMaskIntoConstraints:NO]; + + NSTextView* tv = [[NSTextView alloc] initWithFrame:NSZeroRect]; + [tv setMinSize:NSMakeSize(0, 60)]; + [tv setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [tv setVerticallyResizable:YES]; + [tv setHorizontallyResizable:NO]; + [tv setAutoresizingMask:NSViewWidthSizable]; + if (placeholder && *placeholder) { + /* NSTextView has no native placeholder; we set a hint via + * a custom accessibility description as a minimal approach. */ + [tv setAccessibilityPlaceholderValue: + [NSString stringWithUTF8String:placeholder]]; + } + [[tv textContainer] setWidthTracksTextView:YES]; + [scroll setDocumentView:tv]; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, scroll); + handle = h; + [scroll release]; + [tv release]; + }); + return handle; +} + +int64_t el_appkit_image_create(const char* path_or_name) { + __block int64_t handle = -1; + el_appkit_sync_main(^{ + NSImage* img = nil; + if (path_or_name && *path_or_name) { + /* Try as filesystem path first, then as named system image. */ + img = [[NSImage alloc] initWithContentsOfFile: + [NSString stringWithUTF8String:path_or_name]]; + if (!img) { + img = [NSImage imageNamed:[NSString stringWithUTF8String:path_or_name]]; + if (img) [img retain]; + } + } + NSImageView* iv = [[NSImageView alloc] initWithFrame:NSZeroRect]; + [iv setTranslatesAutoresizingMaskIntoConstraints:NO]; + if (img) { + [iv setImage:img]; + [img release]; + } + handle = el_widget_alloc(EL_WIDGET_IMAGE, iv); + [iv release]; + }); + return handle; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_appkit_widget_set_text(int64_t handle, const char* text) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = [NSString stringWithUTF8String:text ? text : ""]; + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + [(NSTextField*)w->obj setStringValue:s]; + break; + case EL_WIDGET_BUTTON: + [(NSButton*)w->obj setTitle:s]; + break; + case EL_WIDGET_TEXTAREA: { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + [tv setString:s]; + break; + } + case EL_WIDGET_WINDOW: + [(NSWindow*)w->obj setTitle:s]; + break; + default: break; + } + }); +} + +const char* el_appkit_widget_get_text(int64_t handle) { + __block const char* result = ""; + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = nil; + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + s = [(NSTextField*)w->obj stringValue]; + break; + case EL_WIDGET_BUTTON: + s = [(NSButton*)w->obj title]; + break; + case EL_WIDGET_TEXTAREA: { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + s = [tv string]; + break; + } + default: break; + } + if (s) result = strdup([s UTF8String]); + }); + return result; +} + +void el_appkit_widget_set_color(int64_t handle, float r, float g, float b, float a) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r + green:(CGFloat)g + blue:(CGFloat)b + alpha:(CGFloat)a]; + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + [(NSTextField*)w->obj setTextColor:c]; + break; + case EL_WIDGET_BUTTON: { + /* Update foreground color on attributed title, preserving font. */ + NSButton* btn = (NSButton*)w->obj; + NSAttributedString* existing = [btn attributedTitle]; + NSMutableAttributedString* as; + if (existing && [existing length] > 0) { + as = [[NSMutableAttributedString alloc] initWithAttributedString:existing]; + } else { + NSString* t = [btn title]; + if (!t) t = @""; + as = [[NSMutableAttributedString alloc] initWithString:t]; + } + [as addAttribute:NSForegroundColorAttributeName value:c + range:NSMakeRange(0, [as length])]; + [btn setAttributedTitle:as]; + [as release]; + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + case EL_WIDGET_ZSTACK: { + /* Set background via wantsLayer + layer background. */ + NSView* v = (NSView*)w->obj; + [v setWantsLayer:YES]; + [v layer].backgroundColor = [c CGColor]; + break; + } + default: break; + } + }); +} + +void el_appkit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r + green:(CGFloat)g + blue:(CGFloat)b + alpha:(CGFloat)a]; + if (w->kind == EL_WIDGET_WINDOW) { + NSWindow* win = (NSWindow*)w->obj; + [win setBackgroundColor:c]; + /* Also paint the contentView layer so it shows through NSStackView. */ + NSView* cv = [win contentView]; + [cv setWantsLayer:YES]; + [cv layer].backgroundColor = [c CGColor]; + return; + } + if (w->kind == EL_WIDGET_BUTTON) { + /* Disable native bezel so CALayer background is visible. */ + NSButton* btn = (NSButton*)w->obj; + [btn setBordered:NO]; + [btn setWantsLayer:YES]; + [btn layer].backgroundColor = [c CGColor]; + return; + } + NSView* v = (NSView*)w->obj; + if (w->kind == EL_WIDGET_SCROLL) { + /* NSScrollView needs setBackgroundColor, not just layer bg. */ + NSScrollView* sv = (NSScrollView*)v; + [sv setBackgroundColor:c]; + [sv setDrawsBackground:YES]; + } + [v setWantsLayer:YES]; + [v layer].backgroundColor = [c CGColor]; + }); +} + +void el_appkit_widget_set_font(int64_t handle, const char* family, int size, int bold) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSFont* font; + if (bold) { + font = [NSFont boldSystemFontOfSize:(CGFloat)size]; + } else if (family && *family && strcmp(family, "system") != 0) { + font = [NSFont fontWithName:[NSString stringWithUTF8String:family] + size:(CGFloat)size]; + if (!font) font = [NSFont systemFontOfSize:(CGFloat)size]; + } else { + font = [NSFont systemFontOfSize:(CGFloat)size]; + } + switch (w->kind) { + case EL_WIDGET_LABEL: + case EL_WIDGET_TEXTFIELD: + [(NSTextField*)w->obj setFont:font]; + break; + case EL_WIDGET_BUTTON: + /* NSButton: set font via attributed title, preserving existing foreground color. */ + { + NSButton* fbtn = (NSButton*)w->obj; + NSAttributedString* fexisting = [fbtn attributedTitle]; + NSMutableAttributedString* as; + if (fexisting && [fexisting length] > 0) { + as = [[NSMutableAttributedString alloc] initWithAttributedString:fexisting]; + } else { + NSString* ft = [fbtn title]; + if (!ft) ft = @""; + as = [[NSMutableAttributedString alloc] initWithString:ft]; + } + [as addAttribute:NSFontAttributeName value:font + range:NSMakeRange(0, [as length])]; + [fbtn setAttributedTitle:as]; + [as release]; + } + break; + case EL_WIDGET_TEXTAREA: { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + [tv setFont:font]; + break; + } + default: break; + } + }); +} + +void el_appkit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + /* Padding on stack views maps to edgeInsets. */ + if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) { + NSStackView* sv = (NSStackView*)w->obj; + [sv setEdgeInsets:NSEdgeInsetsMake((CGFloat)top, (CGFloat)left, + (CGFloat)bottom, (CGFloat)right)]; + } + /* For individual widgets we set a custom layout margin via constraints. + * For now, NSTextView respects textContainerInset. */ + if (w->kind == EL_WIDGET_TEXTAREA) { + NSScrollView* sv = (NSScrollView*)w->obj; + NSTextView* tv = (NSTextView*)[sv documentView]; + [tv setTextContainerInset:NSMakeSize((CGFloat)(left + right) / 2, + (CGFloat)(top + bottom) / 2)]; + } + }); +} + +void el_appkit_widget_set_width(int64_t handle, int width) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + /* Add a fixed-width constraint. */ + NSLayoutConstraint* c = [NSLayoutConstraint + constraintWithItem:v + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:(CGFloat)width]; + c.priority = NSLayoutPriorityDefaultHigh; + [v addConstraint:c]; + }); +} + +void el_appkit_widget_set_height(int64_t handle, int height) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + NSLayoutConstraint* c = [NSLayoutConstraint + constraintWithItem:v + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:(CGFloat)height]; + c.priority = NSLayoutPriorityDefaultHigh; + [v addConstraint:c]; + }); +} + +void el_appkit_widget_set_flex(int64_t handle, int flex) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + if (flex > 0) { + /* flex:1 → low hugging (wants to expand) + low compression resistance + * (willing to be compressed). This makes NSStackViewDistributionFill + * choose this view to grow/shrink to fill available space. */ + [v setContentHuggingPriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentHuggingPriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationVertical]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow + forOrientation:NSLayoutConstraintOrientationVertical]; + } else { + /* flex:0 → high hugging (stays at natural size) + high compression + * resistance (resists shrinking). */ + [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationVertical]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationHorizontal]; + [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh + forOrientation:NSLayoutConstraintOrientationVertical]; + } + }); +} + +void el_appkit_widget_set_corner_radius(int64_t handle, int radius) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if ([v isKindOfClass:[NSWindow class]]) return; + [v setWantsLayer:YES]; + [v layer].cornerRadius = (CGFloat)radius; + [v layer].masksToBounds = YES; + }); +} + +void el_appkit_widget_set_disabled(int64_t handle, int disabled) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind == EL_WIDGET_BUTTON) { + [(NSButton*)w->obj setEnabled:!disabled]; + } else if (w->kind == EL_WIDGET_TEXTFIELD) { + [(NSTextField*)w->obj setEnabled:!disabled]; + } + }); +} + +void el_appkit_widget_set_hidden(int64_t handle, int hidden) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if (![v isKindOfClass:[NSWindow class]]) { + [v setHidden:(BOOL)hidden]; + } + }); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +/* + * el_appkit_widget_add_child — attach child widget to parent. + * + * - Window: child is added to contentView (NSStackView). + * - VStack/HStack: child is added as an arranged subview. + * - ZStack/generic view: child is added as a plain subview. + * - ScrollView: child becomes the documentView (only first child honoured). + */ +void el_appkit_widget_add_child(int64_t parent, int64_t child) { + el_appkit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!pw || !cw) return; + + NSView* childView = (NSView*)cw->obj; + if ([childView isKindOfClass:[NSWindow class]]) return; /* can't add window as child */ + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + NSWindow* win = (NSWindow*)pw->obj; + NSView* cv = [win contentView]; + /* Standard required 4-edge constraints so the child always + * fills the content view during user-initiated resize. + * AL may shrink the window during the build phase (before show), + * but el_appkit_window_show calls setContentSize: to restore + * the designed size before making the window visible. */ + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [cv addSubview:childView]; + [NSLayoutConstraint activateConstraints:@[ + [childView.topAnchor constraintEqualToAnchor:cv.topAnchor], + [childView.bottomAnchor constraintEqualToAnchor:cv.bottomAnchor], + [childView.leadingAnchor constraintEqualToAnchor:cv.leadingAnchor], + [childView.trailingAnchor constraintEqualToAnchor:cv.trailingAnchor], + ]]; + break; + } + case EL_WIDGET_VSTACK: { + NSStackView* sv = (NSStackView*)pw->obj; + [sv addArrangedSubview:childView]; + /* Pin child leading+trailing to fill cross-axis (width) of the VStack. + * Priority 999 avoids intrinsic-size feedback through NSWindow. */ + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + NSLayoutConstraint* vl = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeLeading + multiplier:1.0 constant:0.0]; + NSLayoutConstraint* vr = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeTrailing + multiplier:1.0 constant:0.0]; + vl.priority = 999; + vr.priority = 999; + [sv addConstraints:@[vl, vr]]; + break; + } + case EL_WIDGET_HSTACK: { + NSStackView* sv = (NSStackView*)pw->obj; + [sv addArrangedSubview:childView]; + /* Pin child top+bottom to fill cross-axis (height) of the HStack. + * Priority 499 (well below required 1000) lets the window frame + * win over child intrinsic size, avoiding NSWindow auto-resize. */ + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + NSLayoutConstraint* ht = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeTop + multiplier:1.0 constant:0.0]; + NSLayoutConstraint* hb = [NSLayoutConstraint + constraintWithItem:childView attribute:NSLayoutAttributeBottom + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeBottom + multiplier:1.0 constant:0.0]; + ht.priority = 499; + hb.priority = 499; + [sv addConstraints:@[ht, hb]]; + break; + } + case EL_WIDGET_SCROLL: { + NSScrollView* sv = (NSScrollView*)pw->obj; + /* Wrap the child in a flipped view so content starts at the + * visual top-left. Without flipping, NSScrollView places the + * document origin at the bottom-left and lists appear inverted. */ + ElFlippedView* flipper = [[ElFlippedView alloc] initWithFrame:NSZeroRect]; + [flipper setTranslatesAutoresizingMaskIntoConstraints:NO]; + [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; + [flipper addSubview:childView]; + /* Pin child to all 4 edges of the flipper. */ + NSArray* hc = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[c]|" + options:0 metrics:nil views:@{@"c": childView}]; + NSArray* vc = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[c]|" + options:0 metrics:nil views:@{@"c": childView}]; + [flipper addConstraints:hc]; + [flipper addConstraints:vc]; + [sv setDocumentView:flipper]; + /* Constrain flipper width to the scroll view so items fill the + * full panel width (vertical-only scrolling). */ + NSLayoutConstraint* wc = [NSLayoutConstraint + constraintWithItem:flipper attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:sv attribute:NSLayoutAttributeWidth + multiplier:1.0 constant:0.0]; + [sv addConstraint:wc]; + [flipper release]; + break; + } + case EL_WIDGET_ZSTACK: + default: { + NSView* pv = (NSView*)pw->obj; + [pv addSubview:childView]; + break; + } + } + }); +} + +/* + * el_appkit_widget_remove_child — remove a child widget from its parent. + */ +void el_appkit_widget_remove_child(int64_t parent, int64_t child) { + el_appkit_sync_main(^{ + ElWidget* cw = el_widget_get(child); + if (!cw) return; + NSView* v = (NSView*)cw->obj; + if (![v isKindOfClass:[NSWindow class]]) { + [v removeFromSuperview]; + } + }); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_appkit_widget_set_data(int64_t handle, const char* data_str) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->user_data); + w->user_data = data_str && *data_str ? strdup(data_str) : NULL; +} + +void el_appkit_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; +} + +void el_appkit_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; +} + +void el_appkit_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, submit = pressing Enter → stored in cb_click slot. */ + el_appkit_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_appkit_widget_destroy(int64_t handle) { + el_appkit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSView* v = (NSView*)w->obj; + if (![v isKindOfClass:[NSWindow class]]) { + [v removeFromSuperview]; + } else { + [(NSWindow*)w->obj close]; + } + [_el_delegates[handle] release]; + _el_delegates[handle] = nil; + el_widget_free(handle); + }); +} diff --git a/lang/el-compiler/runtime/el_gtk4.c b/lang/el-compiler/runtime/el_gtk4.c new file mode 100644 index 0000000..d427786 --- /dev/null +++ b/lang/el-compiler/runtime/el_gtk4.c @@ -0,0 +1,1327 @@ +/* + * el_gtk4.c — GTK4 backend for the el native widget system. + * + * This file implements the GTK4 widget layer that el_seed.c calls through to + * when EL_TARGET_LINUX is defined. It runs on both Linux and macOS (Homebrew). + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_gtk4_* C-callable functions declared here + * → GtkWidget* / GtkWindow* via GTK4 C API + * + * Widget handles: every widget is assigned an int64_t slot index into + * _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved; invalid handle = -1 convention). + * + * macOS lifecycle: + * On macOS the GTK4 Quartz backend sets the NSApplication main menu during + * g_application_run → startup signal, which requires the main thread. The el + * program's compiled main() calls native_init(), then window/widget creation, + * then native_run_loop() — so g_application_run() IS called from the main + * thread in el_gtk4_run_loop(). + * + * The constraint is that GTK widget creation requires an initialized display, + * which only exists after g_application_run starts. To handle this, all + * el_gtk4_* calls made before el_gtk4_run_loop() are DEFERRED into a queue. + * The activate handler replays the queue, then the app runs normally. + * + * Threading (post-activation): + * GTK4 must run on the main thread. el_gtk4_sync_main() uses + * g_main_context_invoke_full() to dispatch from any thread, waiting for + * completion via a GMutex/GCond pair — the same role as dispatch_sync() in + * the AppKit bridge. + * + * Callback mechanism: on signal, the bridge calls + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * matching the el_appkit.m / __thread_create pattern in el_seed.c. + * + * Compile: + * gcc -DEL_TARGET_LINUX $(pkg-config --cflags gtk4) \ + * el_gtk4.c -c -o el_gtk4.o + * Then link el_gtk4.o alongside el_seed.c with: + * $(pkg-config --libs gtk4) -ldl + * + * GTK4 version notes: + * • gtk_widget_get_style_context() is deprecated in GTK 4.10+. + * A #if GTK_MINOR_VERSION guard selects the correct CSS-provider path. + * • gtk_editable_get_text() / gtk_editable_set_text() are the unified + * GTK4 text API for GtkEntry and any GtkEditable. + */ + +#ifdef EL_TARGET_LINUX + +/* _GNU_SOURCE: expose RTLD_DEFAULT (dlfcn.h) and strdup (string.h) on Linux. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include + +/* G_APPLICATION_DEFAULT_FLAGS was added in GLib 2.74. + * Ubuntu 22.04 ships GLib 2.72 — use the older alias when running on it. */ +#ifndef G_APPLICATION_DEFAULT_FLAGS +#define G_APPLICATION_DEFAULT_FLAGS G_APPLICATION_FLAGS_NONE +#endif + +/* el_val_t must already be defined by el_runtime.h / el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +#ifndef EL_CSTR +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#endif +#ifndef EL_STR +#define EL_STR(s) ((el_val_t)(uintptr_t)(s)) +#endif + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_GTK4_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, + EL_WIDGET_DIVIDER = 11, + EL_WIDGET_SPACER = 12, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + GtkWidget* widget; /* Strong reference (g_object_ref'd on alloc). */ + 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_GTK4_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* + * el_widget_alloc — allocate a slot for a fully-constructed widget. + * Takes a strong ref. Returns slot index or -1 if full. + */ +static int64_t el_widget_alloc(ElWidgetKind kind, GtkWidget* w) { + for (int i = 1; i < EL_GTK4_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].widget = (GtkWidget*)g_object_ref(w); + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +/* + * el_widget_prealloc — reserve a slot without a GTK widget (pre-activation). + * The slot kind is set; widget remains NULL until el_widget_install() fills it. + * Returns the reserved slot index, or -1 if full. + */ +static int64_t el_widget_prealloc(ElWidgetKind kind) { + for (int i = 1; i < EL_GTK4_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].widget = NULL; + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +/* + * el_widget_install — install a GTK widget into a pre-allocated slot. + * Takes a strong ref on w. + */ +static void el_widget_install(int64_t slot, GtkWidget* w) { + if (slot <= 0 || slot >= EL_GTK4_MAX_WIDGETS) return; + if (_el_widgets[slot].widget) g_object_unref(_el_widgets[slot].widget); + _el_widgets[slot].widget = (GtkWidget*)g_object_ref(w); +} + +/* Look up a slot. Returns NULL for invalid/free handle. */ +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_GTK4_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +/* Release a slot. Unrefs the widget; callers must remove from parent first. */ +static void el_widget_free(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->widget) { g_object_unref(w->widget); w->widget = NULL; } + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* ── Deferred operation queue ─────────────────────────────────────────────── */ +/* + * Before el_gtk4_run_loop() starts g_application_run(), the GTK display is not + * yet initialized. All el_gtk4_* calls are recorded as deferred ops and + * replayed from the activate handler after the display is ready. + * + * Each deferred op is { fn, data } where fn is a _main-style function and data + * is a heap-allocated copy of the arguments. The _main functions are called + * exactly as they would be post-activation. + * + * Creation functions (window_create, label_create, etc.) pre-allocate a slot + * via el_widget_prealloc() before recording the deferred op. The slot index is + * returned immediately to the el program. The _main function receives the + * pre-allocated slot as part of its arg struct and calls el_widget_install() + * rather than el_widget_alloc(). + */ + +#define EL_DEFERRED_MAX 8192 + +typedef struct { + void (*fn)(void*); + void* data; +} ElDeferredOp; + +static ElDeferredOp _el_deferred[EL_DEFERRED_MAX]; +static int _el_deferred_count = 0; + +/* Queue a deferred op. data must be heap-allocated (will be free()'d after replay). */ +static void el_defer(void (*fn)(void*), void* data) { + if (_el_deferred_count >= EL_DEFERRED_MAX) { + fprintf(stderr, "el_gtk4: deferred op queue full\n"); + return; + } + _el_deferred[_el_deferred_count].fn = fn; + _el_deferred[_el_deferred_count].data = data; + _el_deferred_count++; +} + +/* Replay all deferred ops in order. Called from the activate handler. */ +static void el_deferred_replay(void) { + for (int i = 0; i < _el_deferred_count; i++) { + _el_deferred[i].fn(_el_deferred[i].data); + free(_el_deferred[i].data); + } + _el_deferred_count = 0; +} + +/* ── GtkApplication (global) ─────────────────────────────────────────────── */ + +static GtkApplication* _el_app = NULL; +static int _el_app_running = 0; /* 1 after activate fires */ + +/* ── CSS / styling helpers ────────────────────────────────────────────────── */ + +/* + * apply_css — attach a CSS snippet to a single widget with APPLICATION priority. + * GTK 4.10 deprecated gtk_widget_get_style_context(); use gtk_widget_add_css_class() + * + a display-level provider for new GTK, or the style-context path for older GTK. + * + * We use the style-context path behind a version guard because it reliably + * scopes the CSS to just this widget, which is what the el property setters + * need (each call replaces/adds to the widget's own stylesheet). + */ +static void apply_css(GtkWidget* widget, const char* css_str) { + if (!widget || !css_str) return; + GtkCssProvider* p = gtk_css_provider_new(); + +#if GTK_CHECK_VERSION(4, 12, 0) + /* GTK 4.12+: load_from_string takes a plain string (no length). */ + gtk_css_provider_load_from_string(p, css_str); +#else + gtk_css_provider_load_from_data(p, css_str, -1); +#endif + +#if GTK_MINOR_VERSION >= 10 + /* 4.10+: style context is deprecated; use display-scoped provider + css class. + * We add a unique widget name so the selector targets only this widget. */ + static guint64 _css_id_counter = 0; + char name_buf[32]; + snprintf(name_buf, sizeof(name_buf), "el-w-%" G_GUINT64_FORMAT, ++_css_id_counter); + gtk_widget_set_name(widget, name_buf); + + /* Wrap the CSS to scope to this widget's name. */ + char* scoped = NULL; + int n = asprintf(&scoped, "#%s { %s }", name_buf, css_str); + (void)n; + if (scoped) { + GtkCssProvider* sp = gtk_css_provider_new(); +#if GTK_CHECK_VERSION(4, 12, 0) + gtk_css_provider_load_from_string(sp, scoped); +#else + gtk_css_provider_load_from_data(sp, scoped, -1); +#endif + gtk_style_context_add_provider_for_display( + gtk_widget_get_display(widget), + GTK_STYLE_PROVIDER(sp), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref(sp); + free(scoped); + } + g_object_unref(p); +#else + /* GTK < 4.10: style context is available and widget-scoped. */ + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_style_context_add_provider( + gtk_widget_get_style_context(widget), + GTK_STYLE_PROVIDER(p), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + G_GNUC_END_IGNORE_DEPRECATIONS + g_object_unref(p); +#endif +} + +/* ── Main-thread dispatch ────────────────────────────────────────────────── */ + +/* + * Dispatch a function on the GTK main thread, blocking until it completes. + * If already on the main thread, calls the function directly. + * + * Use a GMainContext + GMutex/GCond pair so we can synchronously wait for + * the idle callback to execute, mirroring dispatch_sync(main_queue, ...). + * + * NOTE: only called post-activation. Pre-activation calls go through el_defer(). + */ + +typedef struct { + void (*fn)(void*); + void* data; + GMutex mutex; + GCond cond; + int done; +} ElSyncCall; + +static gboolean _el_sync_dispatch(gpointer user_data) { + ElSyncCall* call = (ElSyncCall*)user_data; + call->fn(call->data); + g_mutex_lock(&call->mutex); + call->done = 1; + g_cond_signal(&call->cond); + g_mutex_unlock(&call->mutex); + return G_SOURCE_REMOVE; +} + +static void el_gtk4_sync_main(void (*fn)(void*), void* data) { + /* Check if we're already on the thread running the default main context. */ + GMainContext* main_ctx = g_main_context_default(); + if (g_main_context_is_owner(main_ctx)) { + fn(data); + return; + } + ElSyncCall call; + call.fn = fn; + call.data = data; + call.done = 0; + g_mutex_init(&call.mutex); + g_cond_init(&call.cond); + + g_main_context_invoke(main_ctx, _el_sync_dispatch, &call); + + g_mutex_lock(&call.mutex); + while (!call.done) { + g_cond_wait(&call.cond, &call.mutex); + } + g_mutex_unlock(&call.mutex); + g_mutex_clear(&call.mutex); + g_cond_clear(&call.cond); +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ + +/* + * El callback functions have signature (per el_seed.c conventions): + * void fn_name(int64_t handle, int64_t data) + * where data is a (const char*) cast to int64_t — same as AppKit bridge. + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_gtk4_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) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── Signal callbacks ────────────────────────────────────────────────────── */ + +/* Slot index stored as widget data so signal handlers can look it up. */ +#define EL_SLOT_KEY "el-slot" + +static void _el_on_button_clicked(GtkButton* btn, gpointer user_data) { + (void)btn; + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_click) { + el_gtk4_invoke_cb(w->cb_click, slot, ""); + } +} + +static void _el_on_entry_changed(GtkEditable* editable, gpointer user_data) { + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_change) { + const char* text = gtk_editable_get_text(editable); + el_gtk4_invoke_cb(w->cb_change, slot, text ? text : ""); + } +} + +static void _el_on_entry_activate(GtkEntry* entry, gpointer user_data) { + (void)entry; + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (w && w->cb_click) { + const char* text = gtk_editable_get_text(GTK_EDITABLE(entry)); + el_gtk4_invoke_cb(w->cb_click, slot, text ? text : ""); + } +} + +/* GtkTextBuffer "changed" signal for textarea. */ +static void _el_on_textbuffer_changed(GtkTextBuffer* buf, gpointer user_data) { + int64_t slot = (int64_t)(intptr_t)user_data; + ElWidget* w = el_widget_get(slot); + if (!w || !w->cb_change) return; + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + char* text = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + el_gtk4_invoke_cb(w->cb_change, slot, text ? text : ""); + g_free(text); +} + +/* + * _el_on_close_request — prevent spurious window-close events on macOS. + * + * On macOS, the GTK4 Quartz backend sends windowShouldClose: messages to + * GtkApplicationWindows when the app is launched from a terminal or when + * NSApplication sends automatic hide/close notifications. These arrive as + * close-request signals and would cause the app to exit prematurely. + * + * Returning TRUE from close-request tells GTK to NOT destroy the window. + * This is correct default behavior for el apps — windows stay open until + * the el program explicitly destroys them or the process exits. + * + * If the el program needs user-closeable windows, it can connect its own + * close-request handler (registered after this one) that returns FALSE to + * allow the close. + */ +static gboolean _el_on_close_request(GtkWindow* window, gpointer user_data) { + (void)window; (void)user_data; + /* Return TRUE: prevent the default close/destroy action. */ + return TRUE; +} + +/* ── GtkApplication activate callback ───────────────────────────────────── */ + +static void _el_app_activate(GtkApplication* app, gpointer user_data) { + /* + * Replay all deferred widget operations. These are the window/widget + * creation calls made during the el boot sequence before native_run_loop(). + * The display is now initialized, so GTK APIs are safe. + * + * g_application_hold() prevents the GApplication from auto-quitting when + * there are temporarily no open windows (e.g., during the replay phase + * before gtk_window_present is called on the first window). + */ + (void)user_data; + /* + * Hold the GApplication during replay to prevent auto-quit if there are + * temporarily no windows between the start of replay and gtk_window_present. + */ + g_application_hold(G_APPLICATION(app)); + _el_app_running = 1; + el_deferred_replay(); + g_application_release(G_APPLICATION(app)); +} + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_gtk4_init — initialize GTK4 + GtkApplication. Idempotent. + * Must be called once before any other el_gtk4_* function. + */ +void el_gtk4_init(void) { + static int done = 0; + if (done) return; + done = 1; + + _el_app = gtk_application_new("ai.neuralplatform.el", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(_el_app, "activate", G_CALLBACK(_el_app_activate), NULL); +} + +/* + * el_gtk4_run_loop — start the GTK main loop. Never returns. + * el code calls __native_run_loop() which dispatches here. + * + * On macOS the GTK4 Quartz backend requires g_application_run() on the main + * thread. The el boot sequence runs on the main thread, so this call is always + * from the main thread. The deferred queue mechanism handles the fact that all + * widget creation occurred before this call. + */ +void el_gtk4_run_loop(void) { + if (!_el_app) el_gtk4_init(); + int status = g_application_run(G_APPLICATION(_el_app), 0, NULL); + (void)status; +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +typedef struct { + const char* title; + int width, height, min_width, min_height; + int64_t slot; /* pre-allocated slot; _main installs widget here */ +} _ElWindowCreateArgs; + +static void _el_window_create_main(void* vp) { + _ElWindowCreateArgs* a = (_ElWindowCreateArgs*)vp; + GtkWidget* win = gtk_application_window_new(_el_app); + gtk_window_set_title(GTK_WINDOW(win), a->title ? a->title : ""); + gtk_window_set_default_size(GTK_WINDOW(win), a->width, a->height); + + /* Set minimum size via content-area size hint. */ + if (a->min_width > 0 || a->min_height > 0) { + gtk_widget_set_size_request(win, a->min_width, a->min_height); + } + + /* Use a vertical GtkBox as the root content widget so children stack + * vertically by default — mirrors the NSStackView root in AppKit. */ + GtkWidget* root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_window_set_child(GTK_WINDOW(win), root); + + if (a->slot >= 0) { + el_widget_install(a->slot, win); + g_object_unref(win); /* el_widget_install took a ref */ + } + /* Connect close-request handler to prevent macOS GTK4 Quartz auto-close. */ + g_signal_connect(win, "close-request", + G_CALLBACK(_el_on_close_request), NULL); +} + +int64_t el_gtk4_window_create(const char* title, int width, int height, + int min_width, int min_height) { + int64_t slot = el_widget_prealloc(EL_WIDGET_WINDOW); + if (!_el_app_running) { + _ElWindowCreateArgs* a = malloc(sizeof(_ElWindowCreateArgs)); + a->title = title ? strdup(title) : NULL; + a->width = width; + a->height = height; + a->min_width = min_width; + a->min_height = min_height; + a->slot = slot; + el_defer(_el_window_create_main, a); + } else { + _ElWindowCreateArgs a = { title, width, height, min_width, min_height, slot }; + el_gtk4_sync_main(_el_window_create_main, &a); + } + return slot; +} + +typedef struct { int64_t handle; } _ElHandleArgs; + +static void _el_window_show_main(void* vp) { + _ElHandleArgs* a = (_ElHandleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind != EL_WIDGET_WINDOW || !w->widget) return; + gtk_window_present(GTK_WINDOW(w->widget)); +} + +void el_gtk4_window_show(int64_t handle) { + if (!_el_app_running) { + _ElHandleArgs* a = malloc(sizeof(_ElHandleArgs)); + a->handle = handle; + el_defer(_el_window_show_main, a); + } else { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_window_show_main, &a); + } +} + +typedef struct { int64_t handle; char* title; } _ElSetTitleArgs; + +static void _el_window_set_title_main(void* vp) { + _ElSetTitleArgs* a = (_ElSetTitleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || w->kind != EL_WIDGET_WINDOW || !w->widget) return; + gtk_window_set_title(GTK_WINDOW(w->widget), a->title ? a->title : ""); +} + +void el_gtk4_window_set_title(int64_t handle, const char* title) { + if (!_el_app_running) { + _ElSetTitleArgs* a = malloc(sizeof(_ElSetTitleArgs)); + a->handle = handle; + a->title = title ? strdup(title) : NULL; + el_defer(_el_window_set_title_main, a); + } else { + _ElSetTitleArgs a = { handle, (char*)title }; + el_gtk4_sync_main(_el_window_set_title_main, &a); + } +} + +/* ── Layout containers ───────────────────────────────────────────────────── */ + +typedef struct { + GtkOrientation orient; + int spacing; + int64_t slot; +} _ElStackCreateArgs; + +static void _el_stack_create_main(void* vp) { + _ElStackCreateArgs* a = (_ElStackCreateArgs*)vp; + GtkWidget* box = gtk_box_new(a->orient, a->spacing); + if (a->slot >= 0) { + el_widget_install(a->slot, box); + g_object_unref(box); + } +} + +int64_t el_gtk4_vstack_create(int spacing) { + int64_t slot = el_widget_prealloc(EL_WIDGET_VSTACK); + if (!_el_app_running) { + _ElStackCreateArgs* a = malloc(sizeof(_ElStackCreateArgs)); + a->orient = GTK_ORIENTATION_VERTICAL; + a->spacing = spacing; + a->slot = slot; + el_defer(_el_stack_create_main, a); + } else { + _ElStackCreateArgs a = { GTK_ORIENTATION_VERTICAL, spacing, slot }; + el_gtk4_sync_main(_el_stack_create_main, &a); + } + return slot; +} + +int64_t el_gtk4_hstack_create(int spacing) { + int64_t slot = el_widget_prealloc(EL_WIDGET_HSTACK); + if (!_el_app_running) { + _ElStackCreateArgs* a = malloc(sizeof(_ElStackCreateArgs)); + a->orient = GTK_ORIENTATION_HORIZONTAL; + a->spacing = spacing; + a->slot = slot; + el_defer(_el_stack_create_main, a); + } else { + _ElStackCreateArgs a = { GTK_ORIENTATION_HORIZONTAL, spacing, slot }; + el_gtk4_sync_main(_el_stack_create_main, &a); + } + return slot; +} + +typedef struct { int64_t slot; } _ElSlotArgs; + +static void _el_zstack_create_main(void* vp) { + _ElSlotArgs* a = (_ElSlotArgs*)vp; + GtkWidget* overlay = gtk_overlay_new(); + if (a->slot >= 0) { el_widget_install(a->slot, overlay); g_object_unref(overlay); } +} + +int64_t el_gtk4_zstack_create(void) { + int64_t slot = el_widget_prealloc(EL_WIDGET_ZSTACK); + if (!_el_app_running) { + _ElSlotArgs* a = malloc(sizeof(_ElSlotArgs)); a->slot = slot; + el_defer(_el_zstack_create_main, a); + } else { + _ElSlotArgs a = { slot }; + el_gtk4_sync_main(_el_zstack_create_main, &a); + } + return slot; +} + +static void _el_scroll_create_main(void* vp) { + _ElSlotArgs* a = (_ElSlotArgs*)vp; + GtkWidget* sw = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + if (a->slot >= 0) { el_widget_install(a->slot, sw); g_object_unref(sw); } +} + +int64_t el_gtk4_scroll_create(void) { + int64_t slot = el_widget_prealloc(EL_WIDGET_SCROLL); + if (!_el_app_running) { + _ElSlotArgs* a = malloc(sizeof(_ElSlotArgs)); a->slot = slot; + el_defer(_el_scroll_create_main, a); + } else { + _ElSlotArgs a = { slot }; + el_gtk4_sync_main(_el_scroll_create_main, &a); + } + return slot; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +typedef struct { char* text; int64_t slot; } _ElTextSlotArgs; + +static void _el_label_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)vp; + GtkWidget* lbl = gtk_label_new(a->text ? a->text : ""); + gtk_label_set_xalign(GTK_LABEL(lbl), 0.0f); + gtk_label_set_wrap(GTK_LABEL(lbl), FALSE); + if (a->slot >= 0) { el_widget_install(a->slot, lbl); g_object_unref(lbl); } +} + +int64_t el_gtk4_label_create(const char* text) { + int64_t slot = el_widget_prealloc(EL_WIDGET_LABEL); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = text ? strdup(text) : NULL; a->slot = slot; + el_defer(_el_label_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)text, slot }; + el_gtk4_sync_main(_el_label_create_main, &a); + } + return slot; +} + +static void _el_button_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)vp; + GtkWidget* btn = gtk_button_new_with_label(a->text ? a->text : ""); + if (a->slot >= 0) { + el_widget_install(a->slot, btn); + g_object_unref(btn); + g_signal_connect(btn, "clicked", + G_CALLBACK(_el_on_button_clicked), + (gpointer)(intptr_t)a->slot); + } +} + +int64_t el_gtk4_button_create(const char* label) { + int64_t slot = el_widget_prealloc(EL_WIDGET_BUTTON); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = label ? strdup(label) : NULL; a->slot = slot; + el_defer(_el_button_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)label, slot }; + el_gtk4_sync_main(_el_button_create_main, &a); + } + return slot; +} + +static void _el_text_field_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)vp; + GtkWidget* entry = gtk_entry_new(); + if (a->text && *a->text) { + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), a->text); + } + if (a->slot >= 0) { + el_widget_install(a->slot, entry); + g_object_unref(entry); + gpointer slot_ptr = (gpointer)(intptr_t)a->slot; + g_signal_connect(entry, "changed", + G_CALLBACK(_el_on_entry_changed), slot_ptr); + g_signal_connect(entry, "activate", + G_CALLBACK(_el_on_entry_activate), slot_ptr); + } +} + +int64_t el_gtk4_text_field_create(const char* placeholder) { + int64_t slot = el_widget_prealloc(EL_WIDGET_TEXTFIELD); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = placeholder ? strdup(placeholder) : NULL; a->slot = slot; + el_defer(_el_text_field_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)placeholder, slot }; + el_gtk4_sync_main(_el_text_field_create_main, &a); + } + return slot; +} + +static void _el_text_area_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)vp; + GtkWidget* sw = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_vexpand(sw, TRUE); + + GtkWidget* tv = gtk_text_view_new(); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(tv), GTK_WRAP_WORD_CHAR); + gtk_widget_set_hexpand(tv, TRUE); + gtk_widget_set_vexpand(tv, TRUE); + + /* GTK4 GtkTextView has no native placeholder; set accessible description. */ + if (a->text && *a->text) { + gtk_accessible_update_property(GTK_ACCESSIBLE(tv), + GTK_ACCESSIBLE_PROPERTY_PLACEHOLDER, a->text, -1); + } + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), tv); + + if (a->slot >= 0) { + el_widget_install(a->slot, sw); + g_object_unref(sw); + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + g_signal_connect(buf, "changed", + G_CALLBACK(_el_on_textbuffer_changed), + (gpointer)(intptr_t)a->slot); + } +} + +int64_t el_gtk4_text_area_create(const char* placeholder) { + int64_t slot = el_widget_prealloc(EL_WIDGET_TEXTAREA); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = placeholder ? strdup(placeholder) : NULL; a->slot = slot; + el_defer(_el_text_area_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)placeholder, slot }; + el_gtk4_sync_main(_el_text_area_create_main, &a); + } + return slot; +} + +static void _el_image_create_main(void* vp) { + _ElTextSlotArgs* a = (_ElTextSlotArgs*)vp; + GtkWidget* img = NULL; + if (a->text && *a->text) { + /* Try as a filesystem path first. */ + if (g_file_test(a->text, G_FILE_TEST_EXISTS)) { + img = gtk_picture_new_for_filename(a->text); + } + if (!img) { + /* Fall back to icon name. */ + img = gtk_image_new_from_icon_name(a->text); + } + } + if (!img) { + img = gtk_picture_new(); + } + if (a->slot >= 0) { el_widget_install(a->slot, img); g_object_unref(img); } +} + +int64_t el_gtk4_image_create(const char* path_or_name) { + int64_t slot = el_widget_prealloc(EL_WIDGET_IMAGE); + if (!_el_app_running) { + _ElTextSlotArgs* a = malloc(sizeof(_ElTextSlotArgs)); + a->text = path_or_name ? strdup(path_or_name) : NULL; a->slot = slot; + el_defer(_el_image_create_main, a); + } else { + _ElTextSlotArgs a = { (char*)path_or_name, slot }; + el_gtk4_sync_main(_el_image_create_main, &a); + } + return slot; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +typedef struct { int64_t handle; char* text; } _ElWidgetTextArgs; + +static void _el_widget_set_text_main(void* vp) { + _ElWidgetTextArgs* a = (_ElWidgetTextArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget) return; + const char* s = a->text ? a->text : ""; + switch (w->kind) { + case EL_WIDGET_LABEL: + gtk_label_set_text(GTK_LABEL(w->widget), s); + break; + case EL_WIDGET_TEXTFIELD: + gtk_editable_set_text(GTK_EDITABLE(w->widget), s); + break; + case EL_WIDGET_BUTTON: + gtk_button_set_label(GTK_BUTTON(w->widget), s); + break; + case EL_WIDGET_TEXTAREA: { + GtkWidget* tv = gtk_scrolled_window_get_child( + GTK_SCROLLED_WINDOW(w->widget)); + if (tv && GTK_IS_TEXT_VIEW(tv)) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + gtk_text_buffer_set_text(buf, s, -1); + } + break; + } + case EL_WIDGET_WINDOW: + gtk_window_set_title(GTK_WINDOW(w->widget), s); + break; + default: break; + } +} + +void el_gtk4_widget_set_text(int64_t handle, const char* text) { + if (!_el_app_running) { + _ElWidgetTextArgs* a = malloc(sizeof(_ElWidgetTextArgs)); + a->handle = handle; a->text = text ? strdup(text) : NULL; + el_defer(_el_widget_set_text_main, a); + } else { + _ElWidgetTextArgs a = { handle, (char*)text }; + el_gtk4_sync_main(_el_widget_set_text_main, &a); + } +} + +typedef struct { int64_t handle; const char* result; } _ElWidgetGetTextArgs; + +static void _el_widget_get_text_main(void* vp) { + _ElWidgetGetTextArgs* a = (_ElWidgetGetTextArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget) { a->result = ""; return; } + switch (w->kind) { + case EL_WIDGET_LABEL: + a->result = gtk_label_get_text(GTK_LABEL(w->widget)); + break; + case EL_WIDGET_TEXTFIELD: + a->result = gtk_editable_get_text(GTK_EDITABLE(w->widget)); + break; + case EL_WIDGET_BUTTON: + a->result = gtk_button_get_label(GTK_BUTTON(w->widget)); + break; + case EL_WIDGET_TEXTAREA: { + GtkWidget* tv = gtk_scrolled_window_get_child( + GTK_SCROLLED_WINDOW(w->widget)); + if (tv && GTK_IS_TEXT_VIEW(tv)) { + GtkTextBuffer* buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(tv)); + GtkTextIter start, end; + gtk_text_buffer_get_bounds(buf, &start, &end); + /* Returns g_malloc'd string — caller treats it as short-lived. */ + a->result = gtk_text_buffer_get_text(buf, &start, &end, FALSE); + } else { + a->result = ""; + } + break; + } + default: + a->result = ""; + break; + } +} + +const char* el_gtk4_widget_get_text(int64_t handle) { + _ElWidgetGetTextArgs a = { handle, "" }; + el_gtk4_sync_main(_el_widget_get_text_main, &a); + return a.result ? strdup(a.result) : strdup(""); +} + +typedef struct { + int64_t handle; + float r, g, b, a; +} _ElColorArgs; + +static void _el_widget_set_color_main(void* vp) { + _ElColorArgs* a = (_ElColorArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget) return; + char css[128]; + snprintf(css, sizeof(css), "color: rgba(%d,%d,%d,%.3f);", + (int)(a->r * 255.0f), (int)(a->g * 255.0f), + (int)(a->b * 255.0f), (double)a->a); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_color(int64_t handle, float r, float g, float b, float a) { + if (!_el_app_running) { + _ElColorArgs* args = malloc(sizeof(_ElColorArgs)); + *args = ((_ElColorArgs){ handle, r, g, b, a }); + el_defer(_el_widget_set_color_main, args); + } else { + _ElColorArgs args = { handle, r, g, b, a }; + el_gtk4_sync_main(_el_widget_set_color_main, &args); + } +} + +static void _el_widget_set_bg_color_main(void* vp) { + _ElColorArgs* a = (_ElColorArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget || w->kind == EL_WIDGET_WINDOW) return; + char css[128]; + snprintf(css, sizeof(css), "background-color: rgba(%d,%d,%d,%.3f);", + (int)(a->r * 255.0f), (int)(a->g * 255.0f), + (int)(a->b * 255.0f), (double)a->a); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + if (!_el_app_running) { + _ElColorArgs* args = malloc(sizeof(_ElColorArgs)); + *args = ((_ElColorArgs){ handle, r, g, b, a }); + el_defer(_el_widget_set_bg_color_main, args); + } else { + _ElColorArgs args = { handle, r, g, b, a }; + el_gtk4_sync_main(_el_widget_set_bg_color_main, &args); + } +} + +typedef struct { + int64_t handle; + char* family; + int size; + int bold; +} _ElFontArgs; + +static void _el_widget_set_font_main(void* vp) { + _ElFontArgs* a = (_ElFontArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget) return; + char css[256]; + const char* fam = (a->family && *a->family && strcmp(a->family, "system") != 0) + ? a->family : "sans-serif"; + snprintf(css, sizeof(css), + "font-family: %s; font-size: %dpx; font-weight: %s;", + fam, a->size, a->bold ? "bold" : "normal"); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_font(int64_t handle, const char* family, int size, int bold) { + if (!_el_app_running) { + _ElFontArgs* a = malloc(sizeof(_ElFontArgs)); + a->handle = handle; a->family = family ? strdup(family) : NULL; + a->size = size; a->bold = bold; + el_defer(_el_widget_set_font_main, a); + } else { + _ElFontArgs a = { handle, (char*)family, size, bold }; + el_gtk4_sync_main(_el_widget_set_font_main, &a); + } +} + +typedef struct { int64_t handle; int top, right, bottom, left; } _ElPaddingArgs; + +static void _el_widget_set_padding_main(void* vp) { + _ElPaddingArgs* a = (_ElPaddingArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget) return; + char css[128]; + snprintf(css, sizeof(css), "padding: %dpx %dpx %dpx %dpx;", + a->top, a->right, a->bottom, a->left); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + if (!_el_app_running) { + _ElPaddingArgs* a = malloc(sizeof(_ElPaddingArgs)); + *a = ((_ElPaddingArgs){ handle, top, right, bottom, left }); + el_defer(_el_widget_set_padding_main, a); + } else { + _ElPaddingArgs a = { handle, top, right, bottom, left }; + el_gtk4_sync_main(_el_widget_set_padding_main, &a); + } +} + +typedef struct { int64_t handle; int value; } _ElIntPropArgs; + +static void _el_widget_set_width_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget || w->kind == EL_WIDGET_WINDOW) return; + gint cur_h = -1; + gtk_widget_get_size_request(w->widget, NULL, &cur_h); + gtk_widget_set_size_request(w->widget, a->value, cur_h); +} + +void el_gtk4_widget_set_width(int64_t handle, int width) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = width; + el_defer(_el_widget_set_width_main, a); + } else { + _ElIntPropArgs a = { handle, width }; + el_gtk4_sync_main(_el_widget_set_width_main, &a); + } +} + +static void _el_widget_set_height_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget || w->kind == EL_WIDGET_WINDOW) return; + gint cur_w = -1; + gtk_widget_get_size_request(w->widget, &cur_w, NULL); + gtk_widget_set_size_request(w->widget, cur_w, a->value); +} + +void el_gtk4_widget_set_height(int64_t handle, int height) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = height; + el_defer(_el_widget_set_height_main, a); + } else { + _ElIntPropArgs a = { handle, height }; + el_gtk4_sync_main(_el_widget_set_height_main, &a); + } +} + +static void _el_widget_set_flex_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget || w->kind == EL_WIDGET_WINDOW) return; + gboolean expand = (a->value > 0) ? TRUE : FALSE; + gtk_widget_set_hexpand(w->widget, expand); + gtk_widget_set_vexpand(w->widget, expand); +} + +void el_gtk4_widget_set_flex(int64_t handle, int flex) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = flex; + el_defer(_el_widget_set_flex_main, a); + } else { + _ElIntPropArgs a = { handle, flex }; + el_gtk4_sync_main(_el_widget_set_flex_main, &a); + } +} + +static void _el_widget_set_corner_radius_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget || w->kind == EL_WIDGET_WINDOW) return; + char css[64]; + snprintf(css, sizeof(css), "border-radius: %dpx;", a->value); + apply_css(w->widget, css); +} + +void el_gtk4_widget_set_corner_radius(int64_t handle, int radius) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = radius; + el_defer(_el_widget_set_corner_radius_main, a); + } else { + _ElIntPropArgs a = { handle, radius }; + el_gtk4_sync_main(_el_widget_set_corner_radius_main, &a); + } +} + +static void _el_widget_set_disabled_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget) return; + gtk_widget_set_sensitive(w->widget, !a->value); +} + +void el_gtk4_widget_set_disabled(int64_t handle, int disabled) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = disabled; + el_defer(_el_widget_set_disabled_main, a); + } else { + _ElIntPropArgs a = { handle, disabled }; + el_gtk4_sync_main(_el_widget_set_disabled_main, &a); + } +} + +static void _el_widget_set_hidden_main(void* vp) { + _ElIntPropArgs* a = (_ElIntPropArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget || w->kind == EL_WIDGET_WINDOW) return; + gtk_widget_set_visible(w->widget, !a->value); +} + +void el_gtk4_widget_set_hidden(int64_t handle, int hidden) { + if (!_el_app_running) { + _ElIntPropArgs* a = malloc(sizeof(_ElIntPropArgs)); + a->handle = handle; a->value = hidden; + el_defer(_el_widget_set_hidden_main, a); + } else { + _ElIntPropArgs a = { handle, hidden }; + el_gtk4_sync_main(_el_widget_set_hidden_main, &a); + } +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +typedef struct { int64_t parent; int64_t child; } _ElParentChildArgs; + +static void _el_widget_add_child_main(void* vp) { + _ElParentChildArgs* a = (_ElParentChildArgs*)vp; + ElWidget* pw = el_widget_get(a->parent); + ElWidget* cw = el_widget_get(a->child); + if (!pw || !cw || !pw->widget || !cw->widget) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* cannot add a window as child */ + + GtkWidget* child_widget = cw->widget; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + /* Append to the root VBox that window_create installs as child. */ + GtkWidget* root = gtk_window_get_child(GTK_WINDOW(pw->widget)); + if (root && GTK_IS_BOX(root)) { + gtk_box_append(GTK_BOX(root), child_widget); + } else { + /* Fallback: replace root child. */ + gtk_window_set_child(GTK_WINDOW(pw->widget), child_widget); + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + gtk_box_append(GTK_BOX(pw->widget), child_widget); + break; + case EL_WIDGET_ZSTACK: + /* First child becomes the base; subsequent children are overlays. */ + if (!gtk_overlay_get_child(GTK_OVERLAY(pw->widget))) { + gtk_overlay_set_child(GTK_OVERLAY(pw->widget), child_widget); + } else { + gtk_overlay_add_overlay(GTK_OVERLAY(pw->widget), child_widget); + } + break; + case EL_WIDGET_SCROLL: + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(pw->widget), + child_widget); + break; + default: + /* Generic fallback — shouldn't be reached for standard widget kinds. */ + break; + } +} + +void el_gtk4_widget_add_child(int64_t parent, int64_t child) { + if (!_el_app_running) { + _ElParentChildArgs* a = malloc(sizeof(_ElParentChildArgs)); + a->parent = parent; a->child = child; + el_defer(_el_widget_add_child_main, a); + } else { + _ElParentChildArgs a = { parent, child }; + el_gtk4_sync_main(_el_widget_add_child_main, &a); + } +} + +static void _el_widget_remove_child_main(void* vp) { + _ElParentChildArgs* a = (_ElParentChildArgs*)vp; + ElWidget* pw = el_widget_get(a->parent); + ElWidget* cw = el_widget_get(a->child); + if (!pw || !cw || !pw->widget || !cw->widget) return; + if (cw->kind == EL_WIDGET_WINDOW) return; + + GtkWidget* child_widget = cw->widget; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + GtkWidget* root = gtk_window_get_child(GTK_WINDOW(pw->widget)); + if (root && GTK_IS_BOX(root)) { + gtk_box_remove(GTK_BOX(root), child_widget); + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + gtk_box_remove(GTK_BOX(pw->widget), child_widget); + break; + case EL_WIDGET_ZSTACK: + gtk_overlay_remove_overlay(GTK_OVERLAY(pw->widget), child_widget); + break; + case EL_WIDGET_SCROLL: + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(pw->widget), NULL); + break; + default: + break; + } +} + +void el_gtk4_widget_remove_child(int64_t parent, int64_t child) { + if (!_el_app_running) { + _ElParentChildArgs* a = malloc(sizeof(_ElParentChildArgs)); + a->parent = parent; a->child = child; + el_defer(_el_widget_remove_child_main, a); + } else { + _ElParentChildArgs a = { parent, child }; + el_gtk4_sync_main(_el_widget_remove_child_main, &a); + } +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +/* + * on_click / on_change / on_submit just store the function name string. + * The actual g_signal_connect calls happen at widget creation time. + * Here we update the stored callback so the already-connected signal handler + * will call the new function name. + * + * These are safe to call pre-activation because they only touch the slot table, + * not GTK objects. We still defer them to ensure ordering with widget creation. + */ + +typedef struct { int64_t handle; char* fn_name; } _ElCbArgs; + +static void _el_widget_on_click_main(void* vp) { + _ElCbArgs* a = (_ElCbArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (a->fn_name && *a->fn_name) ? strdup(a->fn_name) : NULL; +} + +void el_gtk4_widget_on_click(int64_t handle, const char* fn_name) { + if (!_el_app_running) { + _ElCbArgs* a = malloc(sizeof(_ElCbArgs)); + a->handle = handle; a->fn_name = fn_name ? strdup(fn_name) : NULL; + el_defer(_el_widget_on_click_main, a); + } else { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); + w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + } +} + +static void _el_widget_on_change_main(void* vp) { + _ElCbArgs* a = (_ElCbArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (a->fn_name && *a->fn_name) ? strdup(a->fn_name) : NULL; +} + +void el_gtk4_widget_on_change(int64_t handle, const char* fn_name) { + if (!_el_app_running) { + _ElCbArgs* a = malloc(sizeof(_ElCbArgs)); + a->handle = handle; a->fn_name = fn_name ? strdup(fn_name) : NULL; + el_defer(_el_widget_on_change_main, a); + } else { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_change); + w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL; + } +} + +void el_gtk4_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, "submit" = Enter key activate → stored in cb_click. */ + el_gtk4_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +static void _el_widget_destroy_main(void* vp) { + _ElHandleArgs* a = (_ElHandleArgs*)vp; + ElWidget* w = el_widget_get(a->handle); + if (!w || !w->widget) return; + if (w->kind == EL_WIDGET_WINDOW) { + gtk_window_destroy(GTK_WINDOW(w->widget)); + } else { + /* Unparent the widget before freeing the slot. */ + GtkWidget* parent = gtk_widget_get_parent(w->widget); + if (parent) { + if (GTK_IS_BOX(parent)) { + gtk_box_remove(GTK_BOX(parent), w->widget); + } else if (GTK_IS_OVERLAY(parent)) { + gtk_overlay_remove_overlay(GTK_OVERLAY(parent), w->widget); + } else if (GTK_IS_SCROLLED_WINDOW(parent)) { + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(parent), NULL); + } + /* gtk_window_get_child case: the parent is a GtkBox inside a window */ + } + } + el_widget_free(a->handle); +} + +void el_gtk4_widget_destroy(int64_t handle) { + if (!_el_app_running) { + _ElHandleArgs* a = malloc(sizeof(_ElHandleArgs)); + a->handle = handle; + el_defer(_el_widget_destroy_main, a); + } else { + _ElHandleArgs a = { handle }; + el_gtk4_sync_main(_el_widget_destroy_main, &a); + } +} + +#endif /* EL_TARGET_LINUX */ diff --git a/lang/el-compiler/runtime/el_lvgl.c b/lang/el-compiler/runtime/el_lvgl.c new file mode 100644 index 0000000..4c3a318 --- /dev/null +++ b/lang/el-compiler/runtime/el_lvgl.c @@ -0,0 +1,844 @@ +/* + * el_lvgl.c — LVGL v9 backend for the el native widget system. + * + * This file implements the microcontroller/embedded widget layer that el_seed.c + * calls through to when EL_TARGET_LVGL is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_lvgl_* C functions defined here + * → lv_obj_t widgets via LVGL v9 + * + * Target platforms: ESP32, STM32, industrial panels, any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles: every widget is assigned an int64_t slot index into + * g_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is reserved; -1 = invalid handle. + * + * Window model: on embedded there is one screen. __window_create configures + * lv_scr_act() as the root container. __window_show is a no-op (the screen + * is always visible). __native_run_loop calls lv_task_handler() in a tight + * loop — on RTOS this runs inside a dedicated task; on bare metal it IS the + * main loop. The host application is responsible for initialising the display + * driver and calling lv_tick_inc() before calling __native_run_loop. + * + * Callback dispatch: + * When EL_LVGL_NO_DLSYM is NOT defined (hosted Linux, testing): + * dlsym(RTLD_DEFAULT, fn_name) resolves the El function symbol at runtime. + * When EL_LVGL_NO_DLSYM IS defined (bare-metal ESP32/STM32): + * The caller must provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * which maps function names to function pointers via a compile-time table. + * + * Font mapping: LVGL v9 ships Montserrat in discrete sizes. __widget_set_font + * maps the requested point size to the nearest available Montserrat variant. + * Bold is approximated by stepping up two sizes (no separate bold face in the + * default LVGL font set). Define EL_LVGL_CUSTOM_FONT to override font_select(). + * + * Compile (hosted test build): + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a / lvgl source tree. + * + * Compile (bare-metal, no dynamic linker): + * arm-none-eabi-gcc -DEL_TARGET_LVGL -DEL_LVGL_NO_DLSYM \ + * -I./lvgl el_lvgl.c -c -o el_lvgl.o + */ + +#ifdef EL_TARGET_LVGL + +#include "lvgl/lvgl.h" +#include +#include +#include +#include + +#ifndef EL_LVGL_NO_DLSYM +#include +#endif + +#include "el_runtime.h" + +/* ── Callback dispatch macro ─────────────────────────────────────────────── */ + +#ifdef EL_LVGL_NO_DLSYM +/* + * Bare-metal path. The application provides this function: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * It maps string names → function pointers, typically via a switch on a hash + * or a sorted table of {name, fn_ptr} pairs generated by elc. + */ +extern el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); +#define EL_LVGL_CALL(fn_name, a, b) el_lvgl_dispatch((fn_name), (a), (b)) +#else +/* + * Hosted path. dlsym resolves the El symbol at call time. + * We use a compound-statement expression (GCC/Clang extension) to avoid + * executing dlsym more than once per call. + */ +#define EL_LVGL_CALL(fn_name, a, b) \ + ({ \ + typedef el_val_t (*_el_fn_t)(el_val_t, el_val_t); \ + _el_fn_t _fn = (_el_fn_t)(uintptr_t)dlsym(RTLD_DEFAULT, (fn_name)); \ + _fn ? _fn((a), (b)) : (el_val_t)0; \ + }) +#endif + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_LVGL_MAX_WIDGETS 4096 + +/* + * Widget kinds — mirrors AppKit/GTK4 backends so future tooling can stay + * consistent across all targets. + */ +typedef enum { + EL_LVGL_FREE = 0, + EL_LVGL_WINDOW = 1, + EL_LVGL_VSTACK = 2, + EL_LVGL_HSTACK = 3, + EL_LVGL_ZSTACK = 4, + EL_LVGL_SCROLL = 5, + EL_LVGL_LABEL = 6, + EL_LVGL_BUTTON = 7, /* lv_btn_create; inner label at slot_btn_label */ + EL_LVGL_TEXTFIELD = 8, /* lv_textarea, one-line */ + EL_LVGL_TEXTAREA = 9, /* lv_textarea, multiline */ + EL_LVGL_IMAGE = 10, + EL_LVGL_DIVIDER = 11, + EL_LVGL_SPACER = 12, +} ElLvglKind; + +/* + * Per-slot state. Callback names are stored inline (256 bytes each) to avoid + * heap allocation on targets with no malloc or fragmented heaps. + */ +typedef struct { + ElLvglKind kind; + lv_obj_t *obj; /* primary LVGL object */ + lv_obj_t *btn_label; /* for EL_LVGL_BUTTON: inner lv_label child */ + char cb_click[256]; + char cb_change[256]; + char cb_submit[256]; +} ElLvglWidget; + +static ElLvglWidget g_widgets[EL_LVGL_MAX_WIDGETS]; + +/* ── Slot helpers ─────────────────────────────────────────────────────────── */ + +static int64_t lvgl_slot_alloc(ElLvglKind kind, lv_obj_t *obj) { + for (int i = 1; i < EL_LVGL_MAX_WIDGETS; i++) { + if (g_widgets[i].kind == EL_LVGL_FREE) { + g_widgets[i].kind = kind; + g_widgets[i].obj = obj; + g_widgets[i].btn_label = NULL; + g_widgets[i].cb_click[0] = '\0'; + g_widgets[i].cb_change[0] = '\0'; + g_widgets[i].cb_submit[0] = '\0'; + return (int64_t)i; + } + } + return -1; /* table full */ +} + +static ElLvglWidget *lvgl_slot_get(int64_t handle) { + if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return NULL; + if (g_widgets[handle].kind == EL_LVGL_FREE) return NULL; + return &g_widgets[handle]; +} + +static void lvgl_slot_free(int64_t handle) { + if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return; + ElLvglWidget *w = &g_widgets[handle]; + w->kind = EL_LVGL_FREE; + w->obj = NULL; + w->btn_label = NULL; + w->cb_click[0] = '\0'; + w->cb_change[0] = '\0'; + w->cb_submit[0] = '\0'; +} + +/* ── Font selection ───────────────────────────────────────────────────────── */ +/* + * LVGL ships Montserrat in the following sizes (subset enabled by lv_conf.h): + * 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 + * + * We map the requested size to the nearest available size. Bold is + * approximated by stepping up two sizes (no separate bold face in the default + * font set). Define EL_LVGL_CUSTOM_FONT to replace this function entirely. + */ +#ifndef EL_LVGL_CUSTOM_FONT + +static const lv_font_t *font_select(int size, int bold) { + /* Step up two sizes for bold approximation. */ + if (bold) size += 4; + + /* Clamp to available range. */ + if (size < 8) size = 8; + if (size > 48) size = 48; + + /* Round to nearest even size >= 8. */ + if (size % 2 != 0) size++; + + switch (size) { +#if LV_FONT_MONTSERRAT_8 + case 8: return &lv_font_montserrat_8; +#endif +#if LV_FONT_MONTSERRAT_10 + case 10: return &lv_font_montserrat_10; +#endif +#if LV_FONT_MONTSERRAT_12 + case 12: return &lv_font_montserrat_12; +#endif +#if LV_FONT_MONTSERRAT_14 + case 14: return &lv_font_montserrat_14; +#endif +#if LV_FONT_MONTSERRAT_16 + case 16: return &lv_font_montserrat_16; +#endif +#if LV_FONT_MONTSERRAT_18 + case 18: return &lv_font_montserrat_18; +#endif +#if LV_FONT_MONTSERRAT_20 + case 20: return &lv_font_montserrat_20; +#endif +#if LV_FONT_MONTSERRAT_22 + case 22: return &lv_font_montserrat_22; +#endif +#if LV_FONT_MONTSERRAT_24 + case 24: return &lv_font_montserrat_24; +#endif +#if LV_FONT_MONTSERRAT_26 + case 26: return &lv_font_montserrat_26; +#endif +#if LV_FONT_MONTSERRAT_28 + case 28: return &lv_font_montserrat_28; +#endif +#if LV_FONT_MONTSERRAT_30 + case 30: return &lv_font_montserrat_30; +#endif +#if LV_FONT_MONTSERRAT_32 + case 32: return &lv_font_montserrat_32; +#endif +#if LV_FONT_MONTSERRAT_34 + case 34: return &lv_font_montserrat_34; +#endif +#if LV_FONT_MONTSERRAT_36 + case 36: return &lv_font_montserrat_36; +#endif +#if LV_FONT_MONTSERRAT_38 + case 38: return &lv_font_montserrat_38; +#endif +#if LV_FONT_MONTSERRAT_40 + case 40: return &lv_font_montserrat_40; +#endif +#if LV_FONT_MONTSERRAT_42 + case 42: return &lv_font_montserrat_42; +#endif +#if LV_FONT_MONTSERRAT_44 + case 44: return &lv_font_montserrat_44; +#endif +#if LV_FONT_MONTSERRAT_46 + case 46: return &lv_font_montserrat_46; +#endif +#if LV_FONT_MONTSERRAT_48 + case 48: return &lv_font_montserrat_48; +#endif + default: + /* + * Requested size is not compiled in. Fall back to the default + * theme font, which is guaranteed to be present. + */ + return LV_FONT_DEFAULT; + } +} + +#endif /* EL_LVGL_CUSTOM_FONT */ + +/* ── Event callback ───────────────────────────────────────────────────────── */ + +/* + * Single LVGL event callback used for all widget events. The user_data is + * the slot index cast to (void*) via intptr_t — avoids heap allocation. + * + * Three event codes are handled: + * LV_EVENT_CLICKED → cb_click (buttons, any tappable widget) + * LV_EVENT_VALUE_CHANGED → cb_change (textarea, checkbox, etc.) + * LV_EVENT_READY → cb_submit (Enter pressed in textarea one-line mode) + */ +static void el_lvgl_event_cb(lv_event_t *e) { + lv_event_code_t code = lv_event_get_code(e); + intptr_t slot = (intptr_t)lv_event_get_user_data(e); + ElLvglWidget *w = lvgl_slot_get((int64_t)slot); + if (!w) return; + + if (code == LV_EVENT_CLICKED && w->cb_click[0]) { + EL_LVGL_CALL(w->cb_click, (el_val_t)slot, (el_val_t)0); + } + + if (code == LV_EVENT_VALUE_CHANGED && w->cb_change[0]) { + /* + * Retrieve current text for textarea/textfield widgets so the handler + * receives the updated value as its second argument. + */ + const char *txt = ""; + lv_obj_t *target = lv_event_get_target(e); + if (w->kind == EL_LVGL_TEXTFIELD || w->kind == EL_LVGL_TEXTAREA) { + txt = lv_textarea_get_text(target); + if (!txt) txt = ""; + } + EL_LVGL_CALL(w->cb_change, (el_val_t)slot, + (el_val_t)(uintptr_t)txt); + } + + if (code == LV_EVENT_READY && w->cb_submit[0]) { + /* LV_EVENT_READY fires when Enter is pressed in a one-line textarea. */ + EL_LVGL_CALL(w->cb_submit, (el_val_t)slot, (el_val_t)0); + } +} + +/* ── Initialisation ───────────────────────────────────────────────────────── */ + +/* + * el_lvgl_init — call lv_init(). The host must have already initialised the + * display driver and input driver before this, or immediately after. Idempotent. + */ +void el_lvgl_init(void) { + static int done = 0; + if (done) return; + done = 1; + lv_init(); +} + +/* + * el_lvgl_run_loop — drive lv_task_handler() indefinitely. + * + * On RTOS: this function should run inside a dedicated FreeRTOS/Zephyr task. + * On bare metal: call this as the last statement of main(). + * + * The 5 ms delay between handler calls matches the LVGL documentation + * recommendation for a ~200 Hz refresh budget. + * + * On hosted Linux (EL_LVGL_SDL or similar), usleep(5000) is used. On RTOS + * targets define EL_LVGL_RTOS_DELAY(ms) to map to vTaskDelay/k_sleep/etc. + */ +void el_lvgl_run_loop(void) { + for (;;) { + lv_task_handler(); +#if defined(EL_LVGL_RTOS_DELAY) + EL_LVGL_RTOS_DELAY(5); +#elif defined(__linux__) || defined(__APPLE__) + { + /* Hosted test build — usleep available. */ +#include + usleep(5000); + } +#endif + /* Bare-metal without a delay macro: the HAL tick increment loop + * is the caller's responsibility. No sleep needed if lv_tick_inc() + * is driven from a hardware timer ISR. */ + } +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +/* + * el_lvgl_window_create — configure lv_scr_act() as a vertical flex container + * and return a slot handle wrapping it. The title is stored for informational + * purposes (e.g., a status bar widget the host might create). Width/height + * are ignored on embedded targets because the screen size is fixed by the + * display driver; they are accepted for API compatibility with other backends. + */ +int64_t el_lvgl_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; + + lv_obj_t *scr = lv_scr_act(); + + /* Configure the active screen as a vertical flex container so that + * widgets added via __widget_add_child stack naturally. */ + lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(scr, LV_PCT(100), LV_PCT(100)); + + /* Store the window title in a user-data string on the screen object + * so host code can retrieve it if it wants to render a title bar. */ + if (title && *title) { + /* lv_obj_set_user_data stores a void* — we cast the string pointer. + * The string must outlive the screen object; for literals this is + * always true. For dynamic titles use el_lvgl_window_set_title. */ + lv_obj_set_user_data(scr, (void *)(uintptr_t)title); + } + + /* Allocate a slot for the screen object. */ + int64_t h = lvgl_slot_alloc(EL_LVGL_WINDOW, scr); + return h; +} + +/* + * el_lvgl_window_show — no-op on embedded. The screen is always visible. + */ +void el_lvgl_window_show(int64_t handle) { + (void)handle; + /* On multi-screen setups, load the screen: */ + /* ElLvglWidget *w = lvgl_slot_get(handle); + * if (w) lv_scr_load(w->obj); */ +} + +/* + * el_lvgl_window_set_title — update the user_data pointer on the screen object. + * On embedded, "title" is typically only used by a custom host title bar. + */ +void el_lvgl_window_set_title(int64_t handle, const char *title) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w || w->kind != EL_LVGL_WINDOW) return; + lv_obj_set_user_data(w->obj, (void *)(uintptr_t)(title ? title : "")); +} + +/* ── Layout containers ────────────────────────────────────────────────────── */ + +/* + * el_lvgl_vstack_create — vertical flex column with inter-item gap = spacing. + */ +int64_t el_lvgl_vstack_create(int spacing) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(obj, (lv_coord_t)spacing, 0); + lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + /* Remove default LVGL border and background so containers are transparent + * by default, matching the AppKit/GTK4 backends. */ + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_VSTACK, obj); +} + +/* + * el_lvgl_hstack_create — horizontal flex row with inter-item gap = spacing. + */ +int64_t el_lvgl_hstack_create(int spacing) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(obj, (lv_coord_t)spacing, 0); + lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_HSTACK, obj); +} + +/* + * el_lvgl_zstack_create — plain container, children positioned absolutely. + * No flex flow is set; callers use lv_obj_set_pos() on children directly, + * or rely on their natural 0,0 origin. + */ +int64_t el_lvgl_zstack_create(void) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_ZSTACK, obj); +} + +/* + * el_lvgl_scroll_create — vertically scrollable container. + */ +int64_t el_lvgl_scroll_create(void) { + lv_obj_t *obj = lv_obj_create(lv_scr_act()); + lv_obj_set_scroll_dir(obj, LV_DIR_VER); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN); + lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + return lvgl_slot_alloc(EL_LVGL_SCROLL, obj); +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +/* + * el_lvgl_label_create — static text label. + */ +int64_t el_lvgl_label_create(const char *text) { + lv_obj_t *obj = lv_label_create(lv_scr_act()); + lv_label_set_text(obj, text ? text : ""); + return lvgl_slot_alloc(EL_LVGL_LABEL, obj); +} + +/* + * el_lvgl_button_create — pressable button with a child label. + * + * LVGL buttons are containers; text is placed in an inner lv_label child. + * We store the child label pointer in btn_label so set_text / get_text can + * reach it without searching the object tree at runtime. + */ +int64_t el_lvgl_button_create(const char *label) { + lv_obj_t *btn = lv_btn_create(lv_scr_act()); + lv_obj_t *lbl = lv_label_create(btn); + lv_label_set_text(lbl, label ? label : ""); + lv_obj_center(lbl); + + int64_t h = lvgl_slot_alloc(EL_LVGL_BUTTON, btn); + if (h >= 0) { + g_widgets[h].btn_label = lbl; + /* Register click callback immediately so button responds when a + * callback name is registered later via __widget_on_click. */ + lv_obj_add_event_cb(btn, el_lvgl_event_cb, LV_EVENT_CLICKED, + (void *)(intptr_t)h); + } + return h; +} + +/* + * el_lvgl_text_field_create — single-line text input. + */ +int64_t el_lvgl_text_field_create(const char *placeholder) { + lv_obj_t *obj = lv_textarea_create(lv_scr_act()); + lv_textarea_set_one_line(obj, true); + if (placeholder && *placeholder) { + lv_textarea_set_placeholder_text(obj, placeholder); + } + int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTFIELD, obj); + if (h >= 0) { + lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)h); + lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_READY, + (void *)(intptr_t)h); + } + return h; +} + +/* + * el_lvgl_text_area_create — multi-line text input. + */ +int64_t el_lvgl_text_area_create(const char *placeholder) { + lv_obj_t *obj = lv_textarea_create(lv_scr_act()); + lv_textarea_set_one_line(obj, false); + if (placeholder && *placeholder) { + lv_textarea_set_placeholder_text(obj, placeholder); + } + int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTAREA, obj); + if (h >= 0) { + lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)h); + } + return h; +} + +/* + * el_lvgl_image_create — image widget. + * + * On hosted Linux (SDL backend), path is a filesystem path. + * On embedded with SPIFFS/LittleFS, path is a SPIFFS URI: "S:/image.bin". + * LVGL image decoders are registered separately by the host application. + */ +int64_t el_lvgl_image_create(const char *path_or_name) { + lv_obj_t *obj = lv_img_create(lv_scr_act()); + if (path_or_name && *path_or_name) { + lv_img_set_src(obj, path_or_name); + } + return lvgl_slot_alloc(EL_LVGL_IMAGE, obj); +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +/* + * el_lvgl_widget_set_text — update visible text. + * + * Dispatch per kind: + * LABEL → lv_label_set_text + * BUTTON → lv_label_set_text on inner btn_label child + * TEXTFIELD / TEXTAREA → lv_textarea_set_text + * WINDOW → lv_obj_set_user_data (stores title string) + */ +void el_lvgl_widget_set_text(int64_t handle, const char *text) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + const char *t = text ? text : ""; + switch (w->kind) { + case EL_LVGL_LABEL: + lv_label_set_text(w->obj, t); + break; + case EL_LVGL_BUTTON: + if (w->btn_label) lv_label_set_text(w->btn_label, t); + break; + case EL_LVGL_TEXTFIELD: + case EL_LVGL_TEXTAREA: + lv_textarea_set_text(w->obj, t); + break; + case EL_LVGL_WINDOW: + lv_obj_set_user_data(w->obj, (void *)(uintptr_t)t); + break; + default: + break; + } +} + +/* + * el_lvgl_widget_get_text — retrieve visible text. + * + * Returns a pointer into LVGL's internal storage — valid until the next LVGL + * operation that modifies the widget. Callers that need to hold the value + * across LVGL calls must strdup() it. + */ +const char *el_lvgl_widget_get_text(int64_t handle) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return ""; + switch (w->kind) { + case EL_LVGL_LABEL: + return lv_label_get_text(w->obj); + case EL_LVGL_BUTTON: + return w->btn_label ? lv_label_get_text(w->btn_label) : ""; + case EL_LVGL_TEXTFIELD: + case EL_LVGL_TEXTAREA: + return lv_textarea_get_text(w->obj); + default: + return ""; + } +} + +/* + * el_lvgl_widget_set_color — foreground (text) colour. + * + * r/g/b are 0.0–1.0 floats bit-cast as el_val_t (see el_runtime.h). + * LVGL lv_color_make takes uint8_t 0–255 components. + */ +void el_lvgl_widget_set_color(int64_t handle, + float r, float g, float b, float a) { + (void)a; /* LVGL text colour has no per-glyph alpha channel */ + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_color_t c = lv_color_make( + (uint8_t)(r * 255.0f + 0.5f), + (uint8_t)(g * 255.0f + 0.5f), + (uint8_t)(b * 255.0f + 0.5f)); + lv_obj_set_style_text_color(w->obj, c, 0); + if (w->kind == EL_LVGL_BUTTON && w->btn_label) { + lv_obj_set_style_text_color(w->btn_label, c, 0); + } +} + +/* + * el_lvgl_widget_set_bg_color — background fill colour + opacity. + */ +void el_lvgl_widget_set_bg_color(int64_t handle, + float r, float g, float b, float a) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_color_t c = lv_color_make( + (uint8_t)(r * 255.0f + 0.5f), + (uint8_t)(g * 255.0f + 0.5f), + (uint8_t)(b * 255.0f + 0.5f)); + lv_opa_t opa = (lv_opa_t)(a * 255.0f + 0.5f); + lv_obj_set_style_bg_color(w->obj, c, 0); + lv_obj_set_style_bg_opa(w->obj, opa, 0); +} + +/* + * el_lvgl_widget_set_font — apply font to text-bearing widget. + * + * The `family` parameter is accepted for API compatibility but LVGL uses + * compiled-in fonts only. Only the size and bold flag have effect unless + * EL_LVGL_CUSTOM_FONT is defined by the host. + */ +void el_lvgl_widget_set_font(int64_t handle, + const char *family, int size, int bold) { + (void)family; /* ignored; LVGL uses compiled-in Montserrat fonts */ + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + const lv_font_t *font = font_select(size, bold); + if (!font) return; + lv_obj_set_style_text_font(w->obj, font, 0); + if (w->kind == EL_LVGL_BUTTON && w->btn_label) { + lv_obj_set_style_text_font(w->btn_label, font, 0); + } +} + +/* + * el_lvgl_widget_set_padding — set per-side padding (top/right/bottom/left). + */ +void el_lvgl_widget_set_padding(int64_t handle, + int top, int right, int bottom, int left) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_style_pad_top(w->obj, (lv_coord_t)top, 0); + lv_obj_set_style_pad_right(w->obj, (lv_coord_t)right, 0); + lv_obj_set_style_pad_bottom(w->obj, (lv_coord_t)bottom, 0); + lv_obj_set_style_pad_left(w->obj, (lv_coord_t)left, 0); +} + +/* + * el_lvgl_widget_set_width — set explicit pixel width. + */ +void el_lvgl_widget_set_width(int64_t handle, int width) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_width(w->obj, (lv_coord_t)width); +} + +/* + * el_lvgl_widget_set_height — set explicit pixel height. + */ +void el_lvgl_widget_set_height(int64_t handle, int height) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_height(w->obj, (lv_coord_t)height); +} + +/* + * el_lvgl_widget_set_flex — set flex grow factor. + * + * flex > 0 → lv_obj_set_flex_grow(obj, flex): object expands to fill + * remaining space proportional to its grow factor. + * flex == 0 → lv_obj_set_flex_grow(obj, 0): object uses natural size. + */ +void el_lvgl_widget_set_flex(int64_t handle, int flex) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_flex_grow(w->obj, (uint8_t)(flex > 0 ? flex : 0)); +} + +/* + * el_lvgl_widget_set_corner_radius — set border radius. + */ +void el_lvgl_widget_set_corner_radius(int64_t handle, int radius) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_set_style_radius(w->obj, (lv_coord_t)radius, 0); +} + +/* + * el_lvgl_widget_set_disabled — enable/disable interactive state. + * + * LV_STATE_DISABLED greys out the widget and prevents input events. + */ +void el_lvgl_widget_set_disabled(int64_t handle, int disabled) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + if (disabled) { + lv_obj_add_state(w->obj, LV_STATE_DISABLED); + } else { + lv_obj_clear_state(w->obj, LV_STATE_DISABLED); + } +} + +/* + * el_lvgl_widget_set_hidden — show/hide widget. + * + * LV_OBJ_FLAG_HIDDEN hides the widget and removes it from layout flow. + */ +void el_lvgl_widget_set_hidden(int64_t handle, int hidden) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + if (hidden) { + lv_obj_add_flag(w->obj, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_clear_flag(w->obj, LV_OBJ_FLAG_HIDDEN); + } +} + +/* ── Tree operations ──────────────────────────────────────────────────────── */ + +/* + * el_lvgl_widget_add_child — attach child widget to parent. + * + * lv_obj_set_parent() reparents the child object inside the LVGL tree. + * For WINDOW parents we use the screen object itself as the parent, since + * lv_scr_act() IS the root container. + */ +void el_lvgl_widget_add_child(int64_t parent, int64_t child) { + ElLvglWidget *pw = lvgl_slot_get(parent); + ElLvglWidget *cw = lvgl_slot_get(child); + if (!pw || !cw) return; + lv_obj_set_parent(cw->obj, pw->obj); +} + +/* + * el_lvgl_widget_remove_child — detach child from its current parent. + * + * LVGL has no explicit "remove from parent without deleting" operation. + * We reparent the child back to the active screen (making it a root-level + * floating widget) and then hide it. The widget still occupies a slot and + * can be re-attached or destroyed later. + */ +void el_lvgl_widget_remove_child(int64_t parent, int64_t child) { + (void)parent; + ElLvglWidget *cw = lvgl_slot_get(child); + if (!cw) return; + /* Move to screen root and hide. */ + lv_obj_set_parent(cw->obj, lv_scr_act()); + lv_obj_add_flag(cw->obj, LV_OBJ_FLAG_HIDDEN); +} + +/* + * el_lvgl_widget_destroy — delete widget and its children from the LVGL tree, + * then free the slot. + * + * lv_obj_del() recursively deletes the object and all children. After this + * call the handle is invalid and must not be used. + */ +void el_lvgl_widget_destroy(int64_t handle) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + lv_obj_del(w->obj); + lvgl_slot_free(handle); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +/* + * Event registration stores the El function name in the widget slot. The + * actual lv_obj_add_event_cb() call is made here (or was made in the factory + * for buttons/textfields where we know the relevant event codes upfront). + * + * For widgets that did not register their event callback in the factory (e.g. + * labels receiving a click handler), we add the LVGL event binding now. + */ + +void el_lvgl_widget_on_click(int64_t handle, const char *fn_name) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + strncpy(w->cb_click, fn_name ? fn_name : "", 255); + w->cb_click[255] = '\0'; + /* + * Buttons already have LV_EVENT_CLICKED registered in the factory. + * For other widget kinds (labels, containers used as tap targets), add + * the click flag and register the callback. + */ + if (w->kind != EL_LVGL_BUTTON) { + lv_obj_add_flag(w->obj, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_CLICKED, + (void *)(intptr_t)handle); + } +} + +void el_lvgl_widget_on_change(int64_t handle, const char *fn_name) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + strncpy(w->cb_change, fn_name ? fn_name : "", 255); + w->cb_change[255] = '\0'; + /* + * Textfield/textarea factories already register VALUE_CHANGED. + * For other kinds (e.g. a custom toggle), add the binding now. + */ + if (w->kind != EL_LVGL_TEXTFIELD && w->kind != EL_LVGL_TEXTAREA) { + lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED, + (void *)(intptr_t)handle); + } +} + +void el_lvgl_widget_on_submit(int64_t handle, const char *fn_name) { + ElLvglWidget *w = lvgl_slot_get(handle); + if (!w) return; + strncpy(w->cb_submit, fn_name ? fn_name : "", 255); + w->cb_submit[255] = '\0'; + /* + * LV_EVENT_READY fires when Enter is pressed in a one-line textarea. + * Textfield factories already register READY. For other kinds, add it. + */ + if (w->kind != EL_LVGL_TEXTFIELD) { + lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_READY, + (void *)(intptr_t)handle); + } +} + +#endif /* EL_TARGET_LVGL */ diff --git a/lang/el-compiler/runtime/el_native_target.h b/lang/el-compiler/runtime/el_native_target.h new file mode 100644 index 0000000..7daabb7 --- /dev/null +++ b/lang/el-compiler/runtime/el_native_target.h @@ -0,0 +1,574 @@ +/* + * el_native_target.h — Native widget declarations for el programs targeting + * native desktop UI (AppKit / GTK4 / Win32). + * + * This header is designed to be included AFTER el_runtime.h without conflict: + * - It does NOT redefine el_to_float, el_from_float, or any el_runtime.h + * static inlines. + * - It does NOT redeclare __println, __print, or other functions whose + * return types differ between el_seed.h and el_runtime.h. + * - It adds: native widget builtins + float arithmetic helpers that the + * current el_runtime.h omits but elc still emits calls to. + * + * Usage: + * Inject via -include at compile time, OR #include it after el_runtime.h. + * + * clang -DEL_TARGET_MACOS -include el_native_target.h -c my_app.c ... + */ + +#pragma once + +#include +#include + +/* el_val_t must already be defined by el_runtime.h or el_seed.h. */ +#ifndef EL_VAL_T_DEFINED +typedef int64_t el_val_t; +#endif + +/* ── Float arithmetic helpers ─────────────────────────────────────────────── + * elc emits calls to float_div / float_mul etc. for Float-typed expressions. + * These were in el_runtime.c through v1.0.0-20260501 but are missing from the + * current el_runtime.h. Redeclared here as static inline to avoid link deps. + * Only defined if not already declared (old runtimes that still have them). */ + +#ifndef EL_FLOAT_OPS_DEFINED +#define EL_FLOAT_OPS_DEFINED + +/* el_to_float / el_from_float — bit-cast between el_val_t and double. + * Defined as static inline in both el_runtime.h and el_seed.h; we do NOT + * redefine them here. We rely on one of those headers being included first. */ + +static inline el_val_t float_div(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; + ur.d = (ub.d != 0.0) ? (ua.d / ub.d) : 0.0; + return ur.i; +} + +static inline el_val_t float_mul(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d * ub.d; + return ur.i; +} + +static inline el_val_t float_add(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d + ub.d; + return ur.i; +} + +static inline el_val_t float_sub(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub, ur; + ua.i = a; ub.i = b; ur.d = ua.d - ub.d; + return ur.i; +} + +static inline el_val_t float_lt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d < ub.d); +} + +static inline el_val_t float_gt(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d > ub.d); +} + +static inline el_val_t float_lte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d <= ub.d); +} + +static inline el_val_t float_gte(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d >= ub.d); +} + +static inline el_val_t float_eq(el_val_t a, el_val_t b) { + union { double d; int64_t i; } ua, ub; + ua.i = a; ub.i = b; + return (el_val_t)(ua.d == ub.d); +} + +#endif /* EL_FLOAT_OPS_DEFINED */ + +/* ── Native widget system (macOS AppKit) ──────────────────────────────────── + * Available when compiled with -DEL_TARGET_MACOS and linked with el_appkit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. */ + +#ifdef EL_TARGET_MACOS + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +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); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_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); +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); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t 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); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); +void __widget_set_data(el_val_t handle, el_val_t data_str); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_MACOS */ + +/* ── Native widget system (Linux GTK4) ────────────────────────────────────── + * Available when compiled with -DEL_TARGET_LINUX and linked with el_gtk4.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * All functions have the same signatures as EL_TARGET_MACOS above. */ + +#ifdef EL_TARGET_LINUX + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +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); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_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); +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); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t 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); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as EL_TARGET_MACOS */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LINUX */ + +/* ── Native widget system (Windows Win32) ─────────────────────────────────── + * Available when compiled with -DEL_TARGET_WIN32 and linked with el_win32.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * Link: el_win32.obj comctl32.lib user32.lib gdi32.lib */ + +#ifdef EL_TARGET_WIN32 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +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); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_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); +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); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t 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); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_WIN32 */ + +/* ── Native widget system (iOS UIKit) ─────────────────────────────────────── + * Available when compiled with -DEL_TARGET_IOS and linked with el_uikit.m. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * iOS lifecycle note: UIApplicationMain never returns. The el program must + * store its UI-build logic in a void(*)(void) function pointer, assign it to + * el_main_entry_fn, then call __native_run_loop. ElAppDelegate invokes + * el_main_entry_fn inside didFinishLaunchingWithOptions. + * Call el_uikit_set_args(argc, argv) from main() before __native_run_loop. */ + +#ifdef EL_TARGET_IOS + +/* Lifecycle entry-function hook — set before calling __native_run_loop. */ +extern void (*el_main_entry_fn)(void); + +/* Forward argc/argv from main() to UIApplicationMain. */ +void el_uikit_set_args(int argc, char** argv); + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +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); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_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); +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); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t 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); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_IOS */ + +/* ── Native widget system (Android JNI) ───────────────────────────────────── + * Available when compiled with -DEL_TARGET_ANDROID and linked with + * libelruntime.so (which includes el_android.c compiled by the NDK build). + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Java companion: ElBridge.java (package com.neuron.el) must be compiled into + * the APK. The Activity must call ElBridge.init(this) before any widget ops. + * + * Link flags (in Android.mk or CMakeLists.txt): + * -landroid -llog -ldl */ + +#ifdef EL_TARGET_ANDROID + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); /* no-op on Android */ + +/* Window */ +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); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_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); +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); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t 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); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_ANDROID */ + +/* ── Native widget system (LVGL v9 — embedded / microcontroller) ───────────── + * Available when compiled with -DEL_TARGET_LVGL and linked with el_lvgl.c + * and the LVGL library (lvgl.a or lvgl source tree). + * + * Target platforms: ESP32, STM32, industrial panels. Any system with 256KB+ + * RAM and an LVGL-compatible display driver. No OS required. + * + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Bare-metal / no dynamic linker: + * Compile with -DEL_LVGL_NO_DLSYM and provide: + * el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b); + * + * Compile: + * gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o + * # Then link with lvgl.a. */ + +#ifdef EL_TARGET_LVGL + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +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); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_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); +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); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t 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); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader — same JSON output as all other native targets */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_LVGL */ + +/* ── Native widget system (SDL2 — embedded / Pi) ──────────────────────────── + * Available when compiled with -DEL_TARGET_SDL2 and linked with el_sdl2.c. + * Widget handles are opaque int64_t slot indices; -1 = invalid. + * + * Target: Raspberry Pi Zero, embedded Linux, any system with a framebuffer + * and SDL2 available. No GTK, no desktop environment required. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl */ + +#ifdef EL_TARGET_SDL2 + +/* Initialisation */ +void __native_init(void); +void __native_run_loop(void); + +/* Window */ +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); +void __window_show(el_val_t handle); +void __window_set_title(el_val_t handle, el_val_t title); + +/* Layout containers */ +el_val_t __vstack_create(el_val_t spacing); +el_val_t __hstack_create(el_val_t spacing); +el_val_t __zstack_create(void); +el_val_t __scroll_create(void); + +/* Widgets */ +el_val_t __label_create(el_val_t text); +el_val_t __button_create(el_val_t label); +el_val_t __text_field_create(el_val_t placeholder); +el_val_t __text_area_create(el_val_t placeholder); +el_val_t __image_create(el_val_t path_or_name); + +/* Widget properties */ +void __widget_set_text(el_val_t handle, el_val_t text); +el_val_t __widget_get_text(el_val_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); +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); +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t 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); +void __widget_set_width(el_val_t handle, el_val_t width); +void __widget_set_height(el_val_t handle, el_val_t height); +void __widget_set_flex(el_val_t handle, el_val_t flex); +void __widget_set_corner_radius(el_val_t handle, el_val_t radius); +void __widget_set_disabled(el_val_t handle, el_val_t disabled); +void __widget_set_hidden(el_val_t handle, el_val_t hidden); + +/* Layout / tree */ +void __widget_add_child(el_val_t parent, el_val_t child); +void __widget_remove_child(el_val_t parent, el_val_t child); +void __widget_destroy(el_val_t handle); + +/* Events */ +void __widget_on_click(el_val_t handle, el_val_t fn_name); +void __widget_on_change(el_val_t handle, el_val_t fn_name); +void __widget_on_submit(el_val_t handle, el_val_t fn_name); + +/* Manifest reader */ +el_val_t __manifest_read(el_val_t path); + +#endif /* EL_TARGET_SDL2 */ diff --git a/lang/el-compiler/runtime/el_runtime_win32.c b/lang/el-compiler/runtime/el_runtime_win32.c new file mode 100644 index 0000000..4588839 --- /dev/null +++ b/lang/el-compiler/runtime/el_runtime_win32.c @@ -0,0 +1,1146 @@ +/* + * el_runtime_win32.c — Minimal Win32-compatible el runtime stub. + * + * Provides only the symbols that native-hello (and the el-native vessel) call + * from el_runtime.h. Replaces the full el_runtime.c + el_seed.c for + * cross-compiled Win32 targets where POSIX headers (pthread, curl, dlfcn, + * sys/socket, etc.) are unavailable. + * + * Symbols provided: + * Allocators: el_strdup, el_strbuf, el_wrap_str (static helpers) + * I/O: println, print + * String: el_str_concat, str_eq, str_starts_with, str_len, str_concat, + * int_to_str, str_to_int, native_str_to_int, str_slice, str_trim, + * str_contains, str_ends_with, str_to_upper, str_to_lower, + * str_index_of, str_char_at, str_char_code, str_replace, + * str_pad_left, str_pad_right, str_upper, str_lower + * Math: el_abs, el_max, el_min, int_to_float, float_to_int + * List: el_list_empty, el_list_new, el_list_len, el_list_get, + * el_list_append, el_list_clone + * Map: el_map_new, el_map_get, el_get_field, el_map_set + * Retain/RC: el_retain, el_release + * Arena: el_arena_push, el_arena_pop + * JSON: json_get_string, json_get_int, json_get_float, json_get_bool, + * json_get_raw, json_find_key (static), json_skip_value (static) + * Process: exit_program, env, el_runtime_init_args + * Stubs: All remaining el_runtime.h symbols as no-op / return-0 stubs + * so the linker is satisfied even if the app doesn't call them. + * + * el_request_start / el_request_end are provided as empty no-ops (no HTTP + * server, no per-request arena needed for a native GUI app). + * + * Compile: + * x86_64-w64-mingw32-gcc -std=c11 -DEL_TARGET_WIN32 -I \ + * -c el_runtime_win32.c -o el_runtime_win32.o + */ + +#include "el_runtime.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ── Arena (single-threaded; no pthread needed) ─────────────────────────── */ + +#define EL_ARENA_INITIAL 512 + +typedef struct { + char** ptrs; + size_t count; + size_t cap; +} ElArena; + +static ElArena _arena = {NULL, 0, 0}; +static int _arena_active = 0; + +static void el_arena_track(char* p) { + if (!_arena_active || !p) return; + if (_arena.count >= _arena.cap) { + size_t nc = _arena.cap == 0 ? EL_ARENA_INITIAL : _arena.cap * 2; + char** g = realloc(_arena.ptrs, nc * sizeof(char*)); + if (!g) return; + _arena.ptrs = g; + _arena.cap = nc; + } + _arena.ptrs[_arena.count++] = p; +} + +void el_request_start(void) { _arena.count = 0; _arena_active = 1; } +void el_request_end(void) { + _arena_active = 0; + for (size_t i = 0; i < _arena.count; i++) free(_arena.ptrs[i]); + _arena.count = 0; +} + +#define EL_ARENA_SCOPE_DEPTH 32 +static size_t _arena_scope[EL_ARENA_SCOPE_DEPTH]; +static int _arena_scope_depth = 0; + +el_val_t el_arena_push(void) { + if (!_arena_active) _arena_active = 1; + if (_arena_scope_depth < EL_ARENA_SCOPE_DEPTH) + _arena_scope[_arena_scope_depth++] = _arena.count; + return (el_val_t)(int64_t)_arena.count; +} + +el_val_t el_arena_pop(el_val_t mark) { + size_t save = (size_t)(int64_t)mark; + if (save > _arena.count) save = 0; + for (size_t i = save; i < _arena.count; i++) { + if (_arena.ptrs[i]) { free(_arena.ptrs[i]); _arena.ptrs[i] = NULL; } + } + _arena.count = save; + if (_arena_scope_depth > 0) _arena_scope_depth--; + if (save == 0) _arena_active = 0; + return 0; +} + +/* ── Allocator helpers ───────────────────────────────────────────────────── */ + +static char* el_strdup(const char* s) { + if (!s) { char* p = strdup(""); el_arena_track(p); return p; } + char* p = strdup(s); + el_arena_track(p); + return p; +} + +static char* el_strbuf(size_t n) { + char* p = malloc(n + 1); + if (!p) { fputs("el_runtime_win32: out of memory\n", stderr); exit(1); } + p[0] = '\0'; + el_arena_track(p); + return p; +} + +static el_val_t el_wrap_str(char* s) { + return EL_STR(s); +} + +/* ── I/O ─────────────────────────────────────────────────────────────────── */ + +el_val_t println(el_val_t s) { + const char* str = EL_CSTR(s); + puts(str ? str : ""); + return 0; +} + +el_val_t print(el_val_t s) { + const char* str = EL_CSTR(s); + if (str) fputs(str, stdout); + return 0; +} + +el_val_t readline(void) { + char buf[4096]; + if (!fgets(buf, sizeof(buf), stdin)) return el_wrap_str(el_strdup("")); + size_t len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') buf[len - 1] = '\0'; + return el_wrap_str(el_strdup(buf)); +} + +/* ── String builtins ─────────────────────────────────────────────────────── */ + +el_val_t el_str_concat(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a) a = ""; + if (!b) b = ""; + size_t la = strlen(a), lb = strlen(b); + char* out = el_strbuf(la + lb); + memcpy(out, a, la); + memcpy(out + la, b, lb); + out[la + lb] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_eq(el_val_t av, el_val_t bv) { + const char* a = EL_CSTR(av); + const char* b = EL_CSTR(bv); + if (!a || !b) return (el_val_t)(a == b); + return (el_val_t)(strcmp(a, b) == 0); +} + +el_val_t str_starts_with(el_val_t sv, el_val_t prefv) { + const char* s = EL_CSTR(sv); + const char* p = EL_CSTR(prefv); + if (!s || !p) return 0; + return (el_val_t)(strncmp(s, p, strlen(p)) == 0); +} + +el_val_t str_ends_with(el_val_t sv, el_val_t sufv) { + const char* s = EL_CSTR(sv); + const char* suf = EL_CSTR(sufv); + if (!s || !suf) return 0; + size_t ls = strlen(s), lsuf = strlen(suf); + if (lsuf > ls) return 0; + return (el_val_t)(strcmp(s + ls - lsuf, suf) == 0); +} + +el_val_t str_len(el_val_t sv) { + const char* s = EL_CSTR(sv); + return s ? (el_val_t)strlen(s) : 0; +} + +el_val_t str_concat(el_val_t a, el_val_t b) { + return el_str_concat(a, b); +} + +el_val_t int_to_str(el_val_t n) { + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_to_int(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return (el_val_t)atoll(s); +} + +el_val_t native_str_to_int(el_val_t sv) { return str_to_int(sv); } + +el_val_t str_slice(el_val_t sv, el_val_t start, el_val_t end) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + int64_t len = (int64_t)strlen(s); + if (start < 0) start = 0; + if (end > len) end = len; + if (start >= end) return el_wrap_str(el_strdup("")); + int64_t sz = end - start; + char* out = el_strbuf((size_t)sz); + memcpy(out, s + start, (size_t)sz); + out[sz] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_contains(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return 0; + return (el_val_t)(strstr(s, sub) != NULL); +} + +el_val_t str_replace(el_val_t sv, el_val_t fromv, el_val_t tov) { + const char* s = EL_CSTR(sv); + const char* from = EL_CSTR(fromv); + const char* to = EL_CSTR(tov); + if (!s || !from || !to) return el_wrap_str(el_strdup(s ? s : "")); + size_t ls = strlen(s), lf = strlen(from), lt = strlen(to); + if (lf == 0) return el_wrap_str(el_strdup(s)); + /* Count occurrences */ + size_t count = 0; + const char* p = s; + while ((p = strstr(p, from)) != NULL) { count++; p += lf; } + size_t new_len = ls + count * (lt > lf ? lt - lf : 0) - count * (lf > lt ? lf - lt : 0); + char* out = el_strbuf(new_len); + const char* src = s; + char* dst = out; + while ((p = strstr(src, from)) != NULL) { + size_t pre = (size_t)(p - src); + memcpy(dst, src, pre); dst += pre; + memcpy(dst, to, lt); dst += lt; + src = p + lf; + } + strcpy(dst, src); + return el_wrap_str(out); +} + +el_val_t str_to_upper(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)toupper((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_to_lower(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + size_t n = strlen(s); + char* out = el_strbuf(n); + for (size_t i = 0; i < n; i++) out[i] = (char)tolower((unsigned char)s[i]); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_upper(el_val_t sv) { return str_to_upper(sv); } +el_val_t str_lower(el_val_t sv) { return str_to_lower(sv); } + +el_val_t str_trim(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("")); + while (*s && isspace((unsigned char)*s)) s++; + size_t n = strlen(s); + while (n > 0 && isspace((unsigned char)s[n - 1])) n--; + char* out = el_strbuf(n); + memcpy(out, s, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_index_of(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub) return -1; + const char* p = strstr(s, sub); + return p ? (el_val_t)(p - s) : -1; +} + +el_val_t str_char_at(el_val_t sv, el_val_t i) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t idx = (int64_t)i; + int64_t len = (int64_t)strlen(s); + if (idx < 0 || idx >= len) return 0; + char buf[2] = { s[idx], '\0' }; + return el_wrap_str(el_strdup(buf)); +} + +el_val_t str_char_code(el_val_t sv, el_val_t i) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + int64_t idx = (int64_t)i; + int64_t len = (int64_t)strlen(s); + if (idx < 0 || idx >= len) return 0; + return (el_val_t)(unsigned char)s[idx]; +} + +el_val_t str_pad_left(el_val_t sv, el_val_t widthv, el_val_t padv) { + const char* s = EL_CSTR(sv); + const char* pad = EL_CSTR(padv); + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t width = (int64_t)widthv; + int64_t slen = (int64_t)strlen(s); + if (slen >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - slen; + char* out = el_strbuf((size_t)width); + size_t plen = strlen(pad); + for (int64_t i = 0; i < need; i++) out[i] = pad[i % plen]; + memcpy(out + need, s, (size_t)slen); + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_pad_right(el_val_t sv, el_val_t widthv, el_val_t padv) { + const char* s = EL_CSTR(sv); + const char* pad = EL_CSTR(padv); + if (!s) s = ""; + if (!pad || !*pad) pad = " "; + int64_t width = (int64_t)widthv; + int64_t slen = (int64_t)strlen(s); + if (slen >= width) return el_wrap_str(el_strdup(s)); + int64_t need = width - slen; + char* out = el_strbuf((size_t)width); + memcpy(out, s, (size_t)slen); + size_t plen = strlen(pad); + for (int64_t i = 0; i < need; i++) out[slen + i] = pad[i % plen]; + out[width] = '\0'; + return el_wrap_str(out); +} + +el_val_t str_format(el_val_t fmtv, el_val_t datav) { + /* Minimal stub — just return the format string unchanged. */ + return fmtv; +} + +el_val_t str_split(el_val_t sv, el_val_t sepv) { + /* Return empty list stub. */ + return el_list_empty(); +} + +el_val_t str_count(el_val_t sv, el_val_t subv) { + const char* s = EL_CSTR(sv); + const char* sub = EL_CSTR(subv); + if (!s || !sub || !*sub) return 0; + size_t lsub = strlen(sub); + int64_t cnt = 0; + const char* p = s; + while ((p = strstr(p, sub)) != NULL) { cnt++; p += lsub; } + return (el_val_t)cnt; +} + +/* ── Math ────────────────────────────────────────────────────────────────── */ + +el_val_t el_abs(el_val_t n) { return n < 0 ? -n : n; } +el_val_t el_max(el_val_t a, el_val_t b) { return a > b ? a : b; } +el_val_t el_min(el_val_t a, el_val_t b) { return a < b ? a : b; } + +el_val_t int_to_float(el_val_t n) { return el_from_float((double)(int64_t)n); } +el_val_t float_to_int(el_val_t f) { return (el_val_t)(int64_t)el_to_float(f); } +el_val_t float_to_str(el_val_t f) { + char buf[64]; + snprintf(buf, sizeof(buf), "%g", el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} +el_val_t str_to_float(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return 0; + return el_from_float(strtod(s, NULL)); +} +el_val_t format_float(el_val_t f, el_val_t decimals) { + char buf[128]; + int d = (int)(int64_t)decimals; + if (d < 0) d = 0; if (d > 30) d = 30; + snprintf(buf, sizeof(buf), "%.*f", d, el_to_float(f)); + return el_wrap_str(el_strdup(buf)); +} +el_val_t decimal_round(el_val_t f, el_val_t decimals) { return format_float(f, decimals); } +el_val_t math_sqrt(el_val_t f) { return el_from_float(sqrt(el_to_float(f))); } +el_val_t math_log(el_val_t f) { return el_from_float(log10(el_to_float(f))); } +el_val_t math_ln(el_val_t f) { return el_from_float(log(el_to_float(f))); } +el_val_t math_sin(el_val_t f) { return el_from_float(sin(el_to_float(f))); } +el_val_t math_cos(el_val_t f) { return el_from_float(cos(el_to_float(f))); } +el_val_t math_pi(void) { return el_from_float(3.14159265358979323846); } + +/* ── Refcounted heap objects ─────────────────────────────────────────────── */ + +#define EL_MAGIC_LIST 0xE15710A1u +#define EL_MAGIC_MAP 0xE19A704Bu + +typedef struct { uint32_t magic; uint32_t refcount; } ElHeader; + +typedef struct { + ElHeader hdr; + int64_t length; + int64_t capacity; + el_val_t* elems; +} ElList; + +typedef struct { + ElHeader hdr; + int64_t count; + int64_t capacity; + el_val_t* keys; + el_val_t* values; +} ElMap; + +static int looks_like_heap_obj(el_val_t v) { + if (v == 0) return 0; + int64_t s = (int64_t)v; + if (s > -0x10000 && s < 0x10000) return 0; + uintptr_t p = (uintptr_t)v; + if (p < 0x10000) return 0; + if (p & 0x7) return 0; + return 1; +} + +void el_retain(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST || h->magic == EL_MAGIC_MAP) h->refcount++; +} + +void el_release(el_val_t v) { + if (!looks_like_heap_obj(v)) return; + ElHeader* h = (ElHeader*)(uintptr_t)v; + if (h->magic == EL_MAGIC_LIST) { + if (h->refcount > 0 && --h->refcount == 0) { + ElList* l = (ElList*)h; + free(l->elems); h->magic = 0; free(l); + } + } else if (h->magic == EL_MAGIC_MAP) { + if (h->refcount > 0 && --h->refcount == 0) { + ElMap* m = (ElMap*)h; + free(m->keys); free(m->values); h->magic = 0; free(m); + } + } +} + +/* ── List ────────────────────────────────────────────────────────────────── */ + +static ElList* list_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElList* lst = malloc(sizeof(ElList)); + if (!lst) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + lst->hdr.magic = EL_MAGIC_LIST; + lst->hdr.refcount = 1; + lst->length = 0; + lst->capacity = cap; + lst->elems = malloc((size_t)cap * sizeof(el_val_t)); + if (!lst->elems) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + return lst; +} + +el_val_t el_list_empty(void) { return EL_STR(list_alloc(4)); } + +el_val_t el_list_new(el_val_t count, ...) { + ElList* lst = list_alloc(count > 0 ? count : 4); + va_list ap; va_start(ap, count); + for (int64_t i = 0; i < count; i++) lst->elems[i] = va_arg(ap, el_val_t); + va_end(ap); + lst->length = count; + return EL_STR(lst); +} + +el_val_t el_list_len(el_val_t lv) { + ElList* l = (ElList*)(uintptr_t)lv; + return l ? l->length : 0; +} + +el_val_t el_list_get(el_val_t lv, el_val_t idx) { + ElList* l = (ElList*)(uintptr_t)lv; + if (!l || idx < 0 || idx >= l->length) return 0; + return l->elems[idx]; +} + +el_val_t el_list_append(el_val_t lv, el_val_t elem) { + ElList* old = (ElList*)(uintptr_t)lv; + if (!old) { + ElList* fresh = list_alloc(4); + fresh->elems[0] = elem; + fresh->length = 1; + return EL_STR(fresh); + } + if (old->hdr.refcount <= 1) { + if (old->length >= old->capacity) { + int64_t nc = old->capacity > 0 ? old->capacity * 2 : 4; + el_val_t* g = realloc(old->elems, (size_t)nc * sizeof(el_val_t)); + if (!g) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + old->elems = g; + old->capacity = nc; + } + old->elems[old->length++] = elem; + return lv; + } + int64_t nc = old->length + 1; + if (nc < 4) nc = 4; + ElList* fresh = malloc(sizeof(ElList)); + if (!fresh) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + fresh->hdr.magic = EL_MAGIC_LIST; + fresh->hdr.refcount = 1; + fresh->length = old->length + 1; + fresh->capacity = nc; + fresh->elems = malloc((size_t)nc * sizeof(el_val_t)); + if (!fresh->elems) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + if (old->length > 0) + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + fresh->elems[old->length] = elem; + return EL_STR(fresh); +} + +el_val_t el_list_clone(el_val_t lv) { + ElList* old = (ElList*)(uintptr_t)lv; + if (!old) return el_list_empty(); + ElList* fresh = list_alloc(old->capacity > 0 ? old->capacity : 4); + if (old->length > 0) + memcpy(fresh->elems, old->elems, (size_t)old->length * sizeof(el_val_t)); + fresh->length = old->length; + return EL_STR(fresh); +} + +/* ── Map ─────────────────────────────────────────────────────────────────── */ + +static ElMap* map_alloc(int64_t cap) { + if (cap < 4) cap = 4; + ElMap* m = malloc(sizeof(ElMap)); + if (!m) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + m->hdr.magic = EL_MAGIC_MAP; + m->hdr.refcount = 1; + m->count = 0; + m->capacity = cap; + m->keys = malloc((size_t)cap * sizeof(el_val_t)); + m->values = malloc((size_t)cap * sizeof(el_val_t)); + if (!m->keys || !m->values) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + return m; +} + +el_val_t el_map_new(el_val_t pair_count, ...) { + ElMap* m = map_alloc(pair_count > 0 ? pair_count : 4); + va_list ap; va_start(ap, pair_count); + for (int64_t i = 0; i < pair_count; i++) { + m->keys[i] = va_arg(ap, el_val_t); + m->values[i] = va_arg(ap, el_val_t); + } + va_end(ap); + m->count = pair_count; + return EL_STR(m); +} + +el_val_t el_map_get(el_val_t mv, el_val_t kv) { + ElMap* m = (ElMap*)(uintptr_t)mv; + const char* key = EL_CSTR(kv); + if (!m || !key) return 0; + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) return m->values[i]; + } + return 0; +} + +el_val_t el_get_field(el_val_t mv, el_val_t kv) { return el_map_get(mv, kv); } + +el_val_t el_map_set(el_val_t mv, el_val_t kv, el_val_t value) { + ElMap* m = (ElMap*)(uintptr_t)mv; + if (!m) { + ElMap* fresh = map_alloc(4); + fresh->keys[0] = kv; fresh->values[0] = value; fresh->count = 1; + return EL_STR(fresh); + } + const char* key = EL_CSTR(kv); + if (key) { + for (int64_t i = 0; i < m->count; i++) { + const char* k = EL_CSTR(m->keys[i]); + if (k && strcmp(k, key) == 0) { m->values[i] = value; return mv; } + } + } + if (m->count >= m->capacity) { + int64_t nc = m->capacity > 0 ? m->capacity * 2 : 4; + el_val_t* gk = realloc(m->keys, (size_t)nc * sizeof(el_val_t)); + el_val_t* gv = realloc(m->values, (size_t)nc * sizeof(el_val_t)); + if (!gk || !gv) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + m->keys = gk; m->values = gv; m->capacity = nc; + } + m->keys[m->count] = kv; m->values[m->count] = value; m->count++; + return mv; +} + +/* ── JSON helpers ────────────────────────────────────────────────────────── */ + +typedef struct { + const char* p; + const char* end; + int err; +} JsonParser; + +static void jp_skip_ws(JsonParser* jp) { + while (jp->p < jp->end) { + char c = *jp->p; + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') jp->p++; + else break; + } +} + +static char* jp_parse_string_raw(JsonParser* jp) { + if (jp->p >= jp->end || *jp->p != '"') { jp->err = 1; return el_strdup(""); } + jp->p++; + size_t cap = 32, len = 0; + char* out = malloc(cap); + if (!out) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + while (jp->p < jp->end && *jp->p != '"') { + char c = *jp->p++; + if (c == '\\' && jp->p < jp->end) { + char esc = *jp->p++; + switch (esc) { + case '"': c = '"'; break; + case '\\': c = '\\'; break; + case '/': c = '/'; break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'u': + for (int i = 0; i < 4 && jp->p < jp->end; i++) jp->p++; + c = '?'; + break; + default: c = esc; break; + } + } + if (len + 1 >= cap) { + cap *= 2; + out = realloc(out, cap); + if (!out) { fputs("el_runtime_win32: OOM\n", stderr); exit(1); } + } + out[len++] = c; + } + if (jp->p < jp->end && *jp->p == '"') jp->p++; + else jp->err = 1; + out[len] = '\0'; + return out; +} + +static const char* json_skip_value(const char* p) { + if (!p || !*p) return p; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '"') { + p++; + int e = 0; + while (*p) { + if (e) { e = 0; p++; continue; } + if (*p == '\\') { e = 1; p++; continue; } + if (*p == '"') { p++; break; } + p++; + } + return p; + } + if (*p == '{' || *p == '[') { + char open = *p, close = (open == '{') ? '}' : ']'; + int depth = 0, in_str = 0, e = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) e = 0; + else if (c == '\\') e = 1; + else if (c == '"') in_str = 0; + p++; continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == open) depth++; + else if (c == close) { depth--; p++; if (!depth) return p; continue; } + p++; + } + return p; + } + while (*p && *p != ',' && *p != '}' && *p != ']' && + *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++; + return p; +} + +static const char* json_find_key(const char* s, const char* key) { + if (!s || !key) return NULL; + size_t klen = strlen(key); + int depth = 0, in_str = 0, escape = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (escape) escape = 0; + else if (c == '\\') escape = 1; + else if (c == '"') { + p++; in_str = 0; continue; + } + p++; continue; + } + if (c == '"') { + const char* str_start = p + 1; + const char* q = str_start; + int e = 0; + while (*q) { + if (e) { e = 0; q++; continue; } + if (*q == '\\') { e = 1; q++; continue; } + if (*q == '"') break; + q++; + } + size_t slen = (size_t)(q - str_start); + const char* after = (*q == '"') ? q + 1 : q; + if (depth == 1 && slen == klen && strncmp(str_start, key, klen) == 0) { + const char* r = after; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + if (*r == ':') { + r++; + while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') r++; + return r; + } + } + p = after; continue; + } + if (c == '{' || c == '[') depth++; + else if (c == '}' || c == ']') depth--; + p++; + } + return NULL; +} + +el_val_t json_get_string(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p != '"') return el_wrap_str(el_strdup("")); + JsonParser jp = { p, json + (json ? strlen(json) : 0), 0 }; + char* parsed = jp_parse_string_raw(&jp); + if (jp.err) { free(parsed); return el_wrap_str(el_strdup("")); } + return el_wrap_str(parsed); +} + +el_val_t json_get_int(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return (el_val_t)strtoll(p, NULL, 10); +} + +el_val_t json_get_float(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p || *p == '"' || *p == '{' || *p == '[') return 0; + return el_from_float(strtod(p, NULL)); +} + +el_val_t json_get_bool(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return 0; + return (el_val_t)(strncmp(p, "true", 4) == 0); +} + +el_val_t json_get_raw(el_val_t json_str, el_val_t key) { + const char* json = EL_CSTR(json_str); + const char* k = EL_CSTR(key); + const char* p = json_find_key(json, k); + if (!p) return el_wrap_str(el_strdup("")); + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); + out[n] = '\0'; + return el_wrap_str(out); +} + +el_val_t json_get(el_val_t json, el_val_t key) { + return json_get_raw(json, key); +} + +/* Simple JSON number / scalar parser helpers for json_parse / json_stringify */ + +el_val_t json_parse(el_val_t s) { + /* Minimal: return the string verbatim (caller accesses via json_get_*). */ + return s; +} + +el_val_t json_stringify(el_val_t v) { + /* Basic: if it already looks like a string, return it. */ + const char* s = EL_CSTR(v); + return s ? v : EL_STR("{}"); +} + +el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value) { + /* Stub: returns the original JSON unchanged. */ + return json_str; +} + +el_val_t json_array_len(el_val_t json_str) { + const char* s = EL_CSTR(json_str); + if (!s || *s != '[') return 0; + int64_t cnt = 0; + int depth = 0, in_str = 0, e = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (e) e = 0; else if (c == '\\') e = 1; else if (c == '"') in_str = 0; + p++; continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == '[' || c == '{') { if (depth++ == 0 && c == '[') { p++; continue; } } + else if (c == ']' || c == '}') { if (--depth == 0) break; } + else if (c == ',' && depth == 1) cnt++; + p++; + } + /* If we saw any content, count = separators + 1 */ + if (depth == 0) { /* empty or done */ } + /* Simpler: count commas at depth 1 + 1 if non-empty */ + /* Re-scan for simplicity */ + cnt = 0; + depth = 0; in_str = 0; e = 0; + p = s; + int has_content = 0; + while (*p) { + char c = *p; + if (in_str) { + if (e) e = 0; else if (c == '\\') e = 1; else if (c == '"') in_str = 0; + p++; continue; + } + if (c == '"') { in_str = 1; p++; continue; } + if (c == '[' || c == '{') depth++; + else if (c == ']' || c == '}') { + depth--; + if (depth == 0) break; + } + if (depth == 1) { + if (c != '[' && c != ' ' && c != '\t' && c != '\n' && c != '\r') has_content = 1; + if (c == ',') cnt++; + } + p++; + } + return has_content ? cnt + 1 : 0; +} + +el_val_t json_array_get(el_val_t json_str, el_val_t index) { + const char* s = EL_CSTR(json_str); + if (!s || *s != '[') return 0; + int64_t target = (int64_t)index; + int64_t cur = 0; + int depth = 0, in_str = 0, e = 0; + const char* p = s; + while (*p) { + char c = *p; + if (in_str) { + if (e) e = 0; else if (c == '\\') e = 1; else if (c == '"') in_str = 0; + p++; continue; + } + if (c == '"') { in_str = 1; } + if (c == '[' || c == '{') depth++; + else if (c == ']' || c == '}') { if (--depth == 0) break; } + if (depth == 1 && (p == s + 1 || c == ',')) { + if (c == ',') { cur++; p++; while (*p == ' ' || *p == '\t' || *p == '\n') p++; } + else { p++; while (*p == ' ' || *p == '\t' || *p == '\n') p++; } + if (cur == target) { + const char* end = json_skip_value(p); + size_t n = (size_t)(end - p); + char* out = el_strbuf(n); + memcpy(out, p, n); out[n] = '\0'; + return el_wrap_str(out); + } + continue; + } + p++; + } + return 0; +} + +el_val_t json_array_get_string(el_val_t json_str, el_val_t index) { + el_val_t raw = json_array_get(json_str, index); + if (!raw) return el_wrap_str(el_strdup("")); + const char* s = EL_CSTR(raw); + if (!s || *s != '"') return raw; + JsonParser jp = { s, s + strlen(s), 0 }; + char* parsed = jp_parse_string_raw(&jp); + return el_wrap_str(parsed); +} + +el_val_t json_escape_string(el_val_t sv) { + const char* s = EL_CSTR(sv); + if (!s) return el_wrap_str(el_strdup("\"\"")); + size_t n = strlen(s); + char* out = el_strbuf(n * 6 + 4); + char* p = out; + *p++ = '"'; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + switch (c) { + case '"': *p++ = '\\'; *p++ = '"'; break; + case '\\': *p++ = '\\'; *p++ = '\\'; break; + case '\n': *p++ = '\\'; *p++ = 'n'; break; + case '\r': *p++ = '\\'; *p++ = 'r'; break; + case '\t': *p++ = '\\'; *p++ = 't'; break; + default: + if (c < 0x20) { + p += sprintf(p, "\\u%04x", c); + } else { + *p++ = (char)c; + } + } + } + *p++ = '"'; *p = '\0'; + return el_wrap_str(out); +} + +el_val_t json_build_object(el_val_t kvs) { return kvs; } +el_val_t json_build_array(el_val_t items) { return items; } + +/* ── Environment / process ───────────────────────────────────────────────── */ + +el_val_t env(el_val_t key) { + const char* k = EL_CSTR(key); + if (!k) return el_wrap_str(el_strdup("")); + const char* v = getenv(k); + return el_wrap_str(el_strdup(v ? v : "")); +} + +el_val_t exit_program(el_val_t code) { + exit((int)(int64_t)code); + return 0; +} + +/* Args store — populated by el_runtime_init_args */ +static el_val_t _el_args_list = 0; + +void el_runtime_init_args(int argc, char** argv) { + _el_args_list = el_list_empty(); + for (int i = 1; i < argc; i++) { + _el_args_list = el_list_append(_el_args_list, EL_STR(argv[i])); + } +} + +el_val_t args(void) { + if (!_el_args_list) _el_args_list = el_list_empty(); + return _el_args_list; +} + +/* ── HTML helpers (stubs) ────────────────────────────────────────────────── */ + +el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json) { return input_html; } +el_val_t html_raw(el_val_t s) { return s; } +el_val_t html_escape(el_val_t s) { return s; } +el_val_t url_encode(el_val_t s) { return s; } +el_val_t url_decode(el_val_t s) { return s; } + +/* ── Filesystem stubs ────────────────────────────────────────────────────── */ + +el_val_t fs_read(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return el_wrap_str(el_strdup("")); + FILE* f = fopen(p, "rb"); + if (!f) return el_wrap_str(el_strdup("")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return el_wrap_str(el_strdup("")); } + char* buf = malloc((size_t)sz + 1); + if (!buf) { fclose(f); return el_wrap_str(el_strdup("")); } + size_t got = fread(buf, 1, (size_t)sz, f); + buf[got] = '\0'; + fclose(f); + el_arena_track(buf); + return EL_STR(buf); +} + +el_val_t fs_write(el_val_t path, el_val_t content) { + const char* p = EL_CSTR(path); + const char* c = EL_CSTR(content); + if (!p || !c) return 0; + FILE* f = fopen(p, "wb"); + if (!f) return 0; + fputs(c, f); + fclose(f); + return 1; +} + +el_val_t fs_exists(el_val_t path) { + const char* p = EL_CSTR(path); + if (!p) return 0; + FILE* f = fopen(p, "rb"); + if (!f) return 0; + fclose(f); + return 1; +} + +el_val_t fs_list(el_val_t path) { return el_wrap_str(el_strdup("")); } +el_val_t fs_list_json(el_val_t path) { return el_wrap_str(el_strdup("[]")); } +el_val_t fs_mkdir(el_val_t path) { return 0; } +el_val_t fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n) { return 0; } + +/* ── HTTP stubs (no network in Win32 GUI app) ────────────────────────────── */ + +el_val_t http_get(el_val_t url) { return el_wrap_str(el_strdup("{\"error\":\"no http\"}")); } +el_val_t http_post(el_val_t url, el_val_t body) { return http_get(url); } +el_val_t http_post_json(el_val_t url, el_val_t body) { return http_get(url); } +el_val_t http_get_with_headers(el_val_t url, el_val_t h) { return http_get(url); } +el_val_t http_post_with_headers(el_val_t url, el_val_t b, el_val_t h) { return http_get(url); } +el_val_t http_post_json_with_headers(el_val_t url, el_val_t h, el_val_t b) { return http_get(url); } +el_val_t http_post_form_auth(el_val_t url, el_val_t b, el_val_t a) { return http_get(url); } +el_val_t http_delete(el_val_t url) { return http_get(url); } +el_val_t http_serve(el_val_t port, el_val_t handler) { return 0; } +el_val_t http_set_handler(el_val_t name) { return 0; } +el_val_t http_serve_v2(el_val_t port, el_val_t handler) { return 0; } +el_val_t http_set_handler_v2(el_val_t name) { return 0; } +el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) { return body; } +void el_seed_set_http_conn_fd(int fd) { (void)fd; } +el_val_t http_post_to_file(el_val_t url, el_val_t b, el_val_t h, el_val_t out) { return 0; } +el_val_t http_get_to_file(el_val_t url, el_val_t h, el_val_t out) { return 0; } + +/* ── Time stubs ──────────────────────────────────────────────────────────── */ + +#include + +el_val_t time_now(void) { return (el_val_t)time(NULL); } +el_val_t time_now_utc(void) { return time_now(); } +el_val_t now_ns(void) { return (el_val_t)(time(NULL) * (int64_t)1000000000LL); } +el_val_t el_now_instant(void) { return now_ns(); } +el_val_t now(void) { return el_now_instant(); } +el_val_t unix_seconds(el_val_t n) { return n * 1000000000LL; } +el_val_t unix_millis(el_val_t n) { return n * 1000000LL; } +el_val_t unix_timestamp(void) { return (el_val_t)time(NULL); } +el_val_t sleep_secs(el_val_t secs) { return 0; } +el_val_t sleep_ms(el_val_t ms) { return 0; } +el_val_t el_sleep_duration(el_val_t dur) { return 0; } +el_val_t time_format(el_val_t ts, el_val_t fmt) { return el_wrap_str(el_strdup("")); } +el_val_t time_to_parts(el_val_t ts) { return el_wrap_str(el_strdup("{}")); } +el_val_t time_from_parts(el_val_t s, el_val_t ns, el_val_t tz) { return s; } +el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit) { return ts; } +el_val_t time_diff(el_val_t a, el_val_t b, el_val_t unit) { return a - b; } +el_val_t instant_from_iso8601(el_val_t s) { return 0; } +el_val_t el_duration_from_nanos(el_val_t ns) { return ns; } +el_val_t duration_seconds(el_val_t n) { return n * 1000000000LL; } +el_val_t duration_millis(el_val_t n) { return n * 1000000LL; } +el_val_t duration_nanos(el_val_t n) { return n; } +el_val_t el_instant_add_dur(el_val_t i, el_val_t d) { return i + d; } +el_val_t el_instant_sub_dur(el_val_t i, el_val_t d) { return i - d; } +el_val_t el_instant_diff(el_val_t a, el_val_t b) { return a - b; } +el_val_t el_duration_add(el_val_t a, el_val_t b) { return a + b; } +el_val_t el_duration_sub(el_val_t a, el_val_t b) { return a - b; } +el_val_t el_duration_scale(el_val_t d, el_val_t s) { return d * s; } +el_val_t el_duration_div(el_val_t d, el_val_t s) { return s ? d / s : 0; } +el_val_t el_instant_lt(el_val_t a, el_val_t b) { return a < b; } +el_val_t el_instant_le(el_val_t a, el_val_t b) { return a <= b; } +el_val_t el_instant_gt(el_val_t a, el_val_t b) { return a > b; } +el_val_t el_instant_ge(el_val_t a, el_val_t b) { return a >= b; } +el_val_t el_instant_eq(el_val_t a, el_val_t b) { return a == b; } +el_val_t el_instant_ne(el_val_t a, el_val_t b) { return a != b; } +el_val_t el_duration_lt(el_val_t a, el_val_t b) { return a < b; } +el_val_t el_duration_le(el_val_t a, el_val_t b) { return a <= b; } +el_val_t el_duration_gt(el_val_t a, el_val_t b) { return a > b; } +el_val_t el_duration_ge(el_val_t a, el_val_t b) { return a >= b; } +el_val_t el_duration_eq(el_val_t a, el_val_t b) { return a == b; } +el_val_t el_duration_ne(el_val_t a, el_val_t b) { return a != b; } +el_val_t instant_to_unix_seconds(el_val_t i) { return i / 1000000000LL; } +el_val_t instant_to_unix_millis(el_val_t i) { return i / 1000000LL; } +el_val_t instant_to_iso8601(el_val_t i) { return el_wrap_str(el_strdup("")); } +el_val_t duration_to_seconds(el_val_t d) { return d / 1000000000LL; } +el_val_t duration_to_millis(el_val_t d) { return d / 1000000LL; } +el_val_t duration_to_nanos(el_val_t d) { return d; } + +/* ── Calendar stubs ──────────────────────────────────────────────────────── */ + +el_val_t zone(el_val_t id) { return id; } +el_val_t zone_utc(void) { return EL_STR("UTC"); } +el_val_t zone_local(void) { return EL_STR("local"); } +el_val_t zone_offset(el_val_t h, el_val_t m) { return 0; } +el_val_t earth_calendar(el_val_t z) { return 0; } +el_val_t earth_calendar_default(void) { return 0; } +el_val_t mars_calendar(void) { return 0; } +el_val_t cycle_calendar(el_val_t p) { return 0; } +el_val_t no_cycle_calendar(void) { return 0; } +el_val_t relative_calendar(el_val_t e) { return 0; } +el_val_t now_in(el_val_t cal) { return 0; } +el_val_t in_calendar(el_val_t inst, el_val_t cal) { return 0; } +el_val_t cal_format(el_val_t ct, el_val_t pattern) { return el_wrap_str(el_strdup("")); } +el_val_t cal_to_instant(el_val_t ct) { return 0; } +el_val_t cal_cycle_phase(el_val_t ct) { return 0; } +el_val_t cal_in(el_val_t ct, el_val_t cal) { return 0; } +el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) { return 0; } +el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) { return 0; } +el_val_t local_datetime(el_val_t date, el_val_t time) { return 0; } +el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal) { return 0; } +el_val_t local_date_year(el_val_t ld) { return 0; } +el_val_t local_date_month(el_val_t ld) { return 0; } +el_val_t local_date_day(el_val_t ld) { return 0; } +el_val_t local_time_hour(el_val_t lt) { return 0; } +el_val_t local_time_minute(el_val_t lt) { return 0; } +el_val_t local_time_second(el_val_t lt) { return 0; } +el_val_t local_time_nanos(el_val_t lt) { return 0; } +el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur) { return ld; } +el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur) { return lt; } +el_val_t el_local_date_lt(el_val_t a, el_val_t b) { return a < b; } +el_val_t el_local_date_eq(el_val_t a, el_val_t b) { return a == b; } +el_val_t rhythm_cycle_start(void) { return 0; } +el_val_t rhythm_cycle_phase(el_val_t p) { return 0; } +el_val_t rhythm_duration(el_val_t d) { return 0; } +el_val_t rhythm_session_start(void) { return 0; } +el_val_t rhythm_event(el_val_t name) { return 0; } +el_val_t rhythm_and(el_val_t a, el_val_t b) { return 0; } +el_val_t rhythm_or(el_val_t a, el_val_t b) { return 0; } +el_val_t rhythm_weekday(el_val_t day) { return 0; } +el_val_t rhythm_weekly_at(el_val_t day, el_val_t h, el_val_t m) { return 0; } +el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal) { return 0; } +el_val_t rhythm_matches(el_val_t r, el_val_t ct) { return 0; } + +/* ── UUID stubs ──────────────────────────────────────────────────────────── */ + +el_val_t uuid_new(void) { return el_wrap_str(el_strdup("00000000-0000-0000-0000-000000000000")); } +el_val_t uuid_v4(void) { return uuid_new(); } + +/* ── State K/V stubs ─────────────────────────────────────────────────────── */ + +el_val_t state_set(el_val_t key, el_val_t value) { return value; } +el_val_t state_get(el_val_t key) { return 0; } +el_val_t state_del(el_val_t key) { return 0; } +el_val_t state_keys(void) { return el_wrap_str(el_strdup("[]")); } +el_val_t state_has(el_val_t key) { return 0; } +el_val_t state_get_or(el_val_t key, el_val_t def) { return def; } + +/* ── TTL cache stubs ─────────────────────────────────────────────────────── */ + +el_val_t ttl_cache_set(el_val_t key, el_val_t value) { return value; } +el_val_t ttl_cache_get(el_val_t key, el_val_t max_age) { return 0; } +el_val_t ttl_cache_age(el_val_t key) { return -1; } diff --git a/lang/el-compiler/runtime/el_sdl2.c b/lang/el-compiler/runtime/el_sdl2.c new file mode 100644 index 0000000..6f9a70d --- /dev/null +++ b/lang/el-compiler/runtime/el_sdl2.c @@ -0,0 +1,1360 @@ +/* + * el_sdl2.c — SDL2 backend for the el native widget system. + * + * This file implements the SDL2 widget layer that el_seed.c calls through to + * when EL_TARGET_SDL2 is defined. It is the embedded/Pi counterpart to + * el_appkit.m (macOS) and el_gtk4.c (Linux desktop). + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_sdl2_* C functions declared here + * → SDL2 pixel drawing (SDL_RenderFillRect, TTF_RenderUTF8_Blended) + * + * SDL2 has no widget toolkit — it only draws pixels. This bridge implements + * widgets as drawn primitives: rectangles, text, borders. Each "widget" is a + * logical node in a retained layout tree. On any state change, the whole scene + * re-renders via el_sdl2_layout + el_sdl2_render_widget. + * + * Widget handles: every widget is an int64_t slot index into g_widgets[]. + * Slot 0 is reserved; valid handles are 1..EL_SDL2_MAX_WIDGETS-1. -1 = invalid. + * + * Threading: SDL2 requires that all rendering happen on the thread that called + * SDL_Init. __native_run_loop runs on the main thread and never returns. + * Callback functions fired by the event loop are called synchronously from the + * same thread — no locking required. + * + * Font loading: DejaVu or Liberation Sans searched in standard Linux paths. + * Falls back to a NULL font (text omitted) if none found. + * + * Target: Raspberry Pi Zero 2W, 512 MB RAM, no desktop environment, raw + * framebuffer via SDL2's KMS/DRM or fbdev driver. + * + * Compile: + * gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o + * Link: + * $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl + */ + +#ifdef EL_TARGET_SDL2 + +#include +#include +#include +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Widget type constants ────────────────────────────────────────────────── */ + +#define EL_WIDGET_NONE 0 +#define EL_WIDGET_WINDOW 1 +#define EL_WIDGET_VSTACK 2 +#define EL_WIDGET_HSTACK 3 +#define EL_WIDGET_ZSTACK 4 +#define EL_WIDGET_SCROLL 5 +#define EL_WIDGET_LABEL 6 +#define EL_WIDGET_BUTTON 7 +#define EL_WIDGET_TEXT_FIELD 8 +#define EL_WIDGET_TEXT_AREA 9 +#define EL_WIDGET_IMAGE 10 +#define EL_WIDGET_DIVIDER 11 +#define EL_WIDGET_SPACER 12 + +/* Layout direction stored in layout_type */ +#define EL_LAYOUT_NONE 0 +#define EL_LAYOUT_VSTACK 1 +#define EL_LAYOUT_HSTACK 2 +#define EL_LAYOUT_ZSTACK 3 + +#define EL_SDL2_MAX_WIDGETS 4096 + +/* ── Widget struct ────────────────────────────────────────────────────────── */ + +typedef struct { + int type; /* EL_WIDGET_* */ + SDL_Rect bounds; /* computed layout position */ + char text[512]; /* label text, button label, placeholder, img path */ + char cb_click[256]; /* El callback fn name — button click / text submit */ + char cb_change[256]; /* El callback fn name — text change */ + char cb_submit[256]; /* El callback fn name — text submit (stored here, mirrored to cb_click) */ + /* foreground colour */ + uint8_t fg_r, fg_g, fg_b, fg_a; + /* background colour */ + uint8_t bg_r, bg_g, bg_b, bg_a; + int font_size; + char font_family[128]; + int font_bold; + int padding_t, padding_r, padding_b, padding_l; + int fixed_width, fixed_height; /* 0 = auto */ + int flex; + int corner_radius; + int disabled; + int hidden; + /* tree */ + int children[64]; + int child_count; + int parent; /* -1 = root */ + /* layout when this node IS a container */ + int layout_type; + int layout_spacing; + /* text field / text area state */ + char input_buf[1024]; + int input_focused; + int cursor_pos; + /* hover state for button highlight */ + int hovered; + /* cached texture for image (freed on destroy) */ + SDL_Texture *img_texture; +} ElWidget; + +/* ── Globals ──────────────────────────────────────────────────────────────── */ + +static ElWidget g_widgets[EL_SDL2_MAX_WIDGETS]; +static int g_slot_used[EL_SDL2_MAX_WIDGETS]; +static SDL_Window *g_sdl_window = NULL; +static SDL_Renderer *g_renderer = NULL; +static TTF_Font *g_font_default = NULL; +static int g_root_widget = -1; +static int g_focused_widget = -1; +static int g_dirty = 1; /* 1 = needs redraw */ +static int g_win_w = 800; +static int g_win_h = 600; + +/* ── Slot management ──────────────────────────────────────────────────────── */ + +static int64_t el_sdl2_alloc_slot(int type) { + for (int i = 1; i < EL_SDL2_MAX_WIDGETS; i++) { + if (!g_slot_used[i]) { + g_slot_used[i] = 1; + ElWidget *w = &g_widgets[i]; + memset(w, 0, sizeof(*w)); + w->type = type; + w->parent = -1; + w->layout_type = EL_LAYOUT_NONE; + /* default colours: white fg, transparent bg */ + w->fg_r = 255; w->fg_g = 255; w->fg_b = 255; w->fg_a = 255; + w->bg_r = 0; w->bg_g = 0; w->bg_b = 0; w->bg_a = 0; + w->font_size = 14; + return (int64_t)i; + } + } + return -1; /* full */ +} + +static ElWidget *el_sdl2_get(int64_t handle) { + if (handle <= 0 || handle >= EL_SDL2_MAX_WIDGETS) return NULL; + if (!g_slot_used[handle]) return NULL; + return &g_widgets[handle]; +} + +static void el_sdl2_free_slot(int64_t handle) { + if (handle <= 0 || handle >= EL_SDL2_MAX_WIDGETS) return; + ElWidget *w = &g_widgets[handle]; + if (w->img_texture) { + SDL_DestroyTexture(w->img_texture); + w->img_texture = NULL; + } + g_slot_used[handle] = 0; +} + +/* ── Font loading ─────────────────────────────────────────────────────────── */ + +static TTF_Font *el_sdl2_load_font(int size) { + static const char *paths[] = { + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/TTF/DejaVuSans.ttf", + "/usr/share/fonts/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/freefont/FreeSans.ttf", + "/System/Library/Fonts/Helvetica.ttc", /* macOS dev */ + NULL + }; + for (int i = 0; paths[i]; i++) { + TTF_Font *f = TTF_OpenFont(paths[i], size); + if (f) return f; + } + return NULL; +} + +static TTF_Font *el_sdl2_get_font(int size) { + if (size <= 0) size = 14; + /* Simple approach: reuse the default font if size matches, else open again. + * For Pi Zero with one app, this is sufficient. */ + if (g_font_default && size == 14) return g_font_default; + return el_sdl2_load_font(size); +} + +/* ── Callback dispatch ────────────────────────────────────────────────────── */ + +static void el_sdl2_fire_cb(const char *fn_name, int64_t handle, const char *data) { + if (!fn_name || !fn_name[0]) return; + typedef el_val_t (*el_cb_fn)(el_val_t, el_val_t); + el_cb_fn fn = (el_cb_fn)(uintptr_t)dlsym(RTLD_DEFAULT, fn_name); + if (fn) { + el_val_t d = data ? (el_val_t)(uintptr_t)data : (el_val_t)0; + fn((el_val_t)handle, d); + g_dirty = 1; + } +} + +/* ── Rounded rectangle helpers ────────────────────────────────────────────── */ + +/* Draw a filled rounded rectangle approximation using SDL_RenderFillRect. + * The approach: draw the centre cross and fill the 4 corner arcs by drawing + * small squares step-by-step. Sufficient for Pi Zero where SDL_gfx may not + * be available. */ +static void el_sdl2_fill_rounded_rect(SDL_Renderer *r, const SDL_Rect *rect, + int radius, + uint8_t cr, uint8_t cg, uint8_t cb, uint8_t ca) { + if (radius <= 0 || rect->w <= 0 || rect->h <= 0) { + SDL_SetRenderDrawColor(r, cr, cg, cb, ca); + SDL_RenderFillRect(r, rect); + return; + } + if (radius > rect->w / 2) radius = rect->w / 2; + if (radius > rect->h / 2) radius = rect->h / 2; + + SDL_SetRenderDrawColor(r, cr, cg, cb, ca); + + /* Horizontal band (full width, inner height minus corners) */ + SDL_Rect horiz = {rect->x, rect->y + radius, rect->w, rect->h - 2 * radius}; + SDL_RenderFillRect(r, &horiz); + + /* Top band */ + SDL_Rect top = {rect->x + radius, rect->y, rect->w - 2 * radius, radius}; + SDL_RenderFillRect(r, &top); + + /* Bottom band */ + SDL_Rect bot = {rect->x + radius, rect->y + rect->h - radius, rect->w - 2 * radius, radius}; + SDL_RenderFillRect(r, &bot); + + /* Draw quarter circles at each corner using row-by-row filling. + * For each row dy in [0, radius), compute the chord width from circle eq. */ + for (int dy = 0; dy < radius; dy++) { + /* x-extent of circle arc at this dy from centre */ + double dx = radius - 1 - (int)(0.5 + (double)radius * + (1.0 - ((double)(radius - 1 - dy) * (double)(radius - 1 - dy)) / + ((double)radius * (double)radius))); + int fill_w = radius - (int)dx; + if (fill_w < 1) fill_w = 1; + + /* top-left corner */ + SDL_Rect tl = {rect->x + radius - fill_w, rect->y + dy, fill_w, 1}; + SDL_RenderFillRect(r, &tl); + /* top-right corner */ + SDL_Rect tr = {rect->x + rect->w - radius, rect->y + dy, fill_w, 1}; + SDL_RenderFillRect(r, &tr); + /* bottom-left corner */ + SDL_Rect bl = {rect->x + radius - fill_w, rect->y + rect->h - 1 - dy, fill_w, 1}; + SDL_RenderFillRect(r, &bl); + /* bottom-right corner */ + SDL_Rect br = {rect->x + rect->w - radius, rect->y + rect->h - 1 - dy, fill_w, 1}; + SDL_RenderFillRect(r, &br); + } +} + +/* Draw a 1px border around a (possibly rounded) rect */ +static void el_sdl2_draw_border(SDL_Renderer *r, const SDL_Rect *rect, + uint8_t cr, uint8_t cg, uint8_t cb, uint8_t ca) { + SDL_SetRenderDrawColor(r, cr, cg, cb, ca); + SDL_RenderDrawRect(r, rect); +} + +/* ── Layout engine ────────────────────────────────────────────────────────── */ + +/* Measure the natural size of a leaf widget. + * Returns 0 if size could not be determined. */ +static void el_sdl2_measure(int slot, int *out_w, int *out_h) { + ElWidget *w = el_sdl2_get(slot); + if (!w) { *out_w = 0; *out_h = 0; return; } + + int fs = w->font_size > 0 ? w->font_size : 14; + TTF_Font *font = el_sdl2_get_font(fs); + + switch (w->type) { + case EL_WIDGET_LABEL: { + if (font && w->text[0]) { + int tw = 0, th = 0; + TTF_SizeUTF8(font, w->text, &tw, &th); + *out_w = tw + w->padding_l + w->padding_r; + *out_h = th + w->padding_t + w->padding_b; + } else { + *out_w = 8 + w->padding_l + w->padding_r; + *out_h = fs + 4 + w->padding_t + w->padding_b; + } + break; + } + case EL_WIDGET_BUTTON: { + if (font && w->text[0]) { + int tw = 0, th = 0; + TTF_SizeUTF8(font, w->text, &tw, &th); + *out_w = tw + 32 + w->padding_l + w->padding_r; + *out_h = th + 20 + w->padding_t + w->padding_b; + } else { + *out_w = 80 + w->padding_l + w->padding_r; + *out_h = fs + 20 + w->padding_t + w->padding_b; + } + break; + } + case EL_WIDGET_TEXT_FIELD: + case EL_WIDGET_TEXT_AREA: { + *out_w = 200; /* default — will stretch with flex */ + *out_h = fs + 12 + w->padding_t + w->padding_b; + if (w->type == EL_WIDGET_TEXT_AREA) *out_h = (fs + 4) * 4 + w->padding_t + w->padding_b; + break; + } + case EL_WIDGET_IMAGE: { + *out_w = 64; + *out_h = 64; + break; + } + case EL_WIDGET_SPACER: { + *out_w = 0; + *out_h = 0; + break; + } + case EL_WIDGET_DIVIDER: { + *out_w = 2; + *out_h = 2; + break; + } + default: { + *out_w = 0; + *out_h = 0; + break; + } + } + + /* Fixed dimensions override natural size */ + if (w->fixed_width > 0) *out_w = w->fixed_width; + if (w->fixed_height > 0) *out_h = w->fixed_height; +} + +/* Forward declaration */ +static void el_sdl2_layout(int slot, SDL_Rect available); + +static void el_sdl2_layout_vstack(ElWidget *w, SDL_Rect inner) { + int spacing = w->layout_spacing; + int n = w->child_count; + if (n == 0) return; + + /* First pass: measure non-flex children and sum flex weights */ + int total_fixed_h = 0; + int total_flex = 0; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw || cw->hidden) continue; + if (cw->flex > 0) { + total_flex += cw->flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + total_fixed_h += mh; + } + } + /* Count spacing gaps */ + int gaps = (n > 1) ? (n - 1) * spacing : 0; + int flex_pool = inner.h - total_fixed_h - gaps; + if (flex_pool < 0) flex_pool = 0; + + /* Second pass: assign rects */ + int y = inner.y; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw) continue; + if (cw->hidden) { + SDL_Rect z = {0, 0, 0, 0}; + cw->bounds = z; + continue; + } + int ch; + if (cw->flex > 0 && total_flex > 0) { + ch = (flex_pool * cw->flex) / total_flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + ch = mh; + } + SDL_Rect child_rect = {inner.x, y, inner.w, ch}; + el_sdl2_layout(ci, child_rect); + y += ch + spacing; + } +} + +static void el_sdl2_layout_hstack(ElWidget *w, SDL_Rect inner) { + int spacing = w->layout_spacing; + int n = w->child_count; + if (n == 0) return; + + int total_fixed_w = 0; + int total_flex = 0; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw || cw->hidden) continue; + if (cw->flex > 0) { + total_flex += cw->flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + total_fixed_w += mw; + } + } + int gaps = (n > 1) ? (n - 1) * spacing : 0; + int flex_pool = inner.w - total_fixed_w - gaps; + if (flex_pool < 0) flex_pool = 0; + + int x = inner.x; + for (int i = 0; i < n; i++) { + int ci = w->children[i]; + ElWidget *cw = el_sdl2_get(ci); + if (!cw) continue; + if (cw->hidden) { + SDL_Rect z = {0, 0, 0, 0}; + cw->bounds = z; + continue; + } + int cw_size; + if (cw->flex > 0 && total_flex > 0) { + cw_size = (flex_pool * cw->flex) / total_flex; + } else { + int mw, mh; + el_sdl2_measure(ci, &mw, &mh); + cw_size = mw; + } + SDL_Rect child_rect = {x, inner.y, cw_size, inner.h}; + el_sdl2_layout(ci, child_rect); + x += cw_size + spacing; + } +} + +static void el_sdl2_layout(int slot, SDL_Rect available) { + ElWidget *w = el_sdl2_get(slot); + if (!w) return; + + if (w->hidden) { + SDL_Rect z = {0, 0, 0, 0}; + w->bounds = z; + return; + } + + /* Apply fixed dimensions if set */ + if (w->fixed_width > 0) available.w = w->fixed_width; + if (w->fixed_height > 0) available.h = w->fixed_height; + + w->bounds = available; + + /* For containers: compute inner rect after padding, then layout children */ + SDL_Rect inner = { + available.x + w->padding_l, + available.y + w->padding_t, + available.w - w->padding_l - w->padding_r, + available.h - w->padding_t - w->padding_b + }; + if (inner.w < 0) inner.w = 0; + if (inner.h < 0) inner.h = 0; + + switch (w->layout_type) { + case EL_LAYOUT_VSTACK: + el_sdl2_layout_vstack(w, inner); + break; + case EL_LAYOUT_HSTACK: + el_sdl2_layout_hstack(w, inner); + break; + case EL_LAYOUT_ZSTACK: + /* Each child gets the full inner rect */ + for (int i = 0; i < w->child_count; i++) { + el_sdl2_layout(w->children[i], inner); + } + break; + default: + /* Leaf: layout_type NONE means no children. + * WINDOW/SCROLL/etc. that has a content child: give it full inner. */ + if (w->type == EL_WIDGET_WINDOW || w->type == EL_WIDGET_SCROLL) { + for (int i = 0; i < w->child_count; i++) { + el_sdl2_layout(w->children[i], inner); + } + } + break; + } +} + +/* ── Render pass ──────────────────────────────────────────────────────────── */ + +static void el_sdl2_render_text_in_rect(TTF_Font *font, const char *text, + SDL_Color col, SDL_Rect dst) { + if (!font || !text || !text[0]) return; + SDL_Surface *surf = TTF_RenderUTF8_Blended(font, text, col); + if (!surf) return; + SDL_Texture *tex = SDL_CreateTextureFromSurface(g_renderer, surf); + SDL_FreeSurface(surf); + if (!tex) return; + + int tw, th; + SDL_QueryTexture(tex, NULL, NULL, &tw, &th); + + /* Clip to dst rect */ + SDL_Rect src_rect = {0, 0, tw, th}; + SDL_Rect dst_rect = dst; + /* Centre vertically; left-align horizontally */ + dst_rect.y = dst.y + (dst.h - th) / 2; + dst_rect.h = th; + dst_rect.w = tw; + if (dst_rect.w > dst.w) { + dst_rect.w = dst.w; + src_rect.w = dst.w; + } + + SDL_RenderSetClipRect(g_renderer, &dst); + SDL_RenderCopy(g_renderer, tex, &src_rect, &dst_rect); + SDL_RenderSetClipRect(g_renderer, NULL); + SDL_DestroyTexture(tex); +} + +static void el_sdl2_render_text_centered(TTF_Font *font, const char *text, + SDL_Color col, SDL_Rect dst) { + if (!font || !text || !text[0]) return; + SDL_Surface *surf = TTF_RenderUTF8_Blended(font, text, col); + if (!surf) return; + SDL_Texture *tex = SDL_CreateTextureFromSurface(g_renderer, surf); + SDL_FreeSurface(surf); + if (!tex) return; + + int tw, th; + SDL_QueryTexture(tex, NULL, NULL, &tw, &th); + + SDL_Rect src_rect = {0, 0, tw, th}; + if (src_rect.w > dst.w) src_rect.w = dst.w; + + SDL_Rect dst_rect; + dst_rect.x = dst.x + (dst.w - src_rect.w) / 2; + dst_rect.y = dst.y + (dst.h - th) / 2; + dst_rect.w = src_rect.w; + dst_rect.h = th; + + SDL_RenderSetClipRect(g_renderer, &dst); + SDL_RenderCopy(g_renderer, tex, &src_rect, &dst_rect); + SDL_RenderSetClipRect(g_renderer, NULL); + SDL_DestroyTexture(tex); +} + +/* Forward declaration */ +static void el_sdl2_render_widget(int slot); + +static void el_sdl2_render_widget(int slot) { + ElWidget *w = el_sdl2_get(slot); + if (!w || w->hidden) return; + + SDL_Rect b = w->bounds; + if (b.w <= 0 || b.h <= 0) return; + + int fs = w->font_size > 0 ? w->font_size : 14; + TTF_Font *font = el_sdl2_get_font(fs); + SDL_Color fg = {w->fg_r, w->fg_g, w->fg_b, w->fg_a}; + + /* ── Draw background ── */ + if (w->bg_a > 0) { + el_sdl2_fill_rounded_rect(g_renderer, &b, w->corner_radius, + w->bg_r, w->bg_g, w->bg_b, w->bg_a); + } + + /* ── Widget-specific drawing ── */ + switch (w->type) { + case EL_WIDGET_WINDOW: { + /* Window just provides a background; children render themselves. */ + /* Fill entire window with bg (default dark) */ + SDL_Rect win_rect = {0, 0, g_win_w, g_win_h}; + uint8_t br = w->bg_a > 0 ? w->bg_r : 30; + uint8_t bg_g = w->bg_a > 0 ? w->bg_g : 30; + uint8_t bb = w->bg_a > 0 ? w->bg_b : 30; + SDL_SetRenderDrawColor(g_renderer, br, bg_g, bb, 255); + SDL_RenderFillRect(g_renderer, &win_rect); + break; + } + + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + case EL_WIDGET_ZSTACK: + case EL_WIDGET_SCROLL: + /* Containers: bg already drawn above. Children rendered below. */ + break; + + case EL_WIDGET_LABEL: { + /* Text content */ + SDL_Rect inner = { + b.x + w->padding_l, + b.y + w->padding_t, + b.w - w->padding_l - w->padding_r, + b.h - w->padding_t - w->padding_b + }; + el_sdl2_render_text_in_rect(font, w->text, fg, inner); + break; + } + + case EL_WIDGET_BUTTON: { + /* Button body */ + uint8_t btn_r, btn_g, btn_b; + if (w->disabled) { + btn_r = 80; btn_g = 80; btn_b = 80; + } else if (w->hovered) { + btn_r = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_r + 40) : 100); + btn_g = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_g + 40) : 100); + btn_b = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_b + 40) : 140); + } else { + btn_r = w->bg_a > 0 ? w->bg_r : 60; + btn_g = w->bg_a > 0 ? w->bg_g : 60; + btn_b = w->bg_a > 0 ? w->bg_b : 100; + } + el_sdl2_fill_rounded_rect(g_renderer, &b, w->corner_radius > 0 ? w->corner_radius : 4, + btn_r, btn_g, btn_b, 255); + + /* Border */ + SDL_SetRenderDrawColor(g_renderer, 140, 140, 200, 255); + SDL_RenderDrawRect(g_renderer, &b); + + /* Centered label */ + SDL_Color label_col = {w->fg_r, w->fg_g, w->fg_b, w->disabled ? 128u : 255u}; + SDL_Rect inner = { + b.x + w->padding_l + 4, + b.y + w->padding_t, + b.w - w->padding_l - w->padding_r - 8, + b.h - w->padding_t - w->padding_b + }; + el_sdl2_render_text_centered(font, w->text, label_col, inner); + break; + } + + case EL_WIDGET_TEXT_FIELD: + case EL_WIDGET_TEXT_AREA: { + /* Background */ + uint8_t field_bg_r = 40, field_bg_g = 40, field_bg_b = 50; + if (w->bg_a > 0) { field_bg_r = w->bg_r; field_bg_g = w->bg_g; field_bg_b = w->bg_b; } + el_sdl2_fill_rounded_rect(g_renderer, &b, 3, + field_bg_r, field_bg_g, field_bg_b, 255); + + /* Border — highlighted when focused */ + if (w->input_focused) { + SDL_SetRenderDrawColor(g_renderer, 80, 140, 220, 255); + } else { + SDL_SetRenderDrawColor(g_renderer, 100, 100, 120, 255); + } + SDL_RenderDrawRect(g_renderer, &b); + + SDL_Rect inner = { + b.x + 6 + w->padding_l, + b.y + w->padding_t, + b.w - 12 - w->padding_l - w->padding_r, + b.h - w->padding_t - w->padding_b + }; + + const char *display = w->input_buf[0] ? w->input_buf : w->text; /* text = placeholder */ + SDL_Color text_col; + if (!w->input_buf[0] && w->text[0]) { + /* placeholder style: dimmed */ + text_col = (SDL_Color){120, 120, 140, 255}; + } else { + text_col = fg; + } + + el_sdl2_render_text_in_rect(font, display, text_col, inner); + + /* Draw cursor if focused */ + if (w->input_focused && font) { + /* Measure text up to cursor position */ + char before_cursor[1024]; + int cplen = w->cursor_pos; + if (cplen < 0) cplen = 0; + int buflen = (int)strlen(w->input_buf); + if (cplen > buflen) cplen = buflen; + memcpy(before_cursor, w->input_buf, cplen); + before_cursor[cplen] = '\0'; + int cx = 0, cy_unused = 0; + if (before_cursor[0]) { + TTF_SizeUTF8(font, before_cursor, &cx, &cy_unused); + } + int cursor_x = inner.x + cx; + int cursor_y1 = b.y + 3; + int cursor_y2 = b.y + b.h - 3; + SDL_SetRenderDrawColor(g_renderer, 200, 200, 255, 255); + SDL_RenderDrawLine(g_renderer, cursor_x, cursor_y1, cursor_x, cursor_y2); + } + break; + } + + case EL_WIDGET_IMAGE: { + /* Load or reuse cached texture */ + if (!w->img_texture && w->text[0]) { + SDL_Surface *surf = IMG_Load(w->text); + if (!surf) { + /* Try as raw SDL surface */ + surf = SDL_LoadBMP(w->text); + } + if (surf) { + w->img_texture = SDL_CreateTextureFromSurface(g_renderer, surf); + SDL_FreeSurface(surf); + } + } + if (w->img_texture) { + SDL_RenderCopy(g_renderer, w->img_texture, NULL, &b); + } else { + /* Placeholder: grey box with X */ + SDL_SetRenderDrawColor(g_renderer, 80, 80, 80, 255); + SDL_RenderFillRect(g_renderer, &b); + SDL_SetRenderDrawColor(g_renderer, 140, 140, 140, 255); + SDL_RenderDrawLine(g_renderer, b.x, b.y, b.x + b.w, b.y + b.h); + SDL_RenderDrawLine(g_renderer, b.x + b.w, b.y, b.x, b.y + b.h); + } + break; + } + + case EL_WIDGET_DIVIDER: { + SDL_SetRenderDrawColor(g_renderer, 80, 80, 80, 255); + SDL_RenderFillRect(g_renderer, &b); + break; + } + + case EL_WIDGET_SPACER: + default: + break; + } + + /* ── Recurse into children ── */ + for (int i = 0; i < w->child_count; i++) { + el_sdl2_render_widget(w->children[i]); + } +} + +static void el_sdl2_render(void) { + /* Clear with dark background */ + SDL_SetRenderDrawColor(g_renderer, 30, 30, 30, 255); + SDL_RenderClear(g_renderer); + + if (g_root_widget >= 0) { + el_sdl2_render_widget(g_root_widget); + } +} + +/* ── Event handling ───────────────────────────────────────────────────────── */ + +/* Hit-test: find the deepest interactive widget at (x, y). + * Returns slot index or -1. */ +static int el_sdl2_hit_test(int slot, int x, int y) { + ElWidget *w = el_sdl2_get(slot); + if (!w || w->hidden) return -1; + + SDL_Rect b = w->bounds; + if (x < b.x || x >= b.x + b.w) return -1; + if (y < b.y || y >= b.y + b.h) return -1; + + /* Check children first (front-to-back; last child = topmost) */ + for (int i = w->child_count - 1; i >= 0; i--) { + int r = el_sdl2_hit_test(w->children[i], x, y); + if (r >= 0) return r; + } + + /* This widget itself is a candidate if interactive */ + if (w->type == EL_WIDGET_BUTTON || + w->type == EL_WIDGET_TEXT_FIELD || + w->type == EL_WIDGET_TEXT_AREA) { + return slot; + } + + return -1; +} + +/* Clear hover on all buttons */ +static void el_sdl2_clear_hover(int slot) { + ElWidget *w = el_sdl2_get(slot); + if (!w) return; + if (w->type == EL_WIDGET_BUTTON) w->hovered = 0; + for (int i = 0; i < w->child_count; i++) { + el_sdl2_clear_hover(w->children[i]); + } +} + +static void el_sdl2_handle_event(SDL_Event *e) { + switch (e->type) { + case SDL_QUIT: + exit(0); + break; + + case SDL_WINDOWEVENT: + if (e->window.event == SDL_WINDOWEVENT_RESIZED || + e->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + g_win_w = e->window.data1; + g_win_h = e->window.data2; + /* Re-layout root */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; + } + break; + + case SDL_MOUSEBUTTONDOWN: { + if (e->button.button != SDL_BUTTON_LEFT) break; + int mx = e->button.x; + int my = e->button.y; + + /* Unfocus previous text field */ + if (g_focused_widget >= 0) { + ElWidget *fw = el_sdl2_get(g_focused_widget); + if (fw) fw->input_focused = 0; + } + g_focused_widget = -1; + + int hit = (g_root_widget >= 0) ? el_sdl2_hit_test(g_root_widget, mx, my) : -1; + if (hit < 0) break; + + ElWidget *hw = el_sdl2_get(hit); + if (!hw) break; + + if (hw->type == EL_WIDGET_BUTTON && !hw->disabled) { + el_sdl2_fire_cb(hw->cb_click, (int64_t)hit, ""); + } else if (hw->type == EL_WIDGET_TEXT_FIELD || + hw->type == EL_WIDGET_TEXT_AREA) { + hw->input_focused = 1; + g_focused_widget = hit; + SDL_StartTextInput(); + } + g_dirty = 1; + break; + } + + case SDL_MOUSEMOTION: { + if (g_root_widget < 0) break; + int mx = e->motion.x; + int my = e->motion.y; + int prev_hover = -1; + /* Find any currently hovered button */ + for (int i = 1; i < EL_SDL2_MAX_WIDGETS; i++) { + if (g_slot_used[i] && g_widgets[i].type == EL_WIDGET_BUTTON && + g_widgets[i].hovered) { + prev_hover = i; + break; + } + } + /* Clear all hover */ + el_sdl2_clear_hover(g_root_widget); + /* Set hover on current button under cursor */ + int hit = el_sdl2_hit_test(g_root_widget, mx, my); + if (hit >= 0) { + ElWidget *hw = el_sdl2_get(hit); + if (hw && hw->type == EL_WIDGET_BUTTON && !hw->disabled) { + hw->hovered = 1; + } + } + if (hit != prev_hover) g_dirty = 1; + break; + } + + case SDL_KEYDOWN: { + if (g_focused_widget < 0) break; + ElWidget *fw = el_sdl2_get(g_focused_widget); + if (!fw) break; + + SDL_Keycode key = e->key.keysym.sym; + + if (key == SDLK_BACKSPACE) { + int len = (int)strlen(fw->input_buf); + if (len > 0 && fw->cursor_pos > 0) { + /* Remove character before cursor */ + int cp = fw->cursor_pos; + memmove(fw->input_buf + cp - 1, fw->input_buf + cp, + (size_t)(len - cp + 1)); + fw->cursor_pos--; + el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf); + } + g_dirty = 1; + } else if (key == SDLK_DELETE) { + int len = (int)strlen(fw->input_buf); + if (fw->cursor_pos < len) { + memmove(fw->input_buf + fw->cursor_pos, + fw->input_buf + fw->cursor_pos + 1, + (size_t)(len - fw->cursor_pos)); + el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf); + g_dirty = 1; + } + } else if (key == SDLK_LEFT) { + if (fw->cursor_pos > 0) { fw->cursor_pos--; g_dirty = 1; } + } else if (key == SDLK_RIGHT) { + int len = (int)strlen(fw->input_buf); + if (fw->cursor_pos < len) { fw->cursor_pos++; g_dirty = 1; } + } else if (key == SDLK_HOME) { + fw->cursor_pos = 0; g_dirty = 1; + } else if (key == SDLK_END) { + fw->cursor_pos = (int)strlen(fw->input_buf); g_dirty = 1; + } else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) { + if (fw->type == EL_WIDGET_TEXT_FIELD) { + /* Submit */ + el_sdl2_fire_cb(fw->cb_submit[0] ? fw->cb_submit : fw->cb_click, + (int64_t)g_focused_widget, fw->input_buf); + g_dirty = 1; + } else { + /* Text area: insert newline via SDL_TEXTINPUT */ + } + } else if (key == SDLK_TAB) { + /* Blur current field */ + fw->input_focused = 0; + SDL_StopTextInput(); + g_focused_widget = -1; + g_dirty = 1; + } + break; + } + + case SDL_TEXTINPUT: { + if (g_focused_widget < 0) break; + ElWidget *fw = el_sdl2_get(g_focused_widget); + if (!fw) break; + + const char *inp = e->text.text; + int inp_len = (int)strlen(inp); + int buf_len = (int)strlen(fw->input_buf); + int max_buf = (int)sizeof(fw->input_buf) - 1; + + if (buf_len + inp_len <= max_buf) { + /* Insert at cursor position */ + int cp = fw->cursor_pos; + memmove(fw->input_buf + cp + inp_len, fw->input_buf + cp, + (size_t)(buf_len - cp + 1)); + memcpy(fw->input_buf + cp, inp, (size_t)inp_len); + fw->cursor_pos += inp_len; + el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf); + g_dirty = 1; + } + break; + } + + default: + break; + } +} + +/* ── Public API — called from el_seed.c wrappers ──────────────────────────── */ + +void el_sdl2_init(void) { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { + fprintf(stderr, "el_sdl2: SDL_Init failed: %s\n", SDL_GetError()); + exit(1); + } + if (TTF_Init() != 0) { + fprintf(stderr, "el_sdl2: TTF_Init failed: %s\n", TTF_GetError()); + exit(1); + } + if (!(IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG) & (IMG_INIT_PNG | IMG_INIT_JPG))) { + fprintf(stderr, "el_sdl2: IMG_Init warning: %s\n", IMG_GetError()); + /* Non-fatal: images won't load, but text UI still works */ + } + + /* Load default font */ + g_font_default = el_sdl2_load_font(14); + if (!g_font_default) { + fprintf(stderr, "el_sdl2: warning: no system font found; text will not render\n"); + } +} + +void el_sdl2_run_loop(void) { + SDL_Event event; + while (1) { + while (SDL_PollEvent(&event)) { + el_sdl2_handle_event(&event); + } + if (g_dirty) { + el_sdl2_render(); + SDL_RenderPresent(g_renderer); + g_dirty = 0; + } + SDL_Delay(16); /* ~60 fps cap */ + } +} + +int64_t el_sdl2_window_create(const char *title, int width, int height, + int min_width, int min_height) { + g_win_w = width > 0 ? width : 800; + g_win_h = height > 0 ? height : 600; + + Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; + + /* On Pi with no desktop: use fullscreen desktop if w/h not specified */ + g_sdl_window = SDL_CreateWindow( + title ? title : "", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + g_win_w, g_win_h, flags); + + if (!g_sdl_window) { + fprintf(stderr, "el_sdl2: SDL_CreateWindow failed: %s\n", SDL_GetError()); + return -1; + } + + if (min_width > 0 || min_height > 0) { + SDL_SetWindowMinimumSize(g_sdl_window, min_width, min_height); + } + + g_renderer = SDL_CreateRenderer(g_sdl_window, -1, + SDL_RENDERER_ACCELERATED | + SDL_RENDERER_PRESENTVSYNC); + if (!g_renderer) { + /* Fall back to software renderer (required on Pi Zero fbdev) */ + g_renderer = SDL_CreateRenderer(g_sdl_window, -1, SDL_RENDERER_SOFTWARE); + } + if (!g_renderer) { + fprintf(stderr, "el_sdl2: SDL_CreateRenderer failed: %s\n", SDL_GetError()); + return -1; + } + + SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND); + + int64_t handle = el_sdl2_alloc_slot(EL_WIDGET_WINDOW); + if (handle < 0) return -1; + + ElWidget *w = el_sdl2_get(handle); + snprintf(w->text, sizeof(w->text), "%s", title ? title : ""); + w->layout_type = EL_LAYOUT_VSTACK; + w->layout_spacing = 0; + w->bounds.x = 0; w->bounds.y = 0; + w->bounds.w = g_win_w; w->bounds.h = g_win_h; + + g_root_widget = (int)handle; + g_dirty = 1; + return handle; +} + +void el_sdl2_window_show(int64_t handle) { + if (g_sdl_window) { + SDL_ShowWindow(g_sdl_window); + } + /* Trigger initial layout */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_window_set_title(int64_t handle, const char *title) { + if (g_sdl_window) { + SDL_SetWindowTitle(g_sdl_window, title ? title : ""); + } + ElWidget *w = el_sdl2_get(handle); + if (w) snprintf(w->text, sizeof(w->text), "%s", title ? title : ""); + g_dirty = 1; +} + +int64_t el_sdl2_vstack_create(int spacing) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_VSTACK); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + w->layout_type = EL_LAYOUT_VSTACK; + w->layout_spacing = spacing; + return h; +} + +int64_t el_sdl2_hstack_create(int spacing) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_HSTACK); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + w->layout_type = EL_LAYOUT_HSTACK; + w->layout_spacing = spacing; + return h; +} + +int64_t el_sdl2_zstack_create(void) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_ZSTACK); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + w->layout_type = EL_LAYOUT_ZSTACK; + return h; +} + +int64_t el_sdl2_scroll_create(void) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_SCROLL); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + /* Scroll: single content child; layout passes full inner rect */ + w->layout_type = EL_LAYOUT_NONE; + return h; +} + +int64_t el_sdl2_label_create(const char *text) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_LABEL); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", text ? text : ""); + return h; +} + +int64_t el_sdl2_button_create(const char *label) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_BUTTON); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", label ? label : ""); + w->padding_t = 4; w->padding_b = 4; + w->padding_l = 8; w->padding_r = 8; + return h; +} + +int64_t el_sdl2_text_field_create(const char *placeholder) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_TEXT_FIELD); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + /* text field: text[] holds the placeholder */ + snprintf(w->text, sizeof(w->text), "%s", placeholder ? placeholder : ""); + w->padding_t = 2; w->padding_b = 2; + w->padding_l = 4; w->padding_r = 4; + return h; +} + +int64_t el_sdl2_text_area_create(const char *placeholder) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_TEXT_AREA); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", placeholder ? placeholder : ""); + w->padding_t = 4; w->padding_b = 4; + w->padding_l = 6; w->padding_r = 6; + return h; +} + +int64_t el_sdl2_image_create(const char *path_or_name) { + int64_t h = el_sdl2_alloc_slot(EL_WIDGET_IMAGE); + if (h < 0) return -1; + ElWidget *w = el_sdl2_get(h); + snprintf(w->text, sizeof(w->text), "%s", path_or_name ? path_or_name : ""); + return h; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_sdl2_widget_set_text(int64_t handle, const char *text) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + if (w->type == EL_WIDGET_TEXT_FIELD || w->type == EL_WIDGET_TEXT_AREA) { + /* Setting text on a field = set input buffer content */ + snprintf(w->input_buf, sizeof(w->input_buf), "%s", text ? text : ""); + w->cursor_pos = (int)strlen(w->input_buf); + } else { + snprintf(w->text, sizeof(w->text), "%s", text ? text : ""); + /* Invalidate image texture if path changed */ + if (w->type == EL_WIDGET_IMAGE && w->img_texture) { + SDL_DestroyTexture(w->img_texture); + w->img_texture = NULL; + } + } + g_dirty = 1; +} + +const char *el_sdl2_widget_get_text(int64_t handle) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return ""; + if (w->type == EL_WIDGET_TEXT_FIELD || w->type == EL_WIDGET_TEXT_AREA) { + return w->input_buf; + } + return w->text; +} + +void el_sdl2_widget_set_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->fg_r = (uint8_t)(r * 255.0f); + w->fg_g = (uint8_t)(g * 255.0f); + w->fg_b = (uint8_t)(b * 255.0f); + w->fg_a = (uint8_t)(a * 255.0f); + g_dirty = 1; +} + +void el_sdl2_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->bg_r = (uint8_t)(r * 255.0f); + w->bg_g = (uint8_t)(g * 255.0f); + w->bg_b = (uint8_t)(b * 255.0f); + w->bg_a = (uint8_t)(a * 255.0f); + g_dirty = 1; +} + +void el_sdl2_widget_set_font(int64_t handle, const char *family, int size, int bold) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + if (family) snprintf(w->font_family, sizeof(w->font_family), "%s", family); + if (size > 0) w->font_size = size; + w->font_bold = bold; + g_dirty = 1; +} + +void el_sdl2_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->padding_t = top; + w->padding_r = right; + w->padding_b = bottom; + w->padding_l = left; + /* Re-layout from root */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_width(int64_t handle, int width) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->fixed_width = width; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_height(int64_t handle, int height) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->fixed_height = height; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_flex(int64_t handle, int flex) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->flex = flex; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_set_corner_radius(int64_t handle, int radius) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->corner_radius = radius; + g_dirty = 1; +} + +void el_sdl2_widget_set_disabled(int64_t handle, int disabled) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->disabled = disabled; + g_dirty = 1; +} + +void el_sdl2_widget_set_hidden(int64_t handle, int hidden) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + w->hidden = hidden; + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +void el_sdl2_widget_add_child(int64_t parent, int64_t child) { + ElWidget *pw = el_sdl2_get(parent); + ElWidget *cw = el_sdl2_get(child); + if (!pw || !cw) return; + if (pw->child_count >= 64) return; /* max children per node */ + + /* Prevent duplicates */ + for (int i = 0; i < pw->child_count; i++) { + if (pw->children[i] == (int)child) return; + } + + pw->children[pw->child_count++] = (int)child; + cw->parent = (int)parent; + + /* Re-layout */ + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +void el_sdl2_widget_remove_child(int64_t parent, int64_t child) { + ElWidget *pw = el_sdl2_get(parent); + ElWidget *cw = el_sdl2_get(child); + if (!pw) return; + + for (int i = 0; i < pw->child_count; i++) { + if (pw->children[i] == (int)child) { + memmove(&pw->children[i], &pw->children[i + 1], + (size_t)(pw->child_count - i - 1) * sizeof(int)); + pw->child_count--; + if (cw) cw->parent = -1; + break; + } + } + + if (g_root_widget >= 0) { + SDL_Rect full = {0, 0, g_win_w, g_win_h}; + el_sdl2_layout(g_root_widget, full); + } + g_dirty = 1; +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_sdl2_widget_on_click(int64_t handle, const char *fn_name) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + snprintf(w->cb_click, sizeof(w->cb_click), "%s", fn_name ? fn_name : ""); +} + +void el_sdl2_widget_on_change(int64_t handle, const char *fn_name) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + snprintf(w->cb_change, sizeof(w->cb_change), "%s", fn_name ? fn_name : ""); +} + +void el_sdl2_widget_on_submit(int64_t handle, const char *fn_name) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + snprintf(w->cb_submit, sizeof(w->cb_submit), "%s", fn_name ? fn_name : ""); + /* Mirror into cb_click so RETURN key fires via cb_click fallback too */ + snprintf(w->cb_click, sizeof(w->cb_click), "%s", fn_name ? fn_name : ""); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_sdl2_widget_destroy(int64_t handle) { + ElWidget *w = el_sdl2_get(handle); + if (!w) return; + + /* Remove from parent */ + if (w->parent >= 0) { + el_sdl2_widget_remove_child((int64_t)w->parent, handle); + } + + /* Recursively destroy children */ + for (int i = 0; i < w->child_count; i++) { + el_sdl2_widget_destroy((int64_t)w->children[i]); + } + + if (g_focused_widget == (int)handle) { + g_focused_widget = -1; + SDL_StopTextInput(); + } + if (g_root_widget == (int)handle) { + g_root_widget = -1; + } + + el_sdl2_free_slot(handle); + g_dirty = 1; +} + +#endif /* EL_TARGET_SDL2 */ diff --git a/lang/el-compiler/runtime/el_uikit.m b/lang/el-compiler/runtime/el_uikit.m new file mode 100644 index 0000000..01a97a1 --- /dev/null +++ b/lang/el-compiler/runtime/el_uikit.m @@ -0,0 +1,1118 @@ +/* + * el_uikit.m — UIKit backend for the el native widget system. + * + * This file implements the iOS UIKit widget layer that el_seed.c calls + * through to when EL_TARGET_IOS is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_uikit_* C-callable functions declared here + * → UIView/UIWindow/UIStackView/UIButton/etc. via ObjC + * + * Widget handles: every widget (window, view, control) is assigned an int64_t + * slot index into _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved as null handle = -1 convention). + * + * Threading: All UIKit calls MUST run on the main thread. el_uikit_sync_main + * bounces work to the main thread synchronously if called from a background thread. + * + * Callback mechanism: when a widget fires an event (button tap, text change), + * the bridge looks up the registered callback function name, then calls + * dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string) + * This resolves the El function symbol at runtime, matching the __thread_create + * pattern already established in el_seed.c. + * + * iOS lifecycle: UIApplicationMain never returns. The el program's "main" body + * (window creation, layout, show) must execute inside applicationDidFinishLaunchingWithOptions. + * The caller sets el_main_entry_fn before calling __native_run_loop so the + * delegate can invoke it at the right time. + * + * Compile (MRC — no ARC; manual retain/release needed for struct-embedded id): + * clang -std=gnu11 -ObjC -fno-objc-arc -DEL_TARGET_IOS -framework UIKit \ + * -c el_uikit.m -o el_uikit.o + * Then link el_uikit.o alongside el_seed.c. + * + * Do NOT use -fobjc-arc: the widget table stores id in a plain C struct. ARC + * forbids explicit retain/release on struct-embedded object pointers and cannot + * insert automatic retain/release through C struct boundaries. MRC is the + * correct choice for this kind of C/ObjC bridge. + * + * Link flags required: + * -framework UIKit + */ + +#ifdef EL_TARGET_IOS + +#import +#import +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Widget table ─────────────────────────────────────────────────────────── */ + +#define EL_UIKIT_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, + EL_WIDGET_DIVIDER = 11, + EL_WIDGET_SPACER = 12, +} ElWidgetKind; + +typedef struct { + ElWidgetKind kind; + id obj; /* UIWindow* or UIView* subclass, retained */ + 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_UIKIT_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* Side table for ObjC delegate objects keyed by widget slot (MRC retained). */ +static id _el_delegates[EL_UIKIT_MAX_WIDGETS]; + +/* Allocate a slot and return its index. Returns -1 if full. */ +static int64_t el_widget_alloc(ElWidgetKind kind, id obj) { + for (int i = 1; i < EL_UIKIT_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + _el_widgets[i].kind = kind; + _el_widgets[i].obj = [obj retain]; + _el_widgets[i].cb_click = NULL; + _el_widgets[i].cb_change = NULL; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_UIKIT_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->obj release]; + w->obj = nil; + w->kind = EL_WIDGET_FREE; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; +} + +/* ── Dispatch helpers ─────────────────────────────────────────────────────── */ + +/* Run a block on the main thread, waiting for completion. Safe to call from + * any thread including the main thread. Uses dispatch_sync so callers can + * rely on the block having run before the function returns. */ +static void el_uikit_sync_main(void (^block)(void)) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. The El function must have the + * signature: fn handler(handle: Int, data: String) -> Void + * which compiles to: void fn_name(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_uikit_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) return; + ElCb2 fn = (ElCb2)sym; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── ObjC helper: ElEventProxy — button tap and text event target ─────────── */ + +/* + * ElEventProxy is a lightweight ObjC action target / delegate aggregator. + * One instance is allocated per widget that has events. Stored in _el_delegates[]. + */ +@interface ElEventProxy : NSObject +@property (nonatomic, assign) int64_t widgetHandle; +/* UIButton tap */ +- (void)elButtonTapped:(id)sender; +/* UITextField UIControlEventEditingChanged */ +- (void)elTextChanged:(id)sender; +@end + +@implementation ElEventProxy + +- (void)elButtonTapped:(id)sender { + (void)sender; + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + el_uikit_invoke_cb(w->cb_click, self.widgetHandle, ""); + } +} + +- (void)elTextChanged:(id)sender { + UITextField* tf = (UITextField*)sender; + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w) return; + const char* val = [tf.text UTF8String]; + if (w->cb_change) { + el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); + } +} + +/* UITextFieldDelegate — return key fires on_submit (stored in cb_click). */ +- (BOOL)textFieldShouldReturn:(UITextField*)textField { + ElWidget* w = el_widget_get(self.widgetHandle); + if (w && w->cb_click) { + const char* val = [textField.text UTF8String]; + el_uikit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : ""); + } + [textField resignFirstResponder]; + return YES; +} + +@end + +/* ── UITextView change delegate ──────────────────────────────────────────── */ + +@interface ElTextViewDelegate : NSObject +@property (nonatomic, assign) int64_t widgetHandle; +@end + +@implementation ElTextViewDelegate + +- (void)textViewDidChange:(UITextView*)textView { + ElWidget* w = el_widget_get(self.widgetHandle); + if (!w || !w->cb_change) return; + const char* val = [textView.text UTF8String]; + el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); +} + +@end + +/* ── iOS application lifecycle ───────────────────────────────────────────── */ + +/* + * el_main_entry_fn — set by el's main() (via __native_run_loop) before calling + * UIApplicationMain. The app delegate invokes it in didFinishLaunching so that + * the el program's window/widget setup runs at the correct lifecycle point. + */ +void (*el_main_entry_fn)(void) = NULL; + +/* argc/argv forwarded from el_runtime_init_args (or main). */ +static int _el_argc = 0; +static char** _el_argv = NULL; + +/* + * el_uikit_set_args — call from el_runtime_init_args / main() before + * __native_run_loop to forward argc/argv to UIApplicationMain. + */ +void el_uikit_set_args(int argc, char** argv) { + _el_argc = argc; + _el_argv = argv; +} + +@interface ElAppDelegate : UIResponder +@property (nonatomic, strong) UIWindow *keyWindow; +@end + +@implementation ElAppDelegate + +- (BOOL)application:(UIApplication*)app + didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { + (void)app; (void)launchOptions; + /* + * The el program called __native_init and set up el_main_entry_fn before + * UIApplicationMain was entered. We call it here so window creation and + * widget layout happen on the main thread at the right lifecycle moment. + */ + if (el_main_entry_fn) { + el_main_entry_fn(); + } + return YES; +} + +- (void)applicationWillResignActive:(UIApplication*)app { (void)app; } +- (void)applicationDidEnterBackground:(UIApplication*)app { (void)app; } +- (void)applicationWillEnterForeground:(UIApplication*)app { (void)app; } +- (void)applicationDidBecomeActive:(UIApplication*)app { (void)app; } + +@end + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_uikit_init — one-time initialisation. Idempotent. Call before any other + * el_uikit_* function. On iOS there is no UIApplication equivalent of + * [NSApplication sharedApplication] to call here — the real initialisation + * happens inside UIApplicationMain which is entered from el_uikit_run_loop. + * This function records that init was requested. + */ +static int _el_uikit_init_done = 0; + +void el_uikit_init(void) { + _el_uikit_init_done = 1; +} + +/* + * el_uikit_run_loop — enters UIApplicationMain. Never returns. + * Must be called from the process main thread as the final step of main(). + * Sets el_main_entry_fn to the provided function pointer before entering + * UIApplicationMain so the app delegate can call back into el code. + */ +void el_uikit_run_loop(void (*entry_fn)(void)) { + el_main_entry_fn = entry_fn; + UIApplicationMain(_el_argc, _el_argv, nil, @"ElAppDelegate"); +} + +/* + * el_uikit_window_create — create a UIWindow and configure a root + * UINavigationController + UIViewController. Returns window handle. + * + * On iOS there is exactly one full-screen window. width/height from the + * manifest are stored but the window always fills the screen. The title + * is applied to the root view controller's navigationItem. + */ +int64_t el_uikit_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; + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIScreen* screen = [UIScreen mainScreen]; + UIWindow* win = [[UIWindow alloc] initWithFrame:[screen bounds]]; + win.backgroundColor = [UIColor systemBackgroundColor]; + + /* Root view controller with a vertical UIStackView as content. */ + UIViewController* rootVC = [[UIViewController alloc] init]; + NSString* titleStr = [NSString stringWithUTF8String:title ? title : ""]; + rootVC.title = titleStr; + + UIStackView* rootStack = [[UIStackView alloc] initWithFrame:CGRectZero]; + rootStack.axis = UILayoutConstraintAxisVertical; + rootStack.alignment = UIStackViewAlignmentFill; + rootStack.distribution = UIStackViewDistributionFill; + rootStack.spacing = 0.0; + rootStack.translatesAutoresizingMaskIntoConstraints = NO; + [rootVC.view addSubview:rootStack]; + + /* Pin root stack to safe area. */ + UILayoutGuide* safe = rootVC.view.safeAreaLayoutGuide; + [NSLayoutConstraint activateConstraints:@[ + [rootStack.topAnchor constraintEqualToAnchor:safe.topAnchor], + [rootStack.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor], + [rootStack.trailingAnchor constraintEqualToAnchor:safe.trailingAnchor], + [rootStack.bottomAnchor constraintEqualToAnchor:safe.bottomAnchor], + ]]; + + UINavigationController* navCtrl = + [[UINavigationController alloc] initWithRootViewController:rootVC]; + win.rootViewController = navCtrl; + + /* Store the window. The root stack is accessible via + * ((UINavigationController*)win.rootViewController) + * .topViewController.view.subviews[0] + * but we reach it by casting obj to UIWindow* and digging at add_child time. */ + handle = el_widget_alloc(EL_WIDGET_WINDOW, win); + [win release]; + [rootVC release]; + [rootStack release]; + [navCtrl release]; + }); + return handle; +} + +/* + * el_uikit_window_show — make the window key and visible. + */ +void el_uikit_window_show(int64_t handle) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + UIWindow* win = (UIWindow*)w->obj; + [win makeKeyAndVisible]; + }); +} + +/* + * el_uikit_window_set_title — update nav bar title at runtime. + */ +void el_uikit_window_set_title(int64_t handle, const char* title) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW) return; + UIWindow* win = (UIWindow*)w->obj; + UINavigationController* nav = (UINavigationController*)win.rootViewController; + if ([nav isKindOfClass:[UINavigationController class]]) { + nav.topViewController.title = + [NSString stringWithUTF8String:title ? title : ""]; + } + }); +} + +/* ── Layout container factories ───────────────────────────────────────────── */ + +static int64_t make_uistack(UILayoutConstraintAxis axis, CGFloat spacing) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIStackView* sv = [[UIStackView alloc] initWithFrame:CGRectZero]; + sv.axis = axis; + sv.alignment = UIStackViewAlignmentFill; + sv.distribution = UIStackViewDistributionFill; + sv.spacing = spacing; + sv.translatesAutoresizingMaskIntoConstraints = NO; + ElWidgetKind kind = (axis == UILayoutConstraintAxisVertical) + ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; + handle = el_widget_alloc(kind, sv); + [sv release]; + }); + return handle; +} + +int64_t el_uikit_vstack_create(int spacing) { + return make_uistack(UILayoutConstraintAxisVertical, (CGFloat)spacing); +} + +int64_t el_uikit_hstack_create(int spacing) { + return make_uistack(UILayoutConstraintAxisHorizontal, (CGFloat)spacing); +} + +int64_t el_uikit_zstack_create(void) { + /* ZStack = plain UIView; children are added with addSubview and use + * Auto Layout or absolute frames set by the caller. */ + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIView* v = [[UIView alloc] initWithFrame:CGRectZero]; + v.translatesAutoresizingMaskIntoConstraints = NO; + handle = el_widget_alloc(EL_WIDGET_ZSTACK, v); + [v release]; + }); + return handle; +} + +int64_t el_uikit_scroll_create(void) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIScrollView* sv = [[UIScrollView alloc] initWithFrame:CGRectZero]; + sv.translatesAutoresizingMaskIntoConstraints = NO; + sv.alwaysBounceVertical = YES; + sv.showsHorizontalScrollIndicator = NO; + handle = el_widget_alloc(EL_WIDGET_SCROLL, sv); + [sv release]; + }); + return handle; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_uikit_label_create(const char* text) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UILabel* lbl = [[UILabel alloc] initWithFrame:CGRectZero]; + lbl.text = [NSString stringWithUTF8String:text ? text : ""]; + lbl.numberOfLines = 0; + lbl.translatesAutoresizingMaskIntoConstraints = NO; + handle = el_widget_alloc(EL_WIDGET_LABEL, lbl); + [lbl release]; + }); + return handle; +} + +int64_t el_uikit_button_create(const char* label) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIButton* btn = [UIButton buttonWithType:UIButtonTypeSystem]; + [btn setTitle:[NSString stringWithUTF8String:label ? label : ""] + forState:UIControlStateNormal]; + btn.translatesAutoresizingMaskIntoConstraints = NO; + + ElEventProxy* proxy = [[ElEventProxy alloc] init]; + [btn addTarget:proxy + action:@selector(elButtonTapped:) + forControlEvents:UIControlEventTouchUpInside]; + + int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn); + proxy.widgetHandle = h; + _el_delegates[h] = proxy; /* retain in side table */ + handle = h; + /* btn is autoreleased from buttonWithType: — no release needed */ + }); + return handle; +} + +int64_t el_uikit_text_field_create(const char* placeholder) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UITextField* tf = [[UITextField alloc] initWithFrame:CGRectZero]; + tf.translatesAutoresizingMaskIntoConstraints = NO; + if (placeholder && *placeholder) { + tf.placeholder = [NSString stringWithUTF8String:placeholder]; + } + tf.borderStyle = UITextBorderStyleRoundedRect; + tf.autocorrectionType = UITextAutocorrectionTypeNo; + + ElEventProxy* proxy = [[ElEventProxy alloc] init]; + tf.delegate = proxy; + [tf addTarget:proxy + action:@selector(elTextChanged:) + forControlEvents:UIControlEventEditingChanged]; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf); + proxy.widgetHandle = h; + _el_delegates[h] = proxy; + handle = h; + [tf release]; + }); + return handle; +} + +int64_t el_uikit_text_area_create(const char* placeholder) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UITextView* tv = [[UITextView alloc] initWithFrame:CGRectZero]; + tv.translatesAutoresizingMaskIntoConstraints = NO; + tv.font = [UIFont systemFontOfSize:14.0]; + tv.layer.borderColor = [[UIColor systemGray4Color] CGColor]; + tv.layer.borderWidth = 1.0; + tv.layer.cornerRadius = 6.0; + tv.clipsToBounds = YES; + /* UITextView has no native placeholder; set accessibility hint. */ + if (placeholder && *placeholder) { + tv.accessibilityHint = [NSString stringWithUTF8String:placeholder]; + } + + ElTextViewDelegate* del = [[ElTextViewDelegate alloc] init]; + tv.delegate = del; + + int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, tv); + del.widgetHandle = h; + _el_delegates[h] = del; + handle = h; + [tv release]; + }); + return handle; +} + +int64_t el_uikit_image_create(const char* path_or_name) { + __block int64_t handle = -1; + el_uikit_sync_main(^{ + UIImage* img = nil; + if (path_or_name && *path_or_name) { + /* Try filesystem path first, then bundle asset name. */ + img = [UIImage imageWithContentsOfFile: + [NSString stringWithUTF8String:path_or_name]]; + if (!img) { + img = [UIImage imageNamed: + [NSString stringWithUTF8String:path_or_name]]; + } + } + UIImageView* iv = [[UIImageView alloc] initWithFrame:CGRectZero]; + iv.translatesAutoresizingMaskIntoConstraints = NO; + iv.contentMode = UIViewContentModeScaleAspectFit; + if (img) { + iv.image = img; + } + handle = el_widget_alloc(EL_WIDGET_IMAGE, iv); + [iv release]; + }); + return handle; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_uikit_widget_set_text(int64_t handle, const char* text) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = [NSString stringWithUTF8String:text ? text : ""]; + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).text = s; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).text = s; + break; + case EL_WIDGET_BUTTON: + [(UIButton*)w->obj setTitle:s forState:UIControlStateNormal]; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).text = s; + break; + case EL_WIDGET_WINDOW: + el_uikit_window_set_title(handle, text); + break; + default: break; + } + }); +} + +const char* el_uikit_widget_get_text(int64_t handle) { + __block const char* result = ""; + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + NSString* s = nil; + switch (w->kind) { + case EL_WIDGET_LABEL: + s = ((UILabel*)w->obj).text; + break; + case EL_WIDGET_TEXTFIELD: + s = ((UITextField*)w->obj).text; + break; + case EL_WIDGET_BUTTON: + s = [(UIButton*)w->obj titleForState:UIControlStateNormal]; + break; + case EL_WIDGET_TEXTAREA: + s = ((UITextView*)w->obj).text; + break; + default: break; + } + if (s) result = strdup([s UTF8String]); + }); + return result; +} + +void el_uikit_widget_set_color(int64_t handle, float r, float g, float b, float a) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIColor* c = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).textColor = c; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).textColor = c; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).textColor = c; + break; + case EL_WIDGET_BUTTON: + [(UIButton*)w->obj setTitleColor:c forState:UIControlStateNormal]; + ((UIButton*)w->obj).tintColor = c; + break; + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: + case EL_WIDGET_ZSTACK: + ((UIView*)w->obj).backgroundColor = c; + break; + default: break; + } + }); +} + +void el_uikit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind == EL_WIDGET_WINDOW) { + UIWindow* win = (UIWindow*)w->obj; + win.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + return; + } + UIView* v = (UIView*)w->obj; + v.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0) + green:(CGFloat)(g / 255.0) + blue:(CGFloat)(b / 255.0) + alpha:(CGFloat)(a / 255.0)]; + }); +} + +void el_uikit_widget_set_font(int64_t handle, const char* family, int size, int bold) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIFont* font; + if (bold) { + font = [UIFont boldSystemFontOfSize:(CGFloat)size]; + } else if (family && *family && strcmp(family, "system") != 0) { + font = [UIFont fontWithName:[NSString stringWithUTF8String:family] + size:(CGFloat)size]; + if (!font) font = [UIFont systemFontOfSize:(CGFloat)size]; + } else { + font = [UIFont systemFontOfSize:(CGFloat)size]; + } + switch (w->kind) { + case EL_WIDGET_LABEL: + ((UILabel*)w->obj).font = font; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).font = font; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).font = font; + break; + case EL_WIDGET_BUTTON: + ((UIButton*)w->obj).titleLabel.font = font; + break; + default: break; + } + }); +} + +void el_uikit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + UIEdgeInsets insets = UIEdgeInsetsMake( + (CGFloat)top, (CGFloat)left, (CGFloat)bottom, (CGFloat)right); + if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) { + UIStackView* sv = (UIStackView*)w->obj; + sv.layoutMargins = insets; + sv.layoutMarginsRelativeArrangement = YES; + } else if (w->kind == EL_WIDGET_TEXTAREA) { + UITextView* tv = (UITextView*)w->obj; + tv.textContainerInset = insets; + } else { + UIView* v = (UIView*)w->obj; + v.layoutMargins = insets; + } + }); +} + +void el_uikit_widget_set_width(int64_t handle, int width) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + NSLayoutConstraint* c = [v.widthAnchor + constraintEqualToConstant:(CGFloat)width]; + c.priority = UILayoutPriorityDefaultHigh; + c.active = YES; + }); +} + +void el_uikit_widget_set_height(int64_t handle, int height) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + NSLayoutConstraint* c = [v.heightAnchor + constraintEqualToConstant:(CGFloat)height]; + c.priority = UILayoutPriorityDefaultHigh; + c.active = YES; + }); +} + +void el_uikit_widget_set_flex(int64_t handle, int flex) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + /* flex > 0 → low hugging priority so the view expands to fill space. + * flex == 0 → high hugging priority so the view stays at intrinsic size. */ + UILayoutPriority p = (flex > 0) + ? UILayoutPriorityDefaultLow + : UILayoutPriorityDefaultHigh; + [v setContentHuggingPriority:p + forAxis:UILayoutConstraintAxisHorizontal]; + [v setContentHuggingPriority:p + forAxis:UILayoutConstraintAxisVertical]; + }); +} + +void el_uikit_widget_set_corner_radius(int64_t handle, int radius) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + UIView* v = (UIView*)w->obj; + v.layer.cornerRadius = (CGFloat)radius; + v.clipsToBounds = YES; + }); +} + +void el_uikit_widget_set_disabled(int64_t handle, int disabled) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + switch (w->kind) { + case EL_WIDGET_BUTTON: + ((UIButton*)w->obj).enabled = !disabled; + break; + case EL_WIDGET_TEXTFIELD: + ((UITextField*)w->obj).enabled = !disabled; + break; + case EL_WIDGET_TEXTAREA: + ((UITextView*)w->obj).editable = !disabled; + break; + default: + ((UIView*)w->obj).userInteractionEnabled = !disabled; + break; + } + }); +} + +void el_uikit_widget_set_hidden(int64_t handle, int hidden) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w || w->kind == EL_WIDGET_WINDOW) return; + ((UIView*)w->obj).hidden = (BOOL)hidden; + }); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +/* + * el_uikit_widget_add_child — attach child widget to parent. + * + * - Window: child is added to the root UIStackView inside the root VC. + * - VStack/HStack: child is added as an arranged subview. + * - ZStack: child is added as a plain subview (caller manages layout). + * - ScrollView: child is added as a subview and pinned to content layout guide. + * - Generic view: child is added as a plain subview. + */ +void el_uikit_widget_add_child(int64_t parent, int64_t child) { + el_uikit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!pw || !cw) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* can't add window as subview */ + + UIView* childView = (UIView*)cw->obj; + + switch (pw->kind) { + case EL_WIDGET_WINDOW: { + /* Dig to the root stack view: window → navCtrl → topVC → view → subviews[0] */ + UIWindow* win = (UIWindow*)pw->obj; + UINavigationController* nav = + (UINavigationController*)win.rootViewController; + UIView* rootVCView = nav.topViewController.view; + UIStackView* stack = nil; + for (UIView* sub in rootVCView.subviews) { + if ([sub isKindOfClass:[UIStackView class]]) { + stack = (UIStackView*)sub; + break; + } + } + if (stack) { + [stack addArrangedSubview:childView]; + } else { + [rootVCView addSubview:childView]; + } + break; + } + case EL_WIDGET_VSTACK: + case EL_WIDGET_HSTACK: { + UIStackView* sv = (UIStackView*)pw->obj; + [sv addArrangedSubview:childView]; + break; + } + case EL_WIDGET_SCROLL: { + UIScrollView* sv = (UIScrollView*)pw->obj; + [sv addSubview:childView]; + /* Pin child to content layout guide so scroll view knows content size. */ + UILayoutGuide* cg = sv.contentLayoutGuide; + childView.translatesAutoresizingMaskIntoConstraints = NO; + [NSLayoutConstraint activateConstraints:@[ + [childView.topAnchor constraintEqualToAnchor:cg.topAnchor], + [childView.leadingAnchor constraintEqualToAnchor:cg.leadingAnchor], + [childView.trailingAnchor constraintEqualToAnchor:cg.trailingAnchor], + [childView.bottomAnchor constraintEqualToAnchor:cg.bottomAnchor], + /* Width matches scroll view's frame (vertical scroll only). */ + [childView.widthAnchor constraintEqualToAnchor:sv.frameLayoutGuide.widthAnchor], + ]]; + break; + } + case EL_WIDGET_ZSTACK: + default: { + UIView* pv = (UIView*)pw->obj; + [pv addSubview:childView]; + break; + } + } + }); +} + +/* + * el_uikit_widget_remove_child — detach a child widget from its parent. + */ +void el_uikit_widget_remove_child(int64_t parent, int64_t child) { + el_uikit_sync_main(^{ + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!cw || cw->kind == EL_WIDGET_WINDOW) return; + UIView* childView = (UIView*)cw->obj; + /* If parent is a stack view, remove from arranged subviews too. */ + if (pw && (pw->kind == EL_WIDGET_VSTACK || pw->kind == EL_WIDGET_HSTACK)) { + UIStackView* sv = (UIStackView*)pw->obj; + [sv removeArrangedSubview:childView]; + } else if (pw && pw->kind == EL_WIDGET_WINDOW) { + /* Find and remove from root stack. */ + UIWindow* win = (UIWindow*)pw->obj; + UINavigationController* nav = + (UINavigationController*)win.rootViewController; + for (UIView* sub in nav.topViewController.view.subviews) { + if ([sub isKindOfClass:[UIStackView class]]) { + [(UIStackView*)sub removeArrangedSubview:childView]; + break; + } + } + } + [childView removeFromSuperview]; + }); +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_uikit_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; +} + +void el_uikit_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; +} + +void el_uikit_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, submit = return key → stored in cb_click slot. */ + el_uikit_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_uikit_widget_destroy(int64_t handle) { + el_uikit_sync_main(^{ + ElWidget* w = el_widget_get(handle); + if (!w) return; + if (w->kind != EL_WIDGET_WINDOW) { + UIView* v = (UIView*)w->obj; + /* If it's in a stack view, also remove from arranged subviews. */ + if ([v.superview isKindOfClass:[UIStackView class]]) { + [(UIStackView*)v.superview removeArrangedSubview:v]; + } + [v removeFromSuperview]; + } + [_el_delegates[handle] release]; + _el_delegates[handle] = nil; + el_widget_free(handle); + }); +} + +/* ── Manifest reader ──────────────────────────────────────────────────────── */ + +/* + * el_uikit_manifest_read — read an app manifest file. Tries the given path + * first; if not found, looks in the main bundle. Returns a C string with the + * file contents (heap-allocated; caller must not free — matches AppKit bridge + * convention of returning static/permanent lifetime strings from manifest). + * Returns NULL on failure. + */ +const char* el_uikit_manifest_read(const char* path) { + if (!path || !*path) return NULL; + + NSString* pathStr = [NSString stringWithUTF8String:path]; + NSString* content = [NSString stringWithContentsOfFile:pathStr + encoding:NSUTF8StringEncoding + error:nil]; + if (!content) { + /* Try bundle resource lookup — strip to last component without extension. */ + NSString* baseName = [[pathStr lastPathComponent] + stringByDeletingPathExtension]; + NSString* ext = [pathStr pathExtension]; + NSString* bundlePath = [[NSBundle mainBundle] + pathForResource:baseName ofType:(ext.length ? ext : nil)]; + if (bundlePath) { + content = [NSString stringWithContentsOfFile:bundlePath + encoding:NSUTF8StringEncoding + error:nil]; + } + } + if (!content) return NULL; + return strdup([content UTF8String]); +} + +/* ── __widget_* C API (called from el_seed.c) ─────────────────────────────── */ +/* + * These are the symbols that el compiled programs actually call. They forward + * directly to the el_uikit_* implementations above. el_val_t is int64_t (the + * same type used by the AppKit bridge). + */ + +void __native_init(void) { + el_uikit_init(); +} + +void __native_run_loop(void) { + /* el_main_entry_fn must be set by the caller before this. On iOS the + * entry function pointer pattern is inverted vs macOS: the el program + * packages up its "build UI" work into a lambda / function and registers + * it via el_main_entry_fn, then calls __native_run_loop. + * el_uikit_run_loop takes that fn pointer and passes it to UIApplicationMain. */ + el_uikit_run_loop(el_main_entry_fn); +} + +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_uikit_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_uikit_window_show((int64_t)handle); +} + +void __window_set_title(el_val_t handle, el_val_t title) { + el_uikit_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_uikit_vstack_create((int)spacing); +} + +el_val_t __hstack_create(el_val_t spacing) { + return (el_val_t)el_uikit_hstack_create((int)spacing); +} + +el_val_t __zstack_create(void) { + return (el_val_t)el_uikit_zstack_create(); +} + +el_val_t __scroll_create(void) { + return (el_val_t)el_uikit_scroll_create(); +} + +el_val_t __label_create(el_val_t text) { + return (el_val_t)el_uikit_label_create((const char*)(uintptr_t)text); +} + +el_val_t __button_create(el_val_t label) { + return (el_val_t)el_uikit_button_create((const char*)(uintptr_t)label); +} + +el_val_t __text_field_create(el_val_t placeholder) { + return (el_val_t)el_uikit_text_field_create( + (const char*)(uintptr_t)placeholder); +} + +el_val_t __text_area_create(el_val_t placeholder) { + return (el_val_t)el_uikit_text_area_create( + (const char*)(uintptr_t)placeholder); +} + +el_val_t __image_create(el_val_t path_or_name) { + return (el_val_t)el_uikit_image_create( + (const char*)(uintptr_t)path_or_name); +} + +void __widget_set_text(el_val_t handle, el_val_t text) { + el_uikit_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_uikit_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_uikit_widget_set_color((int64_t)handle, + (float)r, (float)g, (float)b, (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_uikit_widget_set_bg_color((int64_t)handle, + (float)r, (float)g, (float)b, (float)a); +} + +void __widget_set_font(el_val_t handle, el_val_t family, + el_val_t size, el_val_t bold) { + el_uikit_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_uikit_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_uikit_widget_set_width((int64_t)handle, (int)width); +} + +void __widget_set_height(el_val_t handle, el_val_t height) { + el_uikit_widget_set_height((int64_t)handle, (int)height); +} + +void __widget_set_flex(el_val_t handle, el_val_t flex) { + el_uikit_widget_set_flex((int64_t)handle, (int)flex); +} + +void __widget_set_corner_radius(el_val_t handle, el_val_t radius) { + el_uikit_widget_set_corner_radius((int64_t)handle, (int)radius); +} + +void __widget_set_disabled(el_val_t handle, el_val_t disabled) { + el_uikit_widget_set_disabled((int64_t)handle, (int)disabled); +} + +void __widget_set_hidden(el_val_t handle, el_val_t hidden) { + el_uikit_widget_set_hidden((int64_t)handle, (int)hidden); +} + +void __widget_add_child(el_val_t parent, el_val_t child) { + el_uikit_widget_add_child((int64_t)parent, (int64_t)child); +} + +void __widget_remove_child(el_val_t parent, el_val_t child) { + el_uikit_widget_remove_child((int64_t)parent, (int64_t)child); +} + +void __widget_destroy(el_val_t handle) { + el_uikit_widget_destroy((int64_t)handle); +} + +void __widget_on_click(el_val_t handle, el_val_t fn_name) { + el_uikit_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_uikit_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_uikit_widget_on_submit((int64_t)handle, + (const char*)(uintptr_t)fn_name); +} + +el_val_t __manifest_read(el_val_t path) { + return (el_val_t)(uintptr_t)el_uikit_manifest_read( + (const char*)(uintptr_t)path); +} + +#endif /* EL_TARGET_IOS */ diff --git a/lang/el-compiler/runtime/el_win32.c b/lang/el-compiler/runtime/el_win32.c new file mode 100644 index 0000000..b99ea71 --- /dev/null +++ b/lang/el-compiler/runtime/el_win32.c @@ -0,0 +1,1363 @@ +/* + * el_win32.c — Win32 backend for the el native widget system. + * + * This file implements the Windows Win32 widget layer that el_seed.c calls + * through to when EL_TARGET_WIN32 is defined. + * + * Architecture: + * el program (el code) + * → __widget_* C builtins in el_seed.c + * → el_win32_* C functions declared here + * → HWND/Win32 API / GDI + * + * Widget handles: every widget (window, view, control) is assigned an int64_t + * slot index into _el_widgets[]. The el program holds these as opaque Int values. + * Slot 0 is never valid (reserved; handle -1 = invalid). + * + * Threading: Win32 UI must run on the main thread. el is single-threaded for + * native UI, so all calls are direct. The message loop runs on the main thread + * and never returns. + * + * Callback mechanism: when a widget fires an event (button click, text change), + * the bridge looks up the registered callback function name, then calls + * GetProcAddress(GetModuleHandle(NULL), fn_name)(widget_handle, event_data) + * This resolves the El function symbol at runtime, matching el_seed.c's + * dlsym(RTLD_DEFAULT, fn_name) pattern on POSIX. + * + * Layout: VStack and HStack containers maintain a child HWND list and reflow + * children on WM_SIZE using SetWindowPos. ZStack uses absolute positioning. + * + * Subclassing: SetWindowSubclass (comctl32) is used to intercept WM_COMMAND + * and WM_NOTIFY on container windows so button clicks and edit changes route + * back to el callbacks. + * + * String encoding: el strings are UTF-8 (const char*). All Win32 text APIs + * use wide strings (WCHAR*). Conversion via MultiByteToWideChar / + * WideCharToMultiByte throughout. + * + * Compile: + * cl /DEL_TARGET_WIN32 el_win32.c /c /Fo:el_win32.obj + * (link with: comctl32.lib user32.lib gdi32.lib) + * or with clang-cl: + * clang-cl /DEL_TARGET_WIN32 -c el_win32.c -o el_win32.obj + */ + +#ifdef EL_TARGET_WIN32 + +#define WIN32_LEAN_AND_MEAN +#ifndef UNICODE +#define UNICODE +#endif +#ifndef _UNICODE +#define _UNICODE +#endif + +#include +#include +#include +#include +#include +#include +#include "el_runtime.h" + +/* ── Compile-time linkage ─────────────────────────────────────────────────── */ +#pragma comment(lib, "comctl32.lib") +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "gdi32.lib") + +/* ── Custom window messages ───────────────────────────────────────────────── */ +/* WM_USER+1: posted to a container to trigger a layout reflow pass. */ +#define WM_EL_REFLOW (WM_USER + 1) + +/* ── Widget type enum ─────────────────────────────────────────────────────── */ + +#define EL_WIN32_MAX_WIDGETS 4096 +#define EL_WIN32_MAX_CHILDREN 256 + +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; + +/* Per-slot data. */ +typedef struct { + ElWidgetKind kind; + HWND hwnd; /* primary window handle; NULL if free */ + char* cb_click; /* El function name for click / submit events */ + char* cb_change; /* El function name for value-change events */ + + /* Layout data (used by VSTACK / HSTACK / ZSTACK / WINDOW containers) */ + int spacing; /* pixels between children */ + int pad_top; + int pad_right; + int pad_bottom; + int pad_left; + int flex; /* flex weight; 0 = fixed size */ + + /* Fixed size overrides (0 = not set) */ + int fixed_w; + int fixed_h; + + /* Child list for layout containers */ + int64_t children[EL_WIN32_MAX_CHILDREN]; + int child_count; + + /* Color state (stored for WM_CTLCOLOR* replies) */ + COLORREF fg_color; + COLORREF bg_color; + HBRUSH bg_brush; + int has_fg; + int has_bg; + + /* Font */ + HFONT font; /* NULL = default system font */ + + /* Image: HBITMAP loaded for SS_BITMAP static */ + HBITMAP hbmp; +} ElWidget; + +static ElWidget _el_widgets[EL_WIN32_MAX_WIDGETS]; +static int _el_widget_count = 0; + +/* ── Window class names ───────────────────────────────────────────────────── */ +static const WCHAR* EL_WNDCLASS_VSTACK = L"ElVStack"; +static const WCHAR* EL_WNDCLASS_HSTACK = L"ElHStack"; +static const WCHAR* EL_WNDCLASS_ZSTACK = L"ElZStack"; +static const WCHAR* EL_WNDCLASS_SCROLL = L"ElScroll"; +static const WCHAR* EL_WNDCLASS_WINDOW = L"ElWindow"; + +/* ── Forward declarations ─────────────────────────────────────────────────── */ +static void el_win32_reflow(int64_t container_handle); +static LRESULT CALLBACK el_container_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); +static LRESULT CALLBACK el_toplevel_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp); + +/* ── String helpers ───────────────────────────────────────────────────────── */ + +/* UTF-8 → WCHAR*. Caller must free(). Returns NULL on failure. */ +static WCHAR* el_utf8_to_wide(const char* s) { + if (!s) s = ""; + int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0); + if (n <= 0) return _wcsdup(L""); + WCHAR* buf = (WCHAR*)malloc((size_t)n * sizeof(WCHAR)); + if (!buf) return _wcsdup(L""); + MultiByteToWideChar(CP_UTF8, 0, s, -1, buf, n); + return buf; +} + +/* WCHAR* → UTF-8 char*. Caller must free(). Returns NULL on failure. */ +static char* el_wide_to_utf8(const WCHAR* ws) { + if (!ws) return strdup(""); + int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); + if (n <= 0) return strdup(""); + char* buf = (char*)malloc((size_t)n); + if (!buf) return strdup(""); + WideCharToMultiByte(CP_UTF8, 0, ws, -1, buf, n, NULL, NULL); + return buf; +} + +/* ── Slot management ──────────────────────────────────────────────────────── */ + +static int64_t el_widget_alloc(ElWidgetKind kind, HWND hwnd) { + for (int i = 1; i < EL_WIN32_MAX_WIDGETS; i++) { + if (_el_widgets[i].kind == EL_WIDGET_FREE) { + ElWidget* w = &_el_widgets[i]; + memset(w, 0, sizeof(ElWidget)); + w->kind = kind; + w->hwnd = hwnd; + if (i > _el_widget_count) _el_widget_count = i; + return (int64_t)i; + } + } + return -1; +} + +static ElWidget* el_widget_get(int64_t handle) { + if (handle <= 0 || handle >= EL_WIN32_MAX_WIDGETS) return NULL; + if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL; + return &_el_widgets[handle]; +} + +static void el_widget_free_slot(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + free(w->cb_click); w->cb_click = NULL; + free(w->cb_change); w->cb_change = NULL; + if (w->bg_brush) { DeleteObject(w->bg_brush); w->bg_brush = NULL; } + if (w->font) { DeleteObject(w->font); w->font = NULL; } + if (w->hbmp) { DeleteObject(w->hbmp); w->hbmp = NULL; } + w->hwnd = NULL; + w->kind = EL_WIDGET_FREE; +} + +/* Map HWND → widget slot (O(n) linear scan; table is small). */ +static int64_t el_hwnd_to_slot(HWND hwnd) { + for (int i = 1; i <= _el_widget_count; i++) { + if (_el_widgets[i].kind != EL_WIDGET_FREE && _el_widgets[i].hwnd == hwnd) + return (int64_t)i; + } + return -1; +} + +/* ── El callback invocation ──────────────────────────────────────────────── */ +/* + * Invoke an El callback by symbol name. El functions have the signature: + * fn handler(handle: Int, data: String) -> Void + * compiled to: void fn_name(el_val_t handle, el_val_t data) + */ +typedef void (*ElCb2)(int64_t handle, int64_t data); + +static void el_win32_invoke_cb(const char* fn_name, int64_t handle, const char* data) { + if (!fn_name || !*fn_name) return; + FARPROC proc = GetProcAddress(GetModuleHandleW(NULL), fn_name); + if (!proc) return; + ElCb2 fn = (ElCb2)(void*)proc; + fn(handle, (int64_t)(uintptr_t)(data ? data : "")); +} + +/* ── Layout reflow ────────────────────────────────────────────────────────── */ +/* + * Reflow the direct children of a container widget. + * + * For VSTACK: stack children top-to-bottom, respecting padding and spacing. + * Children with flex > 0 divide the leftover height proportionally. + * Children with fixed_h > 0 use that height; otherwise QueryWidget. + * + * For HSTACK: same logic, left-to-right. + * + * For WINDOW: its content area is the client rect; children stacked vertically + * (WINDOW acts like an outer VSTACK for direct child attachment purposes). + * + * For ZSTACK: children fill the entire client rect (absolute; no reflow). + * + * For SCROLL: the single child fills the client rect width; height is its + * natural height (scroll bar handles overflow). + */ +static void el_win32_reflow(int64_t handle) { + ElWidget* pw = el_widget_get(handle); + if (!pw || !pw->hwnd) return; + + RECT cr; + GetClientRect(pw->hwnd, &cr); + int total_w = cr.right - cr.left; + int total_h = cr.bottom - cr.top; + + int pt = pw->pad_top; + int pr = pw->pad_right; + int pb = pw->pad_bottom; + int pl = pw->pad_left; + int sp = pw->spacing; + + int n = pw->child_count; + if (n == 0) return; + + if (pw->kind == EL_WIDGET_ZSTACK) { + /* All children fill the client rect. */ + int cw = total_w - pl - pr; + int ch = total_h - pt - pb; + if (cw < 0) cw = 0; + if (ch < 0) ch = 0; + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + if (!cw_ || !cw_->hwnd) continue; + SetWindowPos(cw_->hwnd, NULL, pl, pt, cw, ch, + SWP_NOZORDER | SWP_NOACTIVATE); + } + return; + } + + if (pw->kind == EL_WIDGET_SCROLL) { + /* Single child fills width; natural height. */ + if (n < 1) return; + ElWidget* cw_ = el_widget_get(pw->children[0]); + if (!cw_ || !cw_->hwnd) return; + int cw = total_w - pl - pr; + if (cw < 0) cw = 0; + /* Measure natural height. */ + RECT cr2; + GetWindowRect(cw_->hwnd, &cr2); + int ch = cr2.bottom - cr2.top; + if (cw_->fixed_h > 0) ch = cw_->fixed_h; + SetWindowPos(cw_->hwnd, NULL, pl, pt, cw, ch, + SWP_NOZORDER | SWP_NOACTIVATE); + /* Update scroll range. */ + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_RANGE | SIF_PAGE; + si.nMin = 0; + si.nMax = ch + pt + pb; + si.nPage = (UINT)total_h; + SetScrollInfo(pw->hwnd, SB_VERT, &si, TRUE); + return; + } + + /* VSTACK, HSTACK, WINDOW */ + int is_vert = (pw->kind == EL_WIDGET_VSTACK || + pw->kind == EL_WIDGET_WINDOW); + + /* First pass: determine fixed sizes and total flex weight. */ + int total_flex = 0; + int fixed_used = 0; /* pixels consumed by fixed children + spacing */ + + /* Per-child resolved size in the main axis. */ + int* sizes = (int*)malloc((size_t)n * sizeof(int)); + if (!sizes) return; + + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + sizes[i] = 0; + if (!cw_ || !cw_->hwnd) continue; + int fixed = is_vert ? cw_->fixed_h : cw_->fixed_w; + if (fixed > 0) { + sizes[i] = fixed; + fixed_used += fixed; + } else if (cw_->flex > 0) { + total_flex += cw_->flex; + } else { + /* No flex, no fixed: measure current window size as default. */ + RECT wr; + GetWindowRect(cw_->hwnd, &wr); + int nat = is_vert ? (wr.bottom - wr.top) : (wr.right - wr.left); + if (nat < 1) nat = is_vert ? 24 : 80; /* reasonable fallback */ + sizes[i] = nat; + fixed_used += nat; + } + if (i < n - 1) fixed_used += sp; /* spacing between children */ + } + + /* Available space for flex children. */ + int avail = (is_vert ? (total_h - pt - pb) : (total_w - pl - pr)) - fixed_used; + if (avail < 0) avail = 0; + + /* Second pass: assign flex sizes. */ + if (total_flex > 0) { + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + if (!cw_ || !cw_->hwnd) continue; + if (cw_->flex > 0 && sizes[i] == 0) { + sizes[i] = (avail * cw_->flex) / total_flex; + } + } + } + + /* Third pass: position children. */ + int cursor = is_vert ? pt : pl; + for (int i = 0; i < n; i++) { + ElWidget* cw_ = el_widget_get(pw->children[i]); + if (!cw_ || !cw_->hwnd) continue; + + int x, y, cw, ch; + if (is_vert) { + x = pl; + y = cursor; + cw = (cw_->fixed_w > 0) ? cw_->fixed_w : (total_w - pl - pr); + ch = sizes[i]; + if (cw < 0) cw = 0; + if (ch < 0) ch = 0; + cursor += ch + sp; + } else { + x = cursor; + y = pt; + cw = sizes[i]; + ch = (cw_->fixed_h > 0) ? cw_->fixed_h : (total_h - pt - pb); + if (cw < 0) cw = 0; + if (ch < 0) ch = 0; + cursor += cw + sp; + } + + SetWindowPos(cw_->hwnd, NULL, x, y, cw, ch, + SWP_NOZORDER | SWP_NOACTIVATE); + + /* Recursively reflow child containers. */ + if (cw_->kind == EL_WIDGET_VSTACK || + cw_->kind == EL_WIDGET_HSTACK || + cw_->kind == EL_WIDGET_ZSTACK || + cw_->kind == EL_WIDGET_SCROLL || + cw_->kind == EL_WIDGET_WINDOW) { + el_win32_reflow(pw->children[i]); + } + } + + free(sizes); +} + +/* ── Container WndProc ────────────────────────────────────────────────────── */ +/* + * Used for VSTACK, HSTACK, ZSTACK, SCROLL containers. + * Handles WM_SIZE (reflow), WM_CTLCOLOR* (color theming), WM_COMMAND (events), + * WM_VSCROLL (scroll container). + */ +static LRESULT CALLBACK el_container_wndproc(HWND hwnd, UINT msg, + WPARAM wp, LPARAM lp) { + int64_t slot = el_hwnd_to_slot(hwnd); + ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL; + + switch (msg) { + case WM_SIZE: + if (w) el_win32_reflow(slot); + return 0; + + case WM_EL_REFLOW: + if (w) el_win32_reflow(slot); + return 0; + + case WM_VSCROLL: { + if (!w || w->kind != EL_WIDGET_SCROLL) break; + SCROLLINFO si; + si.cbSize = sizeof(si); + si.fMask = SIF_ALL; + GetScrollInfo(hwnd, SB_VERT, &si); + int pos = si.nPos; + switch (LOWORD(wp)) { + case SB_LINEUP: pos -= 20; break; + case SB_LINEDOWN: pos += 20; break; + case SB_PAGEUP: pos -= si.nPage; break; + case SB_PAGEDOWN: pos += si.nPage; break; + case SB_THUMBTRACK: pos = si.nTrackPos; break; + case SB_TOP: pos = si.nMin; break; + case SB_BOTTOM: pos = si.nMax; break; + } + if (pos < si.nMin) pos = si.nMin; + if (pos > si.nMax - (int)si.nPage) pos = si.nMax - (int)si.nPage; + si.fMask = SIF_POS; + si.nPos = pos; + SetScrollInfo(hwnd, SB_VERT, &si, TRUE); + /* Scroll child. */ + if (w->child_count > 0) { + ElWidget* cw_ = el_widget_get(w->children[0]); + if (cw_ && cw_->hwnd) { + RECT cr; GetClientRect(hwnd, &cr); + SetWindowPos(cw_->hwnd, NULL, 0, -pos, + cr.right - cr.left, 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + } + } + return 0; + } + + case WM_COMMAND: { + /* Route click / change notifications to el callbacks. */ + HWND ctrl = (HWND)lp; + if (!ctrl) break; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (!cw) break; + + WORD notif = HIWORD(wp); + if (cw->kind == EL_WIDGET_BUTTON && notif == BN_CLICKED) { + el_win32_invoke_cb(cw->cb_click, cs, ""); + } else if (cw->kind == EL_WIDGET_TEXTFIELD) { + if (notif == EN_CHANGE) { + el_win32_invoke_cb(cw->cb_change, cs, ""); + } + } else if (cw->kind == EL_WIDGET_TEXTAREA) { + if (notif == EN_CHANGE) { + el_win32_invoke_cb(cw->cb_change, cs, ""); + } + } + /* Propagate to parent so top-level window also sees it. */ + HWND parent = GetParent(hwnd); + if (parent) SendMessageW(parent, msg, wp, lp); + return 0; + } + + case WM_CTLCOLORSTATIC: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORBTN: { + HWND ctrl = (HWND)lp; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (cw) { + HDC hdc = (HDC)wp; + if (cw->has_fg) SetTextColor(hdc, cw->fg_color); + if (cw->has_bg) { + SetBkColor(hdc, cw->bg_color); + if (cw->bg_brush) return (LRESULT)cw->bg_brush; + } + } + break; + } + + case WM_ERASEBKGND: { + if (w && w->has_bg && w->bg_brush) { + HDC hdc = (HDC)wp; + RECT rc; + GetClientRect(hwnd, &rc); + FillRect(hdc, &rc, w->bg_brush); + return 1; + } + break; + } + + case WM_DESTROY: + return 0; + } + + return DefWindowProcW(hwnd, msg, wp, lp); +} + +/* ── Top-level window WndProc ────────────────────────────────────────────── */ +static LRESULT CALLBACK el_toplevel_wndproc(HWND hwnd, UINT msg, + WPARAM wp, LPARAM lp) { + int64_t slot = el_hwnd_to_slot(hwnd); + ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL; + + switch (msg) { + case WM_SIZE: + if (w) el_win32_reflow(slot); + return 0; + + case WM_EL_REFLOW: + if (w) el_win32_reflow(slot); + return 0; + + case WM_COMMAND: { + /* Bubble COMMAND messages from child controls. */ + HWND ctrl = (HWND)lp; + if (!ctrl) break; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (!cw) { + /* Not a direct slot child — pass to DefWindowProc. */ + break; + } + WORD notif = HIWORD(wp); + if (cw->kind == EL_WIDGET_BUTTON && notif == BN_CLICKED) { + el_win32_invoke_cb(cw->cb_click, cs, ""); + } else if ((cw->kind == EL_WIDGET_TEXTFIELD || + cw->kind == EL_WIDGET_TEXTAREA) && notif == EN_CHANGE) { + el_win32_invoke_cb(cw->cb_change, cs, ""); + } + return 0; + } + + case WM_CTLCOLORSTATIC: + case WM_CTLCOLOREDIT: + case WM_CTLCOLORBTN: { + HWND ctrl = (HWND)lp; + int64_t cs = el_hwnd_to_slot(ctrl); + ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL; + if (cw) { + HDC hdc = (HDC)wp; + if (cw->has_fg) SetTextColor(hdc, cw->fg_color); + if (cw->has_bg) { + SetBkColor(hdc, cw->bg_color); + if (cw->bg_brush) return (LRESULT)cw->bg_brush; + } + } + break; + } + + case WM_ERASEBKGND: { + if (w && w->has_bg && w->bg_brush) { + HDC hdc = (HDC)wp; + RECT rc; + GetClientRect(hwnd, &rc); + FillRect(hdc, &rc, w->bg_brush); + return 1; + } + break; + } + + case WM_GETMINMAXINFO: { + /* Stored in the window's GWLP_USERDATA as packed two ints: min_w | min_h. */ + LONG_PTR ud = GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (ud) { + int min_w = (int)(ud & 0xFFFF); + int min_h = (int)((ud >> 16) & 0xFFFF); + MINMAXINFO* mm = (MINMAXINFO*)lp; + if (min_w > 0) mm->ptMinTrackSize.x = min_w; + if (min_h > 0) mm->ptMinTrackSize.y = min_h; + } + return 0; + } + + case WM_CLOSE: + DestroyWindow(hwnd); + return 0; + + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + + return DefWindowProcW(hwnd, msg, wp, lp); +} + +/* ── Subclass proc for EDIT controls (Enter key → on_submit) ────────────── */ +static LRESULT CALLBACK el_edit_subclass_proc(HWND hwnd, UINT msg, WPARAM wp, + LPARAM lp, UINT_PTR uid, + DWORD_PTR ref_data) { + (void)uid; (void)ref_data; + if (msg == WM_KEYDOWN && wp == VK_RETURN) { + int64_t slot = el_hwnd_to_slot(hwnd); + ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL; + if (w && w->cb_click) { + /* Gather current text as data. */ + int len = GetWindowTextLengthW(hwnd); + WCHAR* wbuf = (WCHAR*)malloc((size_t)(len + 2) * sizeof(WCHAR)); + if (wbuf) { + GetWindowTextW(hwnd, wbuf, len + 1); + char* utf8 = el_wide_to_utf8(wbuf); + free(wbuf); + el_win32_invoke_cb(w->cb_click, slot, utf8); + free(utf8); + } + return 0; /* suppress default beep */ + } + } + return DefSubclassProc(hwnd, msg, wp, lp); +} + +/* ── Window-class registration ────────────────────────────────────────────── */ +static int _el_classes_registered = 0; + +static void el_win32_register_classes(void) { + if (_el_classes_registered) return; + _el_classes_registered = 1; + + /* Initialize common controls (needed for SetWindowSubclass). */ + INITCOMMONCONTROLSEX icc; + icc.dwSize = sizeof(icc); + icc.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES; + InitCommonControlsEx(&icc); + + HINSTANCE hinst = GetModuleHandleW(NULL); + + /* Container class (VStack, HStack, ZStack, Scroll) */ + WNDCLASSW wc; + memset(&wc, 0, sizeof(wc)); + wc.lpfnWndProc = el_container_wndproc; + wc.hInstance = hinst; + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = EL_WNDCLASS_VSTACK; + RegisterClassW(&wc); + + wc.lpszClassName = EL_WNDCLASS_HSTACK; + RegisterClassW(&wc); + + wc.lpszClassName = EL_WNDCLASS_ZSTACK; + RegisterClassW(&wc); + + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpszClassName = EL_WNDCLASS_SCROLL; + RegisterClassW(&wc); + + /* Top-level window class */ + memset(&wc, 0, sizeof(wc)); + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = el_toplevel_wndproc; + wc.hInstance = hinst; + wc.hCursor = LoadCursorW(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = EL_WNDCLASS_WINDOW; + RegisterClassW(&wc); +} + +/* ── Public C API ─────────────────────────────────────────────────────────── */ + +/* + * el_win32_init — one-time initialisation. Must be called before any other + * el_win32_* function. + */ +void el_win32_init(void) { + static int done = 0; + if (done) return; + done = 1; + el_win32_register_classes(); +} + +/* + * el_win32_run_loop — enter the Win32 message loop. Never returns. + * Must be called from the main thread after all windows are shown. + */ +void el_win32_run_loop(void) { + MSG msg; + while (GetMessageW(&msg, NULL, 0, 0) > 0) { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } +} + +/* ── Window ───────────────────────────────────────────────────────────────── */ + +int64_t el_win32_window_create(const char* title, int width, int height, + int min_width, int min_height) { + el_win32_register_classes(); + HINSTANCE hinst = GetModuleHandleW(NULL); + WCHAR* wtitle = el_utf8_to_wide(title ? title : ""); + + DWORD style = WS_OVERLAPPEDWINDOW; + HWND hwnd = CreateWindowW( + EL_WNDCLASS_WINDOW, + wtitle, + style, + CW_USEDEFAULT, CW_USEDEFAULT, + width, height, + NULL, NULL, hinst, NULL); + + free(wtitle); + if (!hwnd) return -1; + + /* Pack min_width / min_height into GWLP_USERDATA (low 16 = min_w, next 16 = min_h). */ + LONG_PTR ud = ((LONG_PTR)(min_height & 0xFFFF) << 16) | (LONG_PTR)(min_width & 0xFFFF); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, ud); + + int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + ElWidget* w = el_widget_get(handle); + w->spacing = 0; + + return handle; +} + +void el_win32_window_show(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW || !w->hwnd) return; + ShowWindow(w->hwnd, SW_SHOW); + UpdateWindow(w->hwnd); +} + +void el_win32_window_set_title(int64_t handle, const char* title) { + ElWidget* w = el_widget_get(handle); + if (!w || w->kind != EL_WIDGET_WINDOW || !w->hwnd) return; + WCHAR* wt = el_utf8_to_wide(title ? title : ""); + SetWindowTextW(w->hwnd, wt); + free(wt); +} + +/* ── Layout container factories ───────────────────────────────────────────── */ + +static int64_t el_win32_make_container(const WCHAR* class_name, ElWidgetKind kind, + int spacing) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + class_name, + L"", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, + 0, 0, 0, 0, + HWND_MESSAGE, /* temporary parent; reparented on add_child */ + NULL, + GetModuleHandleW(NULL), + NULL); + if (!hwnd) return -1; + + int64_t handle = el_widget_alloc(kind, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + ElWidget* w = el_widget_get(handle); + w->spacing = spacing; + return handle; +} + +int64_t el_win32_vstack_create(int spacing) { + return el_win32_make_container(EL_WNDCLASS_VSTACK, EL_WIDGET_VSTACK, spacing); +} + +int64_t el_win32_hstack_create(int spacing) { + return el_win32_make_container(EL_WNDCLASS_HSTACK, EL_WIDGET_HSTACK, spacing); +} + +int64_t el_win32_zstack_create(void) { + return el_win32_make_container(EL_WNDCLASS_ZSTACK, EL_WIDGET_ZSTACK, 0); +} + +int64_t el_win32_scroll_create(void) { + HWND hwnd = CreateWindowW( + EL_WNDCLASS_SCROLL, + L"", + WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_VSCROLL, + 0, 0, 0, 0, + HWND_MESSAGE, + NULL, + GetModuleHandleW(NULL), + NULL); + if (!hwnd) return -1; + + int64_t handle = el_widget_alloc(EL_WIDGET_SCROLL, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + return handle; +} + +/* ── Widget factories ─────────────────────────────────────────────────────── */ + +int64_t el_win32_label_create(const char* text) { + el_win32_register_classes(); + WCHAR* wt = el_utf8_to_wide(text ? text : ""); + HWND hwnd = CreateWindowW( + L"STATIC", + wt, + WS_CHILD | WS_VISIBLE | SS_LEFT, + 0, 0, 0, 24, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + free(wt); + if (!hwnd) return -1; + return el_widget_alloc(EL_WIDGET_LABEL, hwnd); +} + +int64_t el_win32_button_create(const char* label) { + el_win32_register_classes(); + WCHAR* wl = el_utf8_to_wide(label ? label : ""); + HWND hwnd = CreateWindowW( + L"BUTTON", + wl, + WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, + 0, 0, 0, 32, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + free(wl); + if (!hwnd) return -1; + return el_widget_alloc(EL_WIDGET_BUTTON, hwnd); +} + +int64_t el_win32_text_field_create(const char* placeholder) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + L"EDIT", + L"", + WS_CHILD | WS_VISIBLE | WS_BORDER | + ES_AUTOHSCROLL | ES_LEFT, + 0, 0, 0, 26, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + if (!hwnd) return -1; + + /* Set placeholder / cue banner (Windows Vista+). */ + if (placeholder && *placeholder) { + WCHAR* wp = el_utf8_to_wide(placeholder); + SendMessageW(hwnd, EM_SETCUEBANNER, TRUE, (LPARAM)wp); + free(wp); + } + + int64_t handle = el_widget_alloc(EL_WIDGET_TEXTFIELD, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + /* Subclass to intercept VK_RETURN for on_submit. */ + SetWindowSubclass(hwnd, el_edit_subclass_proc, (UINT_PTR)handle, 0); + return handle; +} + +int64_t el_win32_text_area_create(const char* placeholder) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + L"EDIT", + L"", + WS_CHILD | WS_VISIBLE | WS_BORDER | + ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL, + 0, 0, 0, 80, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + if (!hwnd) return -1; + + /* EDIT multiline does not support EM_SETCUEBANNER, so no placeholder. */ + (void)placeholder; + + int64_t handle = el_widget_alloc(EL_WIDGET_TEXTAREA, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + return handle; +} + +int64_t el_win32_image_create(const char* path) { + el_win32_register_classes(); + HWND hwnd = CreateWindowW( + L"STATIC", + L"", + WS_CHILD | WS_VISIBLE | SS_BITMAP | SS_REALSIZECONTROL, + 0, 0, 0, 0, + HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL); + if (!hwnd) return -1; + + int64_t handle = el_widget_alloc(EL_WIDGET_IMAGE, hwnd); + if (handle < 0) { DestroyWindow(hwnd); return -1; } + + ElWidget* w = el_widget_get(handle); + + if (path && *path) { + WCHAR* wpath = el_utf8_to_wide(path); + HBITMAP hbmp = (HBITMAP)LoadImageW(NULL, wpath, IMAGE_BITMAP, + 0, 0, + LR_LOADFROMFILE | LR_DEFAULTSIZE); + free(wpath); + if (hbmp) { + w->hbmp = hbmp; + SendMessageW(hwnd, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hbmp); + } + } + + return handle; +} + +/* ── Widget property setters ─────────────────────────────────────────────── */ + +void el_win32_widget_set_text(int64_t handle, const char* text) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + WCHAR* wt = el_utf8_to_wide(text ? text : ""); + SetWindowTextW(w->hwnd, wt); + free(wt); +} + +const char* el_win32_widget_get_text(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return strdup(""); + int len = GetWindowTextLengthW(w->hwnd); + if (len <= 0) return strdup(""); + WCHAR* wbuf = (WCHAR*)malloc((size_t)(len + 2) * sizeof(WCHAR)); + if (!wbuf) return strdup(""); + GetWindowTextW(w->hwnd, wbuf, len + 1); + char* result = el_wide_to_utf8(wbuf); + free(wbuf); + return result; /* caller must free() */ +} + +void el_win32_widget_set_color(int64_t handle, float r, float g, float b, float a) { + (void)a; /* Win32 classic does not support per-control alpha */ + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + w->fg_color = RGB((int)(r * 255.0f), (int)(g * 255.0f), (int)(b * 255.0f)); + w->has_fg = 1; + InvalidateRect(w->hwnd, NULL, TRUE); +} + +void el_win32_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { + (void)a; + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + w->bg_color = RGB((int)(r * 255.0f), (int)(g * 255.0f), (int)(b * 255.0f)); + w->has_bg = 1; + if (w->bg_brush) DeleteObject(w->bg_brush); + w->bg_brush = CreateSolidBrush(w->bg_color); + InvalidateRect(w->hwnd, NULL, TRUE); +} + +void el_win32_widget_set_font(int64_t handle, const char* family, int size, int bold) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + + LOGFONTW lf; + memset(&lf, 0, sizeof(lf)); + lf.lfHeight = -MulDiv(size > 0 ? size : 12, GetDeviceCaps(GetDC(NULL), LOGPIXELSY), 72); + lf.lfWeight = bold ? FW_BOLD : FW_NORMAL; + lf.lfCharSet = DEFAULT_CHARSET; + + if (family && *family && strcmp(family, "system") != 0) { + WCHAR* wf = el_utf8_to_wide(family); + wcsncpy(lf.lfFaceName, wf, LF_FACESIZE - 1); + free(wf); + } + /* family == NULL or "system" → lfFaceName stays zeroed → system font */ + + HFONT hfont = CreateFontIndirectW(&lf); + if (!hfont) return; + + if (w->font) DeleteObject(w->font); + w->font = hfont; + SendMessageW(w->hwnd, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0)); +} + +void el_win32_widget_set_padding(int64_t handle, int top, int right, + int bottom, int left) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->pad_top = top; + w->pad_right = right; + w->pad_bottom = bottom; + w->pad_left = left; + /* Trigger reflow if already parented. */ + if (w->hwnd) PostMessageW(w->hwnd, WM_EL_REFLOW, 0, 0); +} + +void el_win32_widget_set_width(int64_t handle, int width) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->fixed_w = width; + if (w->hwnd) { + RECT r; GetWindowRect(w->hwnd, &r); + int h = r.bottom - r.top; + SetWindowPos(w->hwnd, NULL, 0, 0, width, h, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } +} + +void el_win32_widget_set_height(int64_t handle, int height) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->fixed_h = height; + if (w->hwnd) { + RECT r; GetWindowRect(w->hwnd, &r); + int cw = r.right - r.left; + SetWindowPos(w->hwnd, NULL, 0, 0, cw, height, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } +} + +void el_win32_widget_set_flex(int64_t handle, int flex) { + ElWidget* w = el_widget_get(handle); + if (!w) return; + w->flex = flex; + /* Let parent reflow absorb the change. */ +} + +void el_win32_widget_set_corner_radius(int64_t handle, int radius) { + /* + * Classic Win32 does not have native per-control corner radii. + * If radius > 0 we apply a rounded rectangular window region so the + * control at least clips to a rounded rect. This is approximate — it + * does not anti-alias and may look boxy on high-DPI displays. + */ + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd || radius <= 0) return; + RECT r; GetWindowRect(w->hwnd, &r); + int ww = r.right - r.left; + int wh = r.bottom - r.top; + if (ww <= 0 || wh <= 0) return; + HRGN rgn = CreateRoundRectRgn(0, 0, ww, wh, radius * 2, radius * 2); + SetWindowRgn(w->hwnd, rgn, TRUE); + /* Note: SetWindowRgn takes ownership of rgn; do NOT DeleteObject it. */ +} + +void el_win32_widget_set_disabled(int64_t handle, int disabled) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + EnableWindow(w->hwnd, disabled ? FALSE : TRUE); +} + +void el_win32_widget_set_hidden(int64_t handle, int hidden) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + ShowWindow(w->hwnd, hidden ? SW_HIDE : SW_SHOW); +} + +/* ── Child management ─────────────────────────────────────────────────────── */ + +/* + * el_win32_widget_add_child — attach child widget to parent. + * + * - WINDOW / VSTACK / HSTACK / ZSTACK: SetParent child into container. + * - SCROLL: first child becomes the scrolled content. + * + * After reparenting, triggers a layout reflow of the parent. + */ +void el_win32_widget_add_child(int64_t parent, int64_t child) { + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!pw || !cw || !pw->hwnd || !cw->hwnd) return; + if (cw->kind == EL_WIDGET_WINDOW) return; /* cannot add window as child */ + if (pw->child_count >= EL_WIN32_MAX_CHILDREN) return; + + /* Reparent the child HWND into the container. */ + SetParent(cw->hwnd, pw->hwnd); + + /* Make sure the WS_CHILD style is set and WS_POPUP is cleared. */ + LONG_PTR style = GetWindowLongPtrW(cw->hwnd, GWL_STYLE); + style |= WS_CHILD; + style &= ~WS_POPUP; + SetWindowLongPtrW(cw->hwnd, GWL_STYLE, style); + ShowWindow(cw->hwnd, SW_SHOW); + + pw->children[pw->child_count++] = child; + + /* Reflow parent so the new child gets positioned. */ + el_win32_reflow(parent); +} + +void el_win32_widget_remove_child(int64_t parent, int64_t child) { + ElWidget* pw = el_widget_get(parent); + ElWidget* cw = el_widget_get(child); + if (!cw || !cw->hwnd) return; + + /* Detach HWND from parent. */ + SetParent(cw->hwnd, NULL); + + /* Remove from parent's child list. */ + if (pw) { + for (int i = 0; i < pw->child_count; i++) { + if (pw->children[i] == child) { + memmove(&pw->children[i], &pw->children[i + 1], + (size_t)(pw->child_count - i - 1) * sizeof(int64_t)); + pw->child_count--; + break; + } + } + el_win32_reflow(parent); + } +} + +/* ── Event registration ───────────────────────────────────────────────────── */ + +void el_win32_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; +} + +void el_win32_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; +} + +void el_win32_widget_on_submit(int64_t handle, const char* fn_name) { + /* For text fields, submit (Enter key) is stored in cb_click, same as AppKit. */ + el_win32_widget_on_click(handle, fn_name); +} + +/* ── Widget destroy ───────────────────────────────────────────────────────── */ + +void el_win32_widget_destroy(int64_t handle) { + ElWidget* w = el_widget_get(handle); + if (!w || !w->hwnd) return; + DestroyWindow(w->hwnd); + el_widget_free_slot(handle); +} + +/* ── Manifest reader ──────────────────────────────────────────────────────── */ +/* + * Same parser as the macOS implementation in el_seed.c. + * Reads the app{} block from a manifest.el file: + * + * app { + * window_title "My App" + * window_width 1200 + * window_height 800 + * min_width 600 + * min_height 400 + * } + * + * Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N} + * Returns "{}" on failure. + * + * NOTE: el_seed.c already defines __manifest_read for both EL_TARGET_MACOS and + * EL_TARGET_WIN32 using the same parser body. We re-implement it here because + * el_seed.c's implementation is inside #ifdef EL_TARGET_MACOS; the Win32 + * version lives here, symmetric with the macOS one. + */ + +static void w32_manifest_skip_ws(const char** p) { + while (**p && (unsigned char)**p <= ' ') (*p)++; +} + +static char* w32_manifest_read_string(const char** p) { + w32_manifest_skip_ws(p); + if (**p != '"') return NULL; + (*p)++; + const char* start = *p; + while (**p && **p != '"') { + if (**p == '\\') (*p)++; + (*p)++; + } + size_t len = (size_t)(*p - start); + if (**p == '"') (*p)++; + char* out = (char*)malloc(len + 1); + if (!out) return NULL; + memcpy(out, start, len); + out[len] = '\0'; + return out; +} + +static int64_t w32_manifest_read_int(const char** p) { + w32_manifest_skip_ws(p); + int64_t result = 0; + int neg = 0; + if (**p == '-') { neg = 1; (*p)++; } + while (**p >= '0' && **p <= '9') { + result = result * 10 + (**p - '0'); + (*p)++; + } + return neg ? -result : result; +} + +static const char* w32_manifest_find_app_block(const char* src) { + while (*src) { + if (src[0] == '/' && src[1] == '/') { + while (*src && *src != '\n') src++; + continue; + } + if (strncmp(src, "app", 3) == 0 && + (src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) { + src += 3; + while (*src && *src != '{') src++; + if (*src == '{') return src + 1; + return NULL; + } + src++; + } + return NULL; +} + +/* el_val_t helpers — mirror of what el_seed.c provides. */ +#ifndef EL_CSTR +#define EL_CSTR(v) ((const char*)(uintptr_t)(v)) +#endif +#ifndef EL_STR +#define EL_STR(p) ((el_val_t)(uintptr_t)(p)) +#endif + +el_val_t el_win32_manifest_read(const char* path) { + if (!path || !*path) return EL_STR(strdup("{}")); + + FILE* f = fopen(path, "r"); + if (!f) return EL_STR(strdup("{}")); + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { fclose(f); return EL_STR(strdup("{}")); } + char* src = (char*)malloc((size_t)sz + 1); + if (!src) { fclose(f); return EL_STR(strdup("{}")); } + size_t got = fread(src, 1, (size_t)sz, f); + src[got] = '\0'; + fclose(f); + + const char* app_start = w32_manifest_find_app_block(src); + if (!app_start) { free(src); return EL_STR(strdup("{}")); } + + char* title = NULL; + int64_t width = 1200, height = 800; + int64_t min_w = 600, min_h = 400; + + const char* cur = app_start; + while (*cur && *cur != '}') { + while (*cur && (unsigned char)*cur <= ' ') cur++; + if (*cur == '}') break; + if (cur[0] == '/' && cur[1] == '/') { + while (*cur && *cur != '\n') cur++; + continue; + } + const char* key_start = cur; + while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++; + size_t key_len = (size_t)(cur - key_start); + if (key_len == 0) { cur++; continue; } + while (*cur == ' ' || *cur == '\t') cur++; + + if (key_len == 12 && strncmp(key_start, "window_title", 12) == 0) { free(title); title = w32_manifest_read_string(&cur); } + else if (key_len == 12 && strncmp(key_start, "window_width", 12) == 0) { width = w32_manifest_read_int(&cur); } + else if (key_len == 13 && strncmp(key_start, "window_height", 13) == 0) { height = w32_manifest_read_int(&cur); } + else if (key_len == 9 && strncmp(key_start, "min_width", 9) == 0) { min_w = w32_manifest_read_int(&cur); } + else if (key_len == 10 && strncmp(key_start, "min_height", 10) == 0) { min_h = w32_manifest_read_int(&cur); } + else { while (*cur && *cur != '\n' && *cur != '}') cur++; } + } + + free(src); + + const char* t = title ? title : "App"; + size_t buf_sz = strlen(t) * 6 + 128; + char* buf = (char*)malloc(buf_sz); + if (!buf) { free(title); return EL_STR(strdup("{}")); } + snprintf(buf, buf_sz, + "{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}", + t, (long long)width, (long long)height, (long long)min_w, (long long)min_h); + free(title); + return EL_STR(buf); +} + +/* ── el_seed.c __* builtins for EL_TARGET_WIN32 ──────────────────────────── */ +/* + * These are the thin el_val_t wrappers that el_seed.c (with EL_TARGET_WIN32) + * will call. They mirror the EL_TARGET_MACOS section in el_seed.c exactly. + * + * el_seed.c includes this file's declarations via el_native_target.h and + * links el_win32.o. The __* symbol definitions below are compiled into + * el_win32.c itself (not el_seed.c) when EL_TARGET_WIN32 is defined, keeping + * el_seed.c platform-agnostic. + */ + +/* Helper: extract el string pointer. */ +static inline const char* _w32_cstr(el_val_t v) { + return (const char*)(uintptr_t)v; +} +static inline el_val_t _w32_str(const char* p) { + return (el_val_t)(uintptr_t)p; +} + +/* Bit-cast helper for float args (el_val_t carries IEEE 754 doubles). */ +static inline double _w32_to_float(el_val_t v) { + union { double d; int64_t i; } u; u.i = (int64_t)v; return u.d; +} + +void __native_init(void) { el_win32_init(); } +void __native_run_loop(void) { el_win32_run_loop(); } + +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_win32_window_create( + _w32_cstr(title), + (int)(int64_t)width, (int)(int64_t)height, + (int)(int64_t)min_width, (int)(int64_t)min_height); +} + +void __window_show(el_val_t h) { el_win32_window_show((int64_t)h); } +void __window_set_title(el_val_t h, el_val_t t) { el_win32_window_set_title((int64_t)h, _w32_cstr(t)); } + +el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_win32_vstack_create((int)(int64_t)spacing); } +el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_win32_hstack_create((int)(int64_t)spacing); } +el_val_t __zstack_create(void) { return (el_val_t)el_win32_zstack_create(); } +el_val_t __scroll_create(void) { return (el_val_t)el_win32_scroll_create(); } + +el_val_t __label_create(el_val_t text) { return (el_val_t)el_win32_label_create(_w32_cstr(text)); } +el_val_t __button_create(el_val_t label) { return (el_val_t)el_win32_button_create(_w32_cstr(label)); } +el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_win32_text_field_create(_w32_cstr(placeholder)); } +el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_win32_text_area_create(_w32_cstr(placeholder)); } +el_val_t __image_create(el_val_t path) { return (el_val_t)el_win32_image_create(_w32_cstr(path)); } + +void __widget_set_text(el_val_t h, el_val_t t) { el_win32_widget_set_text((int64_t)h, _w32_cstr(t)); } + +el_val_t __widget_get_text(el_val_t h) { + const char* s = el_win32_widget_get_text((int64_t)h); + /* el_win32_widget_get_text returns a malloc'd string; caller treats as + * short-lived (same contract as el_appkit version). */ + return s ? _w32_str(s) : _w32_str(strdup("")); +} + +void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_win32_widget_set_color((int64_t)h, + (float)_w32_to_float(r), (float)_w32_to_float(g), + (float)_w32_to_float(b), (float)_w32_to_float(a)); +} + +void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) { + el_win32_widget_set_bg_color((int64_t)h, + (float)_w32_to_float(r), (float)_w32_to_float(g), + (float)_w32_to_float(b), (float)_w32_to_float(a)); +} + +void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) { + el_win32_widget_set_font((int64_t)h, _w32_cstr(family), + (int)(int64_t)size, (int)(int64_t)bold); +} + +void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right, + el_val_t bottom, el_val_t left) { + el_win32_widget_set_padding((int64_t)h, + (int)(int64_t)top, (int)(int64_t)right, + (int)(int64_t)bottom, (int)(int64_t)left); +} + +void __widget_set_width(el_val_t h, el_val_t w) { el_win32_widget_set_width((int64_t)h, (int)(int64_t)w); } +void __widget_set_height(el_val_t h, el_val_t ht) { el_win32_widget_set_height((int64_t)h, (int)(int64_t)ht); } +void __widget_set_flex(el_val_t h, el_val_t f) { el_win32_widget_set_flex((int64_t)h, (int)(int64_t)f); } +void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_win32_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); } +void __widget_set_disabled(el_val_t h, el_val_t d) { el_win32_widget_set_disabled((int64_t)h, (int)(int64_t)d); } +void __widget_set_hidden(el_val_t h, el_val_t hid) { el_win32_widget_set_hidden((int64_t)h, (int)(int64_t)hid); } + +void __widget_add_child(el_val_t p, el_val_t c) { el_win32_widget_add_child((int64_t)p, (int64_t)c); } +void __widget_remove_child(el_val_t p, el_val_t c) { el_win32_widget_remove_child((int64_t)p, (int64_t)c); } +void __widget_destroy(el_val_t h) { el_win32_widget_destroy((int64_t)h); } + +void __widget_on_click(el_val_t h, el_val_t fn_name) { el_win32_widget_on_click((int64_t)h, _w32_cstr(fn_name)); } +void __widget_on_change(el_val_t h, el_val_t fn_name) { el_win32_widget_on_change((int64_t)h, _w32_cstr(fn_name)); } +void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_win32_widget_on_submit((int64_t)h, _w32_cstr(fn_name)); } + +el_val_t __manifest_read(el_val_t path) { + return el_win32_manifest_read(_w32_cstr(path)); +} + +#endif /* EL_TARGET_WIN32 */