Files
el/lang/el-compiler/runtime/el_sdl2.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 */