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