Files
will.anderson 58753a88d7
El SDK Release / build-and-release (pull_request) Failing after 17s
feat(ui): native vessel, HTML vessel update, native hello examples, profile card, UI tools
el-native vessel: El-level wrappers around __widget_* C builtins, exposing
vstack, label, button, text_field, etc. as clean El functions for application code.

el-html/main.elh: updated extern declarations for the HTML vessel's codegen API.

native-hello: cross-platform desktop example (AppKit/GTK4/Win32/SDL2) with
build scripts, Dockerfiles for Linux/Pi, and Win32 cross-compile support.

native-hello-android: Gradle project with ElBridge integration and build script.

native-hello-ios: Xcode project for the iOS UIKit target.

profile-card: manifest.el for a styling/layout/i18n example app that exercises
el-style, el-layout, el-i18n, el-config, and el-secrets vessels.

ui/tools/native-codegen: Python codegen pass (el_ui_native_codegen.py) that
lowers el-ui component DSL to el-native vessel calls, plus build script and
test fixtures.
2026-06-29 12:40:37 -05:00

1028 lines
37 KiB
C

/*
* el_gtk4.c — GTK4 backend for the el native widget system (Linux).
*
* This file implements the Linux GTK4 widget layer that el_seed.c calls
* through to when EL_TARGET_LINUX is defined.
*
* 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).
*
* Threading: 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 <gtk/gtk.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
/* 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;
/* Allocate a slot; returns slot index or -1 if full. Takes a strong ref. */
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;
}
/* 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;
}
/* Forward declaration — defined below in the GtkApplication globals section. */
static int _el_app_running;
/* ── 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, ...).
*/
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) {
/*
* Pre-run phase (el boot sequence, before native_run_loop):
* The GMainContext is not yet iterating. We are on the main thread.
* Call directly — no dispatch needed.
*
* Post-run phase (callbacks from GTK signal handlers or other threads):
* Dispatch to the main context and wait for completion.
*/
if (!_el_app_running) {
fn(data);
return;
}
/* 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);
}
/* Convenience macro: declare a local struct, fill it, dispatch. */
#define EL_SYNC(type, var, ...) \
type var = { __VA_ARGS__ }; \
el_gtk4_sync_main(_el_sync_##var, &var)
/* ── 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 : ""));
}
/* ── GtkApplication (global) ─────────────────────────────────────────────── */
static GtkApplication* _el_app = NULL;
static int _el_app_running = 0; /* set to 1 after activate fires */
/* ── 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);
}
/* ── GtkApplication activate callback ───────────────────────────────────── */
static void _el_app_activate(GtkApplication* app, gpointer user_data) {
/*
* Register all pre-created windows with the application, and mark the
* app as running so subsequent calls use el_gtk4_sync_main dispatching.
*
* On macOS the GTK4 Quartz backend requires g_application_run() on the
* main thread. Windows created before run use gtk_window_new(); we
* register them here via gtk_application_add_window() so they participate
* in the application lifecycle (app quits when last window closes, etc.).
*/
(void)user_data;
_el_app_running = 1;
for (int i = 1; i < EL_GTK4_MAX_WIDGETS; i++) {
if (_el_widgets[i].kind == EL_WIDGET_WINDOW && _el_widgets[i].widget) {
gtk_application_add_window(app, GTK_WINDOW(_el_widgets[i].widget));
}
}
}
/* ── 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() to be called
* from the main thread. Since the el boot sequence (native_init → window_create
* → widget_build → native_run_loop) runs on the main thread, g_application_run
* is always called from the main thread here.
*
* Windows created before this call use gtk_window_new() (no GApplication
* needed); the activate handler registers them with the GtkApplication.
*/
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 result;
} _ElWindowCreateArgs;
static void _el_window_create_main(void* vp) {
_ElWindowCreateArgs* a = (_ElWindowCreateArgs*)vp;
/*
* Use gtk_window_new() rather than gtk_application_window_new().
* gtk_application_window_new() requires the GApplication to be running
* (i.e., after g_application_run / activate), which is not yet the case
* when the el boot sequence creates windows before native_run_loop().
* The activate handler in el_gtk4_run_loop registers these windows with
* the GtkApplication via gtk_application_add_window().
*/
GtkWidget* win = gtk_window_new();
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);
a->result = el_widget_alloc(EL_WIDGET_WINDOW, win);
}
int64_t el_gtk4_window_create(const char* title, int width, int height,
int min_width, int min_height) {
_ElWindowCreateArgs a = { title, width, height, min_width, min_height, -1 };
el_gtk4_sync_main(_el_window_create_main, &a);
return a.result;
}
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) return;
gtk_window_present(GTK_WINDOW(w->widget));
}
void el_gtk4_window_show(int64_t handle) {
_ElHandleArgs a = { handle };
el_gtk4_sync_main(_el_window_show_main, &a);
}
typedef struct { int64_t handle; const 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) 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) {
_ElSetTitleArgs a = { handle, title };
el_gtk4_sync_main(_el_window_set_title_main, &a);
}
/* ── Layout containers ───────────────────────────────────────────────────── */
typedef struct {
GtkOrientation orient;
int spacing;
int64_t result;
} _ElStackCreateArgs;
static void _el_stack_create_main(void* vp) {
_ElStackCreateArgs* a = (_ElStackCreateArgs*)vp;
GtkWidget* box = gtk_box_new(a->orient, a->spacing);
ElWidgetKind kind = (a->orient == GTK_ORIENTATION_VERTICAL)
? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK;
a->result = el_widget_alloc(kind, box);
}
int64_t el_gtk4_vstack_create(int spacing) {
_ElStackCreateArgs a = { GTK_ORIENTATION_VERTICAL, spacing, -1 };
el_gtk4_sync_main(_el_stack_create_main, &a);
return a.result;
}
int64_t el_gtk4_hstack_create(int spacing) {
_ElStackCreateArgs a = { GTK_ORIENTATION_HORIZONTAL, spacing, -1 };
el_gtk4_sync_main(_el_stack_create_main, &a);
return a.result;
}
typedef struct { int64_t result; } _ElResultArgs;
static void _el_zstack_create_main(void* vp) {
_ElResultArgs* a = (_ElResultArgs*)vp;
GtkWidget* overlay = gtk_overlay_new();
a->result = el_widget_alloc(EL_WIDGET_ZSTACK, overlay);
}
int64_t el_gtk4_zstack_create(void) {
_ElResultArgs a = { -1 };
el_gtk4_sync_main(_el_zstack_create_main, &a);
return a.result;
}
static void _el_scroll_create_main(void* vp) {
_ElResultArgs* a = (_ElResultArgs*)vp;
GtkWidget* sw = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
a->result = el_widget_alloc(EL_WIDGET_SCROLL, sw);
}
int64_t el_gtk4_scroll_create(void) {
_ElResultArgs a = { -1 };
el_gtk4_sync_main(_el_scroll_create_main, &a);
return a.result;
}
/* ── Widget factories ─────────────────────────────────────────────────────── */
typedef struct { const char* text; int64_t result; } _ElTextArgs;
static void _el_label_create_main(void* vp) {
_ElTextArgs* a = (_ElTextArgs*)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);
a->result = el_widget_alloc(EL_WIDGET_LABEL, lbl);
}
int64_t el_gtk4_label_create(const char* text) {
_ElTextArgs a = { text, -1 };
el_gtk4_sync_main(_el_label_create_main, &a);
return a.result;
}
static void _el_button_create_main(void* vp) {
_ElTextArgs* a = (_ElTextArgs*)vp;
GtkWidget* btn = gtk_button_new_with_label(a->text ? a->text : "");
int64_t slot = el_widget_alloc(EL_WIDGET_BUTTON, btn);
if (slot >= 0) {
g_signal_connect(btn, "clicked",
G_CALLBACK(_el_on_button_clicked),
(gpointer)(intptr_t)slot);
}
a->result = slot;
}
int64_t el_gtk4_button_create(const char* label) {
_ElTextArgs a = { label, -1 };
el_gtk4_sync_main(_el_button_create_main, &a);
return a.result;
}
static void _el_text_field_create_main(void* vp) {
_ElTextArgs* a = (_ElTextArgs*)vp;
GtkWidget* entry = gtk_entry_new();
if (a->text && *a->text) {
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), a->text);
}
int64_t slot = el_widget_alloc(EL_WIDGET_TEXTFIELD, entry);
if (slot >= 0) {
gpointer slot_ptr = (gpointer)(intptr_t)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);
}
a->result = slot;
}
int64_t el_gtk4_text_field_create(const char* placeholder) {
_ElTextArgs a = { placeholder, -1 };
el_gtk4_sync_main(_el_text_field_create_main, &a);
return a.result;
}
static void _el_text_area_create_main(void* vp) {
_ElTextArgs* a = (_ElTextArgs*)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);
/* Store the scroll window as the widget; text view accessible via child. */
int64_t slot = el_widget_alloc(EL_WIDGET_TEXTAREA, sw);
if (slot >= 0) {
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)slot);
}
a->result = slot;
}
int64_t el_gtk4_text_area_create(const char* placeholder) {
_ElTextArgs a = { placeholder, -1 };
el_gtk4_sync_main(_el_text_area_create_main, &a);
return a.result;
}
static void _el_image_create_main(void* vp) {
_ElTextArgs* a = (_ElTextArgs*)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();
}
a->result = el_widget_alloc(EL_WIDGET_IMAGE, img);
}
int64_t el_gtk4_image_create(const char* path_or_name) {
_ElTextArgs a = { path_or_name, -1 };
el_gtk4_sync_main(_el_image_create_main, &a);
return a.result;
}
/* ── Widget property setters ─────────────────────────────────────────────── */
typedef struct { int64_t handle; const 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) 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) {
_ElWidgetTextArgs a = { handle, 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) { 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) 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) {
_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->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) {
_ElColorArgs args = { handle, r, g, b, a };
el_gtk4_sync_main(_el_widget_set_bg_color_main, &args);
}
typedef struct {
int64_t handle;
const 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) 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) {
_ElFontArgs a = { handle, 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) 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) {
_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->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) {
_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->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) {
_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->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) {
_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->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) {
_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) return;
gtk_widget_set_sensitive(w->widget, !a->value);
}
void el_gtk4_widget_set_disabled(int64_t handle, int disabled) {
_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->kind == EL_WIDGET_WINDOW) return;
gtk_widget_set_visible(w->widget, !a->value);
}
void el_gtk4_widget_set_hidden(int64_t handle, int hidden) {
_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) 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) {
_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) 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) {
_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.
*/
void el_gtk4_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_gtk4_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_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) 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) {
_ElHandleArgs a = { handle };
el_gtk4_sync_main(_el_widget_destroy_main, &a);
}
#endif /* EL_TARGET_LINUX */