/* * 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 #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; /* 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 */