1328 lines
48 KiB
C
1328 lines
48 KiB
C
/*
|
|
* 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 <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;
|
|
|
|
/*
|
|
* 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 */
|