1361 lines
47 KiB
C
1361 lines
47 KiB
C
/*
|
|
* el_sdl2.c — SDL2 backend for the el native widget system.
|
|
*
|
|
* This file implements the SDL2 widget layer that el_seed.c calls through to
|
|
* when EL_TARGET_SDL2 is defined. It is the embedded/Pi counterpart to
|
|
* el_appkit.m (macOS) and el_gtk4.c (Linux desktop).
|
|
*
|
|
* Architecture:
|
|
* el program (el code)
|
|
* → __widget_* C builtins in el_seed.c
|
|
* → el_sdl2_* C functions declared here
|
|
* → SDL2 pixel drawing (SDL_RenderFillRect, TTF_RenderUTF8_Blended)
|
|
*
|
|
* SDL2 has no widget toolkit — it only draws pixels. This bridge implements
|
|
* widgets as drawn primitives: rectangles, text, borders. Each "widget" is a
|
|
* logical node in a retained layout tree. On any state change, the whole scene
|
|
* re-renders via el_sdl2_layout + el_sdl2_render_widget.
|
|
*
|
|
* Widget handles: every widget is an int64_t slot index into g_widgets[].
|
|
* Slot 0 is reserved; valid handles are 1..EL_SDL2_MAX_WIDGETS-1. -1 = invalid.
|
|
*
|
|
* Threading: SDL2 requires that all rendering happen on the thread that called
|
|
* SDL_Init. __native_run_loop runs on the main thread and never returns.
|
|
* Callback functions fired by the event loop are called synchronously from the
|
|
* same thread — no locking required.
|
|
*
|
|
* Font loading: DejaVu or Liberation Sans searched in standard Linux paths.
|
|
* Falls back to a NULL font (text omitted) if none found.
|
|
*
|
|
* Target: Raspberry Pi Zero 2W, 512 MB RAM, no desktop environment, raw
|
|
* framebuffer via SDL2's KMS/DRM or fbdev driver.
|
|
*
|
|
* Compile:
|
|
* gcc -DEL_TARGET_SDL2 $(sdl2-config --cflags) -c el_sdl2.c -o el_sdl2.o
|
|
* Link:
|
|
* $(sdl2-config --libs) -lSDL2_ttf -lSDL2_image -ldl
|
|
*/
|
|
|
|
#ifdef EL_TARGET_SDL2
|
|
|
|
#include <SDL2/SDL.h>
|
|
#include <SDL2/SDL_ttf.h>
|
|
#include <SDL2/SDL_image.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <dlfcn.h>
|
|
#include "el_runtime.h"
|
|
|
|
/* ── Widget type constants ────────────────────────────────────────────────── */
|
|
|
|
#define EL_WIDGET_NONE 0
|
|
#define EL_WIDGET_WINDOW 1
|
|
#define EL_WIDGET_VSTACK 2
|
|
#define EL_WIDGET_HSTACK 3
|
|
#define EL_WIDGET_ZSTACK 4
|
|
#define EL_WIDGET_SCROLL 5
|
|
#define EL_WIDGET_LABEL 6
|
|
#define EL_WIDGET_BUTTON 7
|
|
#define EL_WIDGET_TEXT_FIELD 8
|
|
#define EL_WIDGET_TEXT_AREA 9
|
|
#define EL_WIDGET_IMAGE 10
|
|
#define EL_WIDGET_DIVIDER 11
|
|
#define EL_WIDGET_SPACER 12
|
|
|
|
/* Layout direction stored in layout_type */
|
|
#define EL_LAYOUT_NONE 0
|
|
#define EL_LAYOUT_VSTACK 1
|
|
#define EL_LAYOUT_HSTACK 2
|
|
#define EL_LAYOUT_ZSTACK 3
|
|
|
|
#define EL_SDL2_MAX_WIDGETS 4096
|
|
|
|
/* ── Widget struct ────────────────────────────────────────────────────────── */
|
|
|
|
typedef struct {
|
|
int type; /* EL_WIDGET_* */
|
|
SDL_Rect bounds; /* computed layout position */
|
|
char text[512]; /* label text, button label, placeholder, img path */
|
|
char cb_click[256]; /* El callback fn name — button click / text submit */
|
|
char cb_change[256]; /* El callback fn name — text change */
|
|
char cb_submit[256]; /* El callback fn name — text submit (stored here, mirrored to cb_click) */
|
|
/* foreground colour */
|
|
uint8_t fg_r, fg_g, fg_b, fg_a;
|
|
/* background colour */
|
|
uint8_t bg_r, bg_g, bg_b, bg_a;
|
|
int font_size;
|
|
char font_family[128];
|
|
int font_bold;
|
|
int padding_t, padding_r, padding_b, padding_l;
|
|
int fixed_width, fixed_height; /* 0 = auto */
|
|
int flex;
|
|
int corner_radius;
|
|
int disabled;
|
|
int hidden;
|
|
/* tree */
|
|
int children[64];
|
|
int child_count;
|
|
int parent; /* -1 = root */
|
|
/* layout when this node IS a container */
|
|
int layout_type;
|
|
int layout_spacing;
|
|
/* text field / text area state */
|
|
char input_buf[1024];
|
|
int input_focused;
|
|
int cursor_pos;
|
|
/* hover state for button highlight */
|
|
int hovered;
|
|
/* cached texture for image (freed on destroy) */
|
|
SDL_Texture *img_texture;
|
|
} ElWidget;
|
|
|
|
/* ── Globals ──────────────────────────────────────────────────────────────── */
|
|
|
|
static ElWidget g_widgets[EL_SDL2_MAX_WIDGETS];
|
|
static int g_slot_used[EL_SDL2_MAX_WIDGETS];
|
|
static SDL_Window *g_sdl_window = NULL;
|
|
static SDL_Renderer *g_renderer = NULL;
|
|
static TTF_Font *g_font_default = NULL;
|
|
static int g_root_widget = -1;
|
|
static int g_focused_widget = -1;
|
|
static int g_dirty = 1; /* 1 = needs redraw */
|
|
static int g_win_w = 800;
|
|
static int g_win_h = 600;
|
|
|
|
/* ── Slot management ──────────────────────────────────────────────────────── */
|
|
|
|
static int64_t el_sdl2_alloc_slot(int type) {
|
|
for (int i = 1; i < EL_SDL2_MAX_WIDGETS; i++) {
|
|
if (!g_slot_used[i]) {
|
|
g_slot_used[i] = 1;
|
|
ElWidget *w = &g_widgets[i];
|
|
memset(w, 0, sizeof(*w));
|
|
w->type = type;
|
|
w->parent = -1;
|
|
w->layout_type = EL_LAYOUT_NONE;
|
|
/* default colours: white fg, transparent bg */
|
|
w->fg_r = 255; w->fg_g = 255; w->fg_b = 255; w->fg_a = 255;
|
|
w->bg_r = 0; w->bg_g = 0; w->bg_b = 0; w->bg_a = 0;
|
|
w->font_size = 14;
|
|
return (int64_t)i;
|
|
}
|
|
}
|
|
return -1; /* full */
|
|
}
|
|
|
|
static ElWidget *el_sdl2_get(int64_t handle) {
|
|
if (handle <= 0 || handle >= EL_SDL2_MAX_WIDGETS) return NULL;
|
|
if (!g_slot_used[handle]) return NULL;
|
|
return &g_widgets[handle];
|
|
}
|
|
|
|
static void el_sdl2_free_slot(int64_t handle) {
|
|
if (handle <= 0 || handle >= EL_SDL2_MAX_WIDGETS) return;
|
|
ElWidget *w = &g_widgets[handle];
|
|
if (w->img_texture) {
|
|
SDL_DestroyTexture(w->img_texture);
|
|
w->img_texture = NULL;
|
|
}
|
|
g_slot_used[handle] = 0;
|
|
}
|
|
|
|
/* ── Font loading ─────────────────────────────────────────────────────────── */
|
|
|
|
static TTF_Font *el_sdl2_load_font(int size) {
|
|
static const char *paths[] = {
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
|
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
|
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
|
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
|
|
"/System/Library/Fonts/Helvetica.ttc", /* macOS dev */
|
|
NULL
|
|
};
|
|
for (int i = 0; paths[i]; i++) {
|
|
TTF_Font *f = TTF_OpenFont(paths[i], size);
|
|
if (f) return f;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static TTF_Font *el_sdl2_get_font(int size) {
|
|
if (size <= 0) size = 14;
|
|
/* Simple approach: reuse the default font if size matches, else open again.
|
|
* For Pi Zero with one app, this is sufficient. */
|
|
if (g_font_default && size == 14) return g_font_default;
|
|
return el_sdl2_load_font(size);
|
|
}
|
|
|
|
/* ── Callback dispatch ────────────────────────────────────────────────────── */
|
|
|
|
static void el_sdl2_fire_cb(const char *fn_name, int64_t handle, const char *data) {
|
|
if (!fn_name || !fn_name[0]) return;
|
|
typedef el_val_t (*el_cb_fn)(el_val_t, el_val_t);
|
|
el_cb_fn fn = (el_cb_fn)(uintptr_t)dlsym(RTLD_DEFAULT, fn_name);
|
|
if (fn) {
|
|
el_val_t d = data ? (el_val_t)(uintptr_t)data : (el_val_t)0;
|
|
fn((el_val_t)handle, d);
|
|
g_dirty = 1;
|
|
}
|
|
}
|
|
|
|
/* ── Rounded rectangle helpers ────────────────────────────────────────────── */
|
|
|
|
/* Draw a filled rounded rectangle approximation using SDL_RenderFillRect.
|
|
* The approach: draw the centre cross and fill the 4 corner arcs by drawing
|
|
* small squares step-by-step. Sufficient for Pi Zero where SDL_gfx may not
|
|
* be available. */
|
|
static void el_sdl2_fill_rounded_rect(SDL_Renderer *r, const SDL_Rect *rect,
|
|
int radius,
|
|
uint8_t cr, uint8_t cg, uint8_t cb, uint8_t ca) {
|
|
if (radius <= 0 || rect->w <= 0 || rect->h <= 0) {
|
|
SDL_SetRenderDrawColor(r, cr, cg, cb, ca);
|
|
SDL_RenderFillRect(r, rect);
|
|
return;
|
|
}
|
|
if (radius > rect->w / 2) radius = rect->w / 2;
|
|
if (radius > rect->h / 2) radius = rect->h / 2;
|
|
|
|
SDL_SetRenderDrawColor(r, cr, cg, cb, ca);
|
|
|
|
/* Horizontal band (full width, inner height minus corners) */
|
|
SDL_Rect horiz = {rect->x, rect->y + radius, rect->w, rect->h - 2 * radius};
|
|
SDL_RenderFillRect(r, &horiz);
|
|
|
|
/* Top band */
|
|
SDL_Rect top = {rect->x + radius, rect->y, rect->w - 2 * radius, radius};
|
|
SDL_RenderFillRect(r, &top);
|
|
|
|
/* Bottom band */
|
|
SDL_Rect bot = {rect->x + radius, rect->y + rect->h - radius, rect->w - 2 * radius, radius};
|
|
SDL_RenderFillRect(r, &bot);
|
|
|
|
/* Draw quarter circles at each corner using row-by-row filling.
|
|
* For each row dy in [0, radius), compute the chord width from circle eq. */
|
|
for (int dy = 0; dy < radius; dy++) {
|
|
/* x-extent of circle arc at this dy from centre */
|
|
double dx = radius - 1 - (int)(0.5 + (double)radius *
|
|
(1.0 - ((double)(radius - 1 - dy) * (double)(radius - 1 - dy)) /
|
|
((double)radius * (double)radius)));
|
|
int fill_w = radius - (int)dx;
|
|
if (fill_w < 1) fill_w = 1;
|
|
|
|
/* top-left corner */
|
|
SDL_Rect tl = {rect->x + radius - fill_w, rect->y + dy, fill_w, 1};
|
|
SDL_RenderFillRect(r, &tl);
|
|
/* top-right corner */
|
|
SDL_Rect tr = {rect->x + rect->w - radius, rect->y + dy, fill_w, 1};
|
|
SDL_RenderFillRect(r, &tr);
|
|
/* bottom-left corner */
|
|
SDL_Rect bl = {rect->x + radius - fill_w, rect->y + rect->h - 1 - dy, fill_w, 1};
|
|
SDL_RenderFillRect(r, &bl);
|
|
/* bottom-right corner */
|
|
SDL_Rect br = {rect->x + rect->w - radius, rect->y + rect->h - 1 - dy, fill_w, 1};
|
|
SDL_RenderFillRect(r, &br);
|
|
}
|
|
}
|
|
|
|
/* Draw a 1px border around a (possibly rounded) rect */
|
|
static void el_sdl2_draw_border(SDL_Renderer *r, const SDL_Rect *rect,
|
|
uint8_t cr, uint8_t cg, uint8_t cb, uint8_t ca) {
|
|
SDL_SetRenderDrawColor(r, cr, cg, cb, ca);
|
|
SDL_RenderDrawRect(r, rect);
|
|
}
|
|
|
|
/* ── Layout engine ────────────────────────────────────────────────────────── */
|
|
|
|
/* Measure the natural size of a leaf widget.
|
|
* Returns 0 if size could not be determined. */
|
|
static void el_sdl2_measure(int slot, int *out_w, int *out_h) {
|
|
ElWidget *w = el_sdl2_get(slot);
|
|
if (!w) { *out_w = 0; *out_h = 0; return; }
|
|
|
|
int fs = w->font_size > 0 ? w->font_size : 14;
|
|
TTF_Font *font = el_sdl2_get_font(fs);
|
|
|
|
switch (w->type) {
|
|
case EL_WIDGET_LABEL: {
|
|
if (font && w->text[0]) {
|
|
int tw = 0, th = 0;
|
|
TTF_SizeUTF8(font, w->text, &tw, &th);
|
|
*out_w = tw + w->padding_l + w->padding_r;
|
|
*out_h = th + w->padding_t + w->padding_b;
|
|
} else {
|
|
*out_w = 8 + w->padding_l + w->padding_r;
|
|
*out_h = fs + 4 + w->padding_t + w->padding_b;
|
|
}
|
|
break;
|
|
}
|
|
case EL_WIDGET_BUTTON: {
|
|
if (font && w->text[0]) {
|
|
int tw = 0, th = 0;
|
|
TTF_SizeUTF8(font, w->text, &tw, &th);
|
|
*out_w = tw + 32 + w->padding_l + w->padding_r;
|
|
*out_h = th + 20 + w->padding_t + w->padding_b;
|
|
} else {
|
|
*out_w = 80 + w->padding_l + w->padding_r;
|
|
*out_h = fs + 20 + w->padding_t + w->padding_b;
|
|
}
|
|
break;
|
|
}
|
|
case EL_WIDGET_TEXT_FIELD:
|
|
case EL_WIDGET_TEXT_AREA: {
|
|
*out_w = 200; /* default — will stretch with flex */
|
|
*out_h = fs + 12 + w->padding_t + w->padding_b;
|
|
if (w->type == EL_WIDGET_TEXT_AREA) *out_h = (fs + 4) * 4 + w->padding_t + w->padding_b;
|
|
break;
|
|
}
|
|
case EL_WIDGET_IMAGE: {
|
|
*out_w = 64;
|
|
*out_h = 64;
|
|
break;
|
|
}
|
|
case EL_WIDGET_SPACER: {
|
|
*out_w = 0;
|
|
*out_h = 0;
|
|
break;
|
|
}
|
|
case EL_WIDGET_DIVIDER: {
|
|
*out_w = 2;
|
|
*out_h = 2;
|
|
break;
|
|
}
|
|
default: {
|
|
*out_w = 0;
|
|
*out_h = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Fixed dimensions override natural size */
|
|
if (w->fixed_width > 0) *out_w = w->fixed_width;
|
|
if (w->fixed_height > 0) *out_h = w->fixed_height;
|
|
}
|
|
|
|
/* Forward declaration */
|
|
static void el_sdl2_layout(int slot, SDL_Rect available);
|
|
|
|
static void el_sdl2_layout_vstack(ElWidget *w, SDL_Rect inner) {
|
|
int spacing = w->layout_spacing;
|
|
int n = w->child_count;
|
|
if (n == 0) return;
|
|
|
|
/* First pass: measure non-flex children and sum flex weights */
|
|
int total_fixed_h = 0;
|
|
int total_flex = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
int ci = w->children[i];
|
|
ElWidget *cw = el_sdl2_get(ci);
|
|
if (!cw || cw->hidden) continue;
|
|
if (cw->flex > 0) {
|
|
total_flex += cw->flex;
|
|
} else {
|
|
int mw, mh;
|
|
el_sdl2_measure(ci, &mw, &mh);
|
|
total_fixed_h += mh;
|
|
}
|
|
}
|
|
/* Count spacing gaps */
|
|
int gaps = (n > 1) ? (n - 1) * spacing : 0;
|
|
int flex_pool = inner.h - total_fixed_h - gaps;
|
|
if (flex_pool < 0) flex_pool = 0;
|
|
|
|
/* Second pass: assign rects */
|
|
int y = inner.y;
|
|
for (int i = 0; i < n; i++) {
|
|
int ci = w->children[i];
|
|
ElWidget *cw = el_sdl2_get(ci);
|
|
if (!cw) continue;
|
|
if (cw->hidden) {
|
|
SDL_Rect z = {0, 0, 0, 0};
|
|
cw->bounds = z;
|
|
continue;
|
|
}
|
|
int ch;
|
|
if (cw->flex > 0 && total_flex > 0) {
|
|
ch = (flex_pool * cw->flex) / total_flex;
|
|
} else {
|
|
int mw, mh;
|
|
el_sdl2_measure(ci, &mw, &mh);
|
|
ch = mh;
|
|
}
|
|
SDL_Rect child_rect = {inner.x, y, inner.w, ch};
|
|
el_sdl2_layout(ci, child_rect);
|
|
y += ch + spacing;
|
|
}
|
|
}
|
|
|
|
static void el_sdl2_layout_hstack(ElWidget *w, SDL_Rect inner) {
|
|
int spacing = w->layout_spacing;
|
|
int n = w->child_count;
|
|
if (n == 0) return;
|
|
|
|
int total_fixed_w = 0;
|
|
int total_flex = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
int ci = w->children[i];
|
|
ElWidget *cw = el_sdl2_get(ci);
|
|
if (!cw || cw->hidden) continue;
|
|
if (cw->flex > 0) {
|
|
total_flex += cw->flex;
|
|
} else {
|
|
int mw, mh;
|
|
el_sdl2_measure(ci, &mw, &mh);
|
|
total_fixed_w += mw;
|
|
}
|
|
}
|
|
int gaps = (n > 1) ? (n - 1) * spacing : 0;
|
|
int flex_pool = inner.w - total_fixed_w - gaps;
|
|
if (flex_pool < 0) flex_pool = 0;
|
|
|
|
int x = inner.x;
|
|
for (int i = 0; i < n; i++) {
|
|
int ci = w->children[i];
|
|
ElWidget *cw = el_sdl2_get(ci);
|
|
if (!cw) continue;
|
|
if (cw->hidden) {
|
|
SDL_Rect z = {0, 0, 0, 0};
|
|
cw->bounds = z;
|
|
continue;
|
|
}
|
|
int cw_size;
|
|
if (cw->flex > 0 && total_flex > 0) {
|
|
cw_size = (flex_pool * cw->flex) / total_flex;
|
|
} else {
|
|
int mw, mh;
|
|
el_sdl2_measure(ci, &mw, &mh);
|
|
cw_size = mw;
|
|
}
|
|
SDL_Rect child_rect = {x, inner.y, cw_size, inner.h};
|
|
el_sdl2_layout(ci, child_rect);
|
|
x += cw_size + spacing;
|
|
}
|
|
}
|
|
|
|
static void el_sdl2_layout(int slot, SDL_Rect available) {
|
|
ElWidget *w = el_sdl2_get(slot);
|
|
if (!w) return;
|
|
|
|
if (w->hidden) {
|
|
SDL_Rect z = {0, 0, 0, 0};
|
|
w->bounds = z;
|
|
return;
|
|
}
|
|
|
|
/* Apply fixed dimensions if set */
|
|
if (w->fixed_width > 0) available.w = w->fixed_width;
|
|
if (w->fixed_height > 0) available.h = w->fixed_height;
|
|
|
|
w->bounds = available;
|
|
|
|
/* For containers: compute inner rect after padding, then layout children */
|
|
SDL_Rect inner = {
|
|
available.x + w->padding_l,
|
|
available.y + w->padding_t,
|
|
available.w - w->padding_l - w->padding_r,
|
|
available.h - w->padding_t - w->padding_b
|
|
};
|
|
if (inner.w < 0) inner.w = 0;
|
|
if (inner.h < 0) inner.h = 0;
|
|
|
|
switch (w->layout_type) {
|
|
case EL_LAYOUT_VSTACK:
|
|
el_sdl2_layout_vstack(w, inner);
|
|
break;
|
|
case EL_LAYOUT_HSTACK:
|
|
el_sdl2_layout_hstack(w, inner);
|
|
break;
|
|
case EL_LAYOUT_ZSTACK:
|
|
/* Each child gets the full inner rect */
|
|
for (int i = 0; i < w->child_count; i++) {
|
|
el_sdl2_layout(w->children[i], inner);
|
|
}
|
|
break;
|
|
default:
|
|
/* Leaf: layout_type NONE means no children.
|
|
* WINDOW/SCROLL/etc. that has a content child: give it full inner. */
|
|
if (w->type == EL_WIDGET_WINDOW || w->type == EL_WIDGET_SCROLL) {
|
|
for (int i = 0; i < w->child_count; i++) {
|
|
el_sdl2_layout(w->children[i], inner);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ── Render pass ──────────────────────────────────────────────────────────── */
|
|
|
|
static void el_sdl2_render_text_in_rect(TTF_Font *font, const char *text,
|
|
SDL_Color col, SDL_Rect dst) {
|
|
if (!font || !text || !text[0]) return;
|
|
SDL_Surface *surf = TTF_RenderUTF8_Blended(font, text, col);
|
|
if (!surf) return;
|
|
SDL_Texture *tex = SDL_CreateTextureFromSurface(g_renderer, surf);
|
|
SDL_FreeSurface(surf);
|
|
if (!tex) return;
|
|
|
|
int tw, th;
|
|
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
|
|
|
|
/* Clip to dst rect */
|
|
SDL_Rect src_rect = {0, 0, tw, th};
|
|
SDL_Rect dst_rect = dst;
|
|
/* Centre vertically; left-align horizontally */
|
|
dst_rect.y = dst.y + (dst.h - th) / 2;
|
|
dst_rect.h = th;
|
|
dst_rect.w = tw;
|
|
if (dst_rect.w > dst.w) {
|
|
dst_rect.w = dst.w;
|
|
src_rect.w = dst.w;
|
|
}
|
|
|
|
SDL_RenderSetClipRect(g_renderer, &dst);
|
|
SDL_RenderCopy(g_renderer, tex, &src_rect, &dst_rect);
|
|
SDL_RenderSetClipRect(g_renderer, NULL);
|
|
SDL_DestroyTexture(tex);
|
|
}
|
|
|
|
static void el_sdl2_render_text_centered(TTF_Font *font, const char *text,
|
|
SDL_Color col, SDL_Rect dst) {
|
|
if (!font || !text || !text[0]) return;
|
|
SDL_Surface *surf = TTF_RenderUTF8_Blended(font, text, col);
|
|
if (!surf) return;
|
|
SDL_Texture *tex = SDL_CreateTextureFromSurface(g_renderer, surf);
|
|
SDL_FreeSurface(surf);
|
|
if (!tex) return;
|
|
|
|
int tw, th;
|
|
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
|
|
|
|
SDL_Rect src_rect = {0, 0, tw, th};
|
|
if (src_rect.w > dst.w) src_rect.w = dst.w;
|
|
|
|
SDL_Rect dst_rect;
|
|
dst_rect.x = dst.x + (dst.w - src_rect.w) / 2;
|
|
dst_rect.y = dst.y + (dst.h - th) / 2;
|
|
dst_rect.w = src_rect.w;
|
|
dst_rect.h = th;
|
|
|
|
SDL_RenderSetClipRect(g_renderer, &dst);
|
|
SDL_RenderCopy(g_renderer, tex, &src_rect, &dst_rect);
|
|
SDL_RenderSetClipRect(g_renderer, NULL);
|
|
SDL_DestroyTexture(tex);
|
|
}
|
|
|
|
/* Forward declaration */
|
|
static void el_sdl2_render_widget(int slot);
|
|
|
|
static void el_sdl2_render_widget(int slot) {
|
|
ElWidget *w = el_sdl2_get(slot);
|
|
if (!w || w->hidden) return;
|
|
|
|
SDL_Rect b = w->bounds;
|
|
if (b.w <= 0 || b.h <= 0) return;
|
|
|
|
int fs = w->font_size > 0 ? w->font_size : 14;
|
|
TTF_Font *font = el_sdl2_get_font(fs);
|
|
SDL_Color fg = {w->fg_r, w->fg_g, w->fg_b, w->fg_a};
|
|
|
|
/* ── Draw background ── */
|
|
if (w->bg_a > 0) {
|
|
el_sdl2_fill_rounded_rect(g_renderer, &b, w->corner_radius,
|
|
w->bg_r, w->bg_g, w->bg_b, w->bg_a);
|
|
}
|
|
|
|
/* ── Widget-specific drawing ── */
|
|
switch (w->type) {
|
|
case EL_WIDGET_WINDOW: {
|
|
/* Window just provides a background; children render themselves. */
|
|
/* Fill entire window with bg (default dark) */
|
|
SDL_Rect win_rect = {0, 0, g_win_w, g_win_h};
|
|
uint8_t br = w->bg_a > 0 ? w->bg_r : 30;
|
|
uint8_t bg_g = w->bg_a > 0 ? w->bg_g : 30;
|
|
uint8_t bb = w->bg_a > 0 ? w->bg_b : 30;
|
|
SDL_SetRenderDrawColor(g_renderer, br, bg_g, bb, 255);
|
|
SDL_RenderFillRect(g_renderer, &win_rect);
|
|
break;
|
|
}
|
|
|
|
case EL_WIDGET_VSTACK:
|
|
case EL_WIDGET_HSTACK:
|
|
case EL_WIDGET_ZSTACK:
|
|
case EL_WIDGET_SCROLL:
|
|
/* Containers: bg already drawn above. Children rendered below. */
|
|
break;
|
|
|
|
case EL_WIDGET_LABEL: {
|
|
/* Text content */
|
|
SDL_Rect inner = {
|
|
b.x + w->padding_l,
|
|
b.y + w->padding_t,
|
|
b.w - w->padding_l - w->padding_r,
|
|
b.h - w->padding_t - w->padding_b
|
|
};
|
|
el_sdl2_render_text_in_rect(font, w->text, fg, inner);
|
|
break;
|
|
}
|
|
|
|
case EL_WIDGET_BUTTON: {
|
|
/* Button body */
|
|
uint8_t btn_r, btn_g, btn_b;
|
|
if (w->disabled) {
|
|
btn_r = 80; btn_g = 80; btn_b = 80;
|
|
} else if (w->hovered) {
|
|
btn_r = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_r + 40) : 100);
|
|
btn_g = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_g + 40) : 100);
|
|
btn_b = (uint8_t)(w->bg_a > 0 ? SDL_min(255, (int)w->bg_b + 40) : 140);
|
|
} else {
|
|
btn_r = w->bg_a > 0 ? w->bg_r : 60;
|
|
btn_g = w->bg_a > 0 ? w->bg_g : 60;
|
|
btn_b = w->bg_a > 0 ? w->bg_b : 100;
|
|
}
|
|
el_sdl2_fill_rounded_rect(g_renderer, &b, w->corner_radius > 0 ? w->corner_radius : 4,
|
|
btn_r, btn_g, btn_b, 255);
|
|
|
|
/* Border */
|
|
SDL_SetRenderDrawColor(g_renderer, 140, 140, 200, 255);
|
|
SDL_RenderDrawRect(g_renderer, &b);
|
|
|
|
/* Centered label */
|
|
SDL_Color label_col = {w->fg_r, w->fg_g, w->fg_b, w->disabled ? 128u : 255u};
|
|
SDL_Rect inner = {
|
|
b.x + w->padding_l + 4,
|
|
b.y + w->padding_t,
|
|
b.w - w->padding_l - w->padding_r - 8,
|
|
b.h - w->padding_t - w->padding_b
|
|
};
|
|
el_sdl2_render_text_centered(font, w->text, label_col, inner);
|
|
break;
|
|
}
|
|
|
|
case EL_WIDGET_TEXT_FIELD:
|
|
case EL_WIDGET_TEXT_AREA: {
|
|
/* Background */
|
|
uint8_t field_bg_r = 40, field_bg_g = 40, field_bg_b = 50;
|
|
if (w->bg_a > 0) { field_bg_r = w->bg_r; field_bg_g = w->bg_g; field_bg_b = w->bg_b; }
|
|
el_sdl2_fill_rounded_rect(g_renderer, &b, 3,
|
|
field_bg_r, field_bg_g, field_bg_b, 255);
|
|
|
|
/* Border — highlighted when focused */
|
|
if (w->input_focused) {
|
|
SDL_SetRenderDrawColor(g_renderer, 80, 140, 220, 255);
|
|
} else {
|
|
SDL_SetRenderDrawColor(g_renderer, 100, 100, 120, 255);
|
|
}
|
|
SDL_RenderDrawRect(g_renderer, &b);
|
|
|
|
SDL_Rect inner = {
|
|
b.x + 6 + w->padding_l,
|
|
b.y + w->padding_t,
|
|
b.w - 12 - w->padding_l - w->padding_r,
|
|
b.h - w->padding_t - w->padding_b
|
|
};
|
|
|
|
const char *display = w->input_buf[0] ? w->input_buf : w->text; /* text = placeholder */
|
|
SDL_Color text_col;
|
|
if (!w->input_buf[0] && w->text[0]) {
|
|
/* placeholder style: dimmed */
|
|
text_col = (SDL_Color){120, 120, 140, 255};
|
|
} else {
|
|
text_col = fg;
|
|
}
|
|
|
|
el_sdl2_render_text_in_rect(font, display, text_col, inner);
|
|
|
|
/* Draw cursor if focused */
|
|
if (w->input_focused && font) {
|
|
/* Measure text up to cursor position */
|
|
char before_cursor[1024];
|
|
int cplen = w->cursor_pos;
|
|
if (cplen < 0) cplen = 0;
|
|
int buflen = (int)strlen(w->input_buf);
|
|
if (cplen > buflen) cplen = buflen;
|
|
memcpy(before_cursor, w->input_buf, cplen);
|
|
before_cursor[cplen] = '\0';
|
|
int cx = 0, cy_unused = 0;
|
|
if (before_cursor[0]) {
|
|
TTF_SizeUTF8(font, before_cursor, &cx, &cy_unused);
|
|
}
|
|
int cursor_x = inner.x + cx;
|
|
int cursor_y1 = b.y + 3;
|
|
int cursor_y2 = b.y + b.h - 3;
|
|
SDL_SetRenderDrawColor(g_renderer, 200, 200, 255, 255);
|
|
SDL_RenderDrawLine(g_renderer, cursor_x, cursor_y1, cursor_x, cursor_y2);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EL_WIDGET_IMAGE: {
|
|
/* Load or reuse cached texture */
|
|
if (!w->img_texture && w->text[0]) {
|
|
SDL_Surface *surf = IMG_Load(w->text);
|
|
if (!surf) {
|
|
/* Try as raw SDL surface */
|
|
surf = SDL_LoadBMP(w->text);
|
|
}
|
|
if (surf) {
|
|
w->img_texture = SDL_CreateTextureFromSurface(g_renderer, surf);
|
|
SDL_FreeSurface(surf);
|
|
}
|
|
}
|
|
if (w->img_texture) {
|
|
SDL_RenderCopy(g_renderer, w->img_texture, NULL, &b);
|
|
} else {
|
|
/* Placeholder: grey box with X */
|
|
SDL_SetRenderDrawColor(g_renderer, 80, 80, 80, 255);
|
|
SDL_RenderFillRect(g_renderer, &b);
|
|
SDL_SetRenderDrawColor(g_renderer, 140, 140, 140, 255);
|
|
SDL_RenderDrawLine(g_renderer, b.x, b.y, b.x + b.w, b.y + b.h);
|
|
SDL_RenderDrawLine(g_renderer, b.x + b.w, b.y, b.x, b.y + b.h);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EL_WIDGET_DIVIDER: {
|
|
SDL_SetRenderDrawColor(g_renderer, 80, 80, 80, 255);
|
|
SDL_RenderFillRect(g_renderer, &b);
|
|
break;
|
|
}
|
|
|
|
case EL_WIDGET_SPACER:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* ── Recurse into children ── */
|
|
for (int i = 0; i < w->child_count; i++) {
|
|
el_sdl2_render_widget(w->children[i]);
|
|
}
|
|
}
|
|
|
|
static void el_sdl2_render(void) {
|
|
/* Clear with dark background */
|
|
SDL_SetRenderDrawColor(g_renderer, 30, 30, 30, 255);
|
|
SDL_RenderClear(g_renderer);
|
|
|
|
if (g_root_widget >= 0) {
|
|
el_sdl2_render_widget(g_root_widget);
|
|
}
|
|
}
|
|
|
|
/* ── Event handling ───────────────────────────────────────────────────────── */
|
|
|
|
/* Hit-test: find the deepest interactive widget at (x, y).
|
|
* Returns slot index or -1. */
|
|
static int el_sdl2_hit_test(int slot, int x, int y) {
|
|
ElWidget *w = el_sdl2_get(slot);
|
|
if (!w || w->hidden) return -1;
|
|
|
|
SDL_Rect b = w->bounds;
|
|
if (x < b.x || x >= b.x + b.w) return -1;
|
|
if (y < b.y || y >= b.y + b.h) return -1;
|
|
|
|
/* Check children first (front-to-back; last child = topmost) */
|
|
for (int i = w->child_count - 1; i >= 0; i--) {
|
|
int r = el_sdl2_hit_test(w->children[i], x, y);
|
|
if (r >= 0) return r;
|
|
}
|
|
|
|
/* This widget itself is a candidate if interactive */
|
|
if (w->type == EL_WIDGET_BUTTON ||
|
|
w->type == EL_WIDGET_TEXT_FIELD ||
|
|
w->type == EL_WIDGET_TEXT_AREA) {
|
|
return slot;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/* Clear hover on all buttons */
|
|
static void el_sdl2_clear_hover(int slot) {
|
|
ElWidget *w = el_sdl2_get(slot);
|
|
if (!w) return;
|
|
if (w->type == EL_WIDGET_BUTTON) w->hovered = 0;
|
|
for (int i = 0; i < w->child_count; i++) {
|
|
el_sdl2_clear_hover(w->children[i]);
|
|
}
|
|
}
|
|
|
|
static void el_sdl2_handle_event(SDL_Event *e) {
|
|
switch (e->type) {
|
|
case SDL_QUIT:
|
|
exit(0);
|
|
break;
|
|
|
|
case SDL_WINDOWEVENT:
|
|
if (e->window.event == SDL_WINDOWEVENT_RESIZED ||
|
|
e->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
|
g_win_w = e->window.data1;
|
|
g_win_h = e->window.data2;
|
|
/* Re-layout root */
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
break;
|
|
|
|
case SDL_MOUSEBUTTONDOWN: {
|
|
if (e->button.button != SDL_BUTTON_LEFT) break;
|
|
int mx = e->button.x;
|
|
int my = e->button.y;
|
|
|
|
/* Unfocus previous text field */
|
|
if (g_focused_widget >= 0) {
|
|
ElWidget *fw = el_sdl2_get(g_focused_widget);
|
|
if (fw) fw->input_focused = 0;
|
|
}
|
|
g_focused_widget = -1;
|
|
|
|
int hit = (g_root_widget >= 0) ? el_sdl2_hit_test(g_root_widget, mx, my) : -1;
|
|
if (hit < 0) break;
|
|
|
|
ElWidget *hw = el_sdl2_get(hit);
|
|
if (!hw) break;
|
|
|
|
if (hw->type == EL_WIDGET_BUTTON && !hw->disabled) {
|
|
el_sdl2_fire_cb(hw->cb_click, (int64_t)hit, "");
|
|
} else if (hw->type == EL_WIDGET_TEXT_FIELD ||
|
|
hw->type == EL_WIDGET_TEXT_AREA) {
|
|
hw->input_focused = 1;
|
|
g_focused_widget = hit;
|
|
SDL_StartTextInput();
|
|
}
|
|
g_dirty = 1;
|
|
break;
|
|
}
|
|
|
|
case SDL_MOUSEMOTION: {
|
|
if (g_root_widget < 0) break;
|
|
int mx = e->motion.x;
|
|
int my = e->motion.y;
|
|
int prev_hover = -1;
|
|
/* Find any currently hovered button */
|
|
for (int i = 1; i < EL_SDL2_MAX_WIDGETS; i++) {
|
|
if (g_slot_used[i] && g_widgets[i].type == EL_WIDGET_BUTTON &&
|
|
g_widgets[i].hovered) {
|
|
prev_hover = i;
|
|
break;
|
|
}
|
|
}
|
|
/* Clear all hover */
|
|
el_sdl2_clear_hover(g_root_widget);
|
|
/* Set hover on current button under cursor */
|
|
int hit = el_sdl2_hit_test(g_root_widget, mx, my);
|
|
if (hit >= 0) {
|
|
ElWidget *hw = el_sdl2_get(hit);
|
|
if (hw && hw->type == EL_WIDGET_BUTTON && !hw->disabled) {
|
|
hw->hovered = 1;
|
|
}
|
|
}
|
|
if (hit != prev_hover) g_dirty = 1;
|
|
break;
|
|
}
|
|
|
|
case SDL_KEYDOWN: {
|
|
if (g_focused_widget < 0) break;
|
|
ElWidget *fw = el_sdl2_get(g_focused_widget);
|
|
if (!fw) break;
|
|
|
|
SDL_Keycode key = e->key.keysym.sym;
|
|
|
|
if (key == SDLK_BACKSPACE) {
|
|
int len = (int)strlen(fw->input_buf);
|
|
if (len > 0 && fw->cursor_pos > 0) {
|
|
/* Remove character before cursor */
|
|
int cp = fw->cursor_pos;
|
|
memmove(fw->input_buf + cp - 1, fw->input_buf + cp,
|
|
(size_t)(len - cp + 1));
|
|
fw->cursor_pos--;
|
|
el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf);
|
|
}
|
|
g_dirty = 1;
|
|
} else if (key == SDLK_DELETE) {
|
|
int len = (int)strlen(fw->input_buf);
|
|
if (fw->cursor_pos < len) {
|
|
memmove(fw->input_buf + fw->cursor_pos,
|
|
fw->input_buf + fw->cursor_pos + 1,
|
|
(size_t)(len - fw->cursor_pos));
|
|
el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf);
|
|
g_dirty = 1;
|
|
}
|
|
} else if (key == SDLK_LEFT) {
|
|
if (fw->cursor_pos > 0) { fw->cursor_pos--; g_dirty = 1; }
|
|
} else if (key == SDLK_RIGHT) {
|
|
int len = (int)strlen(fw->input_buf);
|
|
if (fw->cursor_pos < len) { fw->cursor_pos++; g_dirty = 1; }
|
|
} else if (key == SDLK_HOME) {
|
|
fw->cursor_pos = 0; g_dirty = 1;
|
|
} else if (key == SDLK_END) {
|
|
fw->cursor_pos = (int)strlen(fw->input_buf); g_dirty = 1;
|
|
} else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) {
|
|
if (fw->type == EL_WIDGET_TEXT_FIELD) {
|
|
/* Submit */
|
|
el_sdl2_fire_cb(fw->cb_submit[0] ? fw->cb_submit : fw->cb_click,
|
|
(int64_t)g_focused_widget, fw->input_buf);
|
|
g_dirty = 1;
|
|
} else {
|
|
/* Text area: insert newline via SDL_TEXTINPUT */
|
|
}
|
|
} else if (key == SDLK_TAB) {
|
|
/* Blur current field */
|
|
fw->input_focused = 0;
|
|
SDL_StopTextInput();
|
|
g_focused_widget = -1;
|
|
g_dirty = 1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case SDL_TEXTINPUT: {
|
|
if (g_focused_widget < 0) break;
|
|
ElWidget *fw = el_sdl2_get(g_focused_widget);
|
|
if (!fw) break;
|
|
|
|
const char *inp = e->text.text;
|
|
int inp_len = (int)strlen(inp);
|
|
int buf_len = (int)strlen(fw->input_buf);
|
|
int max_buf = (int)sizeof(fw->input_buf) - 1;
|
|
|
|
if (buf_len + inp_len <= max_buf) {
|
|
/* Insert at cursor position */
|
|
int cp = fw->cursor_pos;
|
|
memmove(fw->input_buf + cp + inp_len, fw->input_buf + cp,
|
|
(size_t)(buf_len - cp + 1));
|
|
memcpy(fw->input_buf + cp, inp, (size_t)inp_len);
|
|
fw->cursor_pos += inp_len;
|
|
el_sdl2_fire_cb(fw->cb_change, (int64_t)g_focused_widget, fw->input_buf);
|
|
g_dirty = 1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ── Public API — called from el_seed.c wrappers ──────────────────────────── */
|
|
|
|
void el_sdl2_init(void) {
|
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
|
fprintf(stderr, "el_sdl2: SDL_Init failed: %s\n", SDL_GetError());
|
|
exit(1);
|
|
}
|
|
if (TTF_Init() != 0) {
|
|
fprintf(stderr, "el_sdl2: TTF_Init failed: %s\n", TTF_GetError());
|
|
exit(1);
|
|
}
|
|
if (!(IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG) & (IMG_INIT_PNG | IMG_INIT_JPG))) {
|
|
fprintf(stderr, "el_sdl2: IMG_Init warning: %s\n", IMG_GetError());
|
|
/* Non-fatal: images won't load, but text UI still works */
|
|
}
|
|
|
|
/* Load default font */
|
|
g_font_default = el_sdl2_load_font(14);
|
|
if (!g_font_default) {
|
|
fprintf(stderr, "el_sdl2: warning: no system font found; text will not render\n");
|
|
}
|
|
}
|
|
|
|
void el_sdl2_run_loop(void) {
|
|
SDL_Event event;
|
|
while (1) {
|
|
while (SDL_PollEvent(&event)) {
|
|
el_sdl2_handle_event(&event);
|
|
}
|
|
if (g_dirty) {
|
|
el_sdl2_render();
|
|
SDL_RenderPresent(g_renderer);
|
|
g_dirty = 0;
|
|
}
|
|
SDL_Delay(16); /* ~60 fps cap */
|
|
}
|
|
}
|
|
|
|
int64_t el_sdl2_window_create(const char *title, int width, int height,
|
|
int min_width, int min_height) {
|
|
g_win_w = width > 0 ? width : 800;
|
|
g_win_h = height > 0 ? height : 600;
|
|
|
|
Uint32 flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE;
|
|
|
|
/* On Pi with no desktop: use fullscreen desktop if w/h not specified */
|
|
g_sdl_window = SDL_CreateWindow(
|
|
title ? title : "",
|
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
|
g_win_w, g_win_h, flags);
|
|
|
|
if (!g_sdl_window) {
|
|
fprintf(stderr, "el_sdl2: SDL_CreateWindow failed: %s\n", SDL_GetError());
|
|
return -1;
|
|
}
|
|
|
|
if (min_width > 0 || min_height > 0) {
|
|
SDL_SetWindowMinimumSize(g_sdl_window, min_width, min_height);
|
|
}
|
|
|
|
g_renderer = SDL_CreateRenderer(g_sdl_window, -1,
|
|
SDL_RENDERER_ACCELERATED |
|
|
SDL_RENDERER_PRESENTVSYNC);
|
|
if (!g_renderer) {
|
|
/* Fall back to software renderer (required on Pi Zero fbdev) */
|
|
g_renderer = SDL_CreateRenderer(g_sdl_window, -1, SDL_RENDERER_SOFTWARE);
|
|
}
|
|
if (!g_renderer) {
|
|
fprintf(stderr, "el_sdl2: SDL_CreateRenderer failed: %s\n", SDL_GetError());
|
|
return -1;
|
|
}
|
|
|
|
SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
|
|
|
|
int64_t handle = el_sdl2_alloc_slot(EL_WIDGET_WINDOW);
|
|
if (handle < 0) return -1;
|
|
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
snprintf(w->text, sizeof(w->text), "%s", title ? title : "");
|
|
w->layout_type = EL_LAYOUT_VSTACK;
|
|
w->layout_spacing = 0;
|
|
w->bounds.x = 0; w->bounds.y = 0;
|
|
w->bounds.w = g_win_w; w->bounds.h = g_win_h;
|
|
|
|
g_root_widget = (int)handle;
|
|
g_dirty = 1;
|
|
return handle;
|
|
}
|
|
|
|
void el_sdl2_window_show(int64_t handle) {
|
|
if (g_sdl_window) {
|
|
SDL_ShowWindow(g_sdl_window);
|
|
}
|
|
/* Trigger initial layout */
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_window_set_title(int64_t handle, const char *title) {
|
|
if (g_sdl_window) {
|
|
SDL_SetWindowTitle(g_sdl_window, title ? title : "");
|
|
}
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (w) snprintf(w->text, sizeof(w->text), "%s", title ? title : "");
|
|
g_dirty = 1;
|
|
}
|
|
|
|
int64_t el_sdl2_vstack_create(int spacing) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_VSTACK);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
w->layout_type = EL_LAYOUT_VSTACK;
|
|
w->layout_spacing = spacing;
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_hstack_create(int spacing) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_HSTACK);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
w->layout_type = EL_LAYOUT_HSTACK;
|
|
w->layout_spacing = spacing;
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_zstack_create(void) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_ZSTACK);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
w->layout_type = EL_LAYOUT_ZSTACK;
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_scroll_create(void) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_SCROLL);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
/* Scroll: single content child; layout passes full inner rect */
|
|
w->layout_type = EL_LAYOUT_NONE;
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_label_create(const char *text) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_LABEL);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
snprintf(w->text, sizeof(w->text), "%s", text ? text : "");
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_button_create(const char *label) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_BUTTON);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
snprintf(w->text, sizeof(w->text), "%s", label ? label : "");
|
|
w->padding_t = 4; w->padding_b = 4;
|
|
w->padding_l = 8; w->padding_r = 8;
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_text_field_create(const char *placeholder) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_TEXT_FIELD);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
/* text field: text[] holds the placeholder */
|
|
snprintf(w->text, sizeof(w->text), "%s", placeholder ? placeholder : "");
|
|
w->padding_t = 2; w->padding_b = 2;
|
|
w->padding_l = 4; w->padding_r = 4;
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_text_area_create(const char *placeholder) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_TEXT_AREA);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
snprintf(w->text, sizeof(w->text), "%s", placeholder ? placeholder : "");
|
|
w->padding_t = 4; w->padding_b = 4;
|
|
w->padding_l = 6; w->padding_r = 6;
|
|
return h;
|
|
}
|
|
|
|
int64_t el_sdl2_image_create(const char *path_or_name) {
|
|
int64_t h = el_sdl2_alloc_slot(EL_WIDGET_IMAGE);
|
|
if (h < 0) return -1;
|
|
ElWidget *w = el_sdl2_get(h);
|
|
snprintf(w->text, sizeof(w->text), "%s", path_or_name ? path_or_name : "");
|
|
return h;
|
|
}
|
|
|
|
/* ── Widget property setters ─────────────────────────────────────────────── */
|
|
|
|
void el_sdl2_widget_set_text(int64_t handle, const char *text) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
if (w->type == EL_WIDGET_TEXT_FIELD || w->type == EL_WIDGET_TEXT_AREA) {
|
|
/* Setting text on a field = set input buffer content */
|
|
snprintf(w->input_buf, sizeof(w->input_buf), "%s", text ? text : "");
|
|
w->cursor_pos = (int)strlen(w->input_buf);
|
|
} else {
|
|
snprintf(w->text, sizeof(w->text), "%s", text ? text : "");
|
|
/* Invalidate image texture if path changed */
|
|
if (w->type == EL_WIDGET_IMAGE && w->img_texture) {
|
|
SDL_DestroyTexture(w->img_texture);
|
|
w->img_texture = NULL;
|
|
}
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
const char *el_sdl2_widget_get_text(int64_t handle) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return "";
|
|
if (w->type == EL_WIDGET_TEXT_FIELD || w->type == EL_WIDGET_TEXT_AREA) {
|
|
return w->input_buf;
|
|
}
|
|
return w->text;
|
|
}
|
|
|
|
void el_sdl2_widget_set_color(int64_t handle, float r, float g, float b, float a) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->fg_r = (uint8_t)(r * 255.0f);
|
|
w->fg_g = (uint8_t)(g * 255.0f);
|
|
w->fg_b = (uint8_t)(b * 255.0f);
|
|
w->fg_a = (uint8_t)(a * 255.0f);
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->bg_r = (uint8_t)(r * 255.0f);
|
|
w->bg_g = (uint8_t)(g * 255.0f);
|
|
w->bg_b = (uint8_t)(b * 255.0f);
|
|
w->bg_a = (uint8_t)(a * 255.0f);
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_font(int64_t handle, const char *family, int size, int bold) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
if (family) snprintf(w->font_family, sizeof(w->font_family), "%s", family);
|
|
if (size > 0) w->font_size = size;
|
|
w->font_bold = bold;
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->padding_t = top;
|
|
w->padding_r = right;
|
|
w->padding_b = bottom;
|
|
w->padding_l = left;
|
|
/* Re-layout from root */
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_width(int64_t handle, int width) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->fixed_width = width;
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_height(int64_t handle, int height) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->fixed_height = height;
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_flex(int64_t handle, int flex) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->flex = flex;
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_corner_radius(int64_t handle, int radius) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->corner_radius = radius;
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_disabled(int64_t handle, int disabled) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->disabled = disabled;
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_set_hidden(int64_t handle, int hidden) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
w->hidden = hidden;
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
/* ── Child management ─────────────────────────────────────────────────────── */
|
|
|
|
void el_sdl2_widget_add_child(int64_t parent, int64_t child) {
|
|
ElWidget *pw = el_sdl2_get(parent);
|
|
ElWidget *cw = el_sdl2_get(child);
|
|
if (!pw || !cw) return;
|
|
if (pw->child_count >= 64) return; /* max children per node */
|
|
|
|
/* Prevent duplicates */
|
|
for (int i = 0; i < pw->child_count; i++) {
|
|
if (pw->children[i] == (int)child) return;
|
|
}
|
|
|
|
pw->children[pw->child_count++] = (int)child;
|
|
cw->parent = (int)parent;
|
|
|
|
/* Re-layout */
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
void el_sdl2_widget_remove_child(int64_t parent, int64_t child) {
|
|
ElWidget *pw = el_sdl2_get(parent);
|
|
ElWidget *cw = el_sdl2_get(child);
|
|
if (!pw) return;
|
|
|
|
for (int i = 0; i < pw->child_count; i++) {
|
|
if (pw->children[i] == (int)child) {
|
|
memmove(&pw->children[i], &pw->children[i + 1],
|
|
(size_t)(pw->child_count - i - 1) * sizeof(int));
|
|
pw->child_count--;
|
|
if (cw) cw->parent = -1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (g_root_widget >= 0) {
|
|
SDL_Rect full = {0, 0, g_win_w, g_win_h};
|
|
el_sdl2_layout(g_root_widget, full);
|
|
}
|
|
g_dirty = 1;
|
|
}
|
|
|
|
/* ── Event registration ───────────────────────────────────────────────────── */
|
|
|
|
void el_sdl2_widget_on_click(int64_t handle, const char *fn_name) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
snprintf(w->cb_click, sizeof(w->cb_click), "%s", fn_name ? fn_name : "");
|
|
}
|
|
|
|
void el_sdl2_widget_on_change(int64_t handle, const char *fn_name) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
snprintf(w->cb_change, sizeof(w->cb_change), "%s", fn_name ? fn_name : "");
|
|
}
|
|
|
|
void el_sdl2_widget_on_submit(int64_t handle, const char *fn_name) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
snprintf(w->cb_submit, sizeof(w->cb_submit), "%s", fn_name ? fn_name : "");
|
|
/* Mirror into cb_click so RETURN key fires via cb_click fallback too */
|
|
snprintf(w->cb_click, sizeof(w->cb_click), "%s", fn_name ? fn_name : "");
|
|
}
|
|
|
|
/* ── Widget destroy ───────────────────────────────────────────────────────── */
|
|
|
|
void el_sdl2_widget_destroy(int64_t handle) {
|
|
ElWidget *w = el_sdl2_get(handle);
|
|
if (!w) return;
|
|
|
|
/* Remove from parent */
|
|
if (w->parent >= 0) {
|
|
el_sdl2_widget_remove_child((int64_t)w->parent, handle);
|
|
}
|
|
|
|
/* Recursively destroy children */
|
|
for (int i = 0; i < w->child_count; i++) {
|
|
el_sdl2_widget_destroy((int64_t)w->children[i]);
|
|
}
|
|
|
|
if (g_focused_widget == (int)handle) {
|
|
g_focused_widget = -1;
|
|
SDL_StopTextInput();
|
|
}
|
|
if (g_root_widget == (int)handle) {
|
|
g_root_widget = -1;
|
|
}
|
|
|
|
el_sdl2_free_slot(handle);
|
|
g_dirty = 1;
|
|
}
|
|
|
|
#endif /* EL_TARGET_SDL2 */
|