845 lines
30 KiB
C
845 lines
30 KiB
C
/*
|
||
* el_lvgl.c — LVGL v9 backend for the el native widget system.
|
||
*
|
||
* This file implements the microcontroller/embedded widget layer that el_seed.c
|
||
* calls through to when EL_TARGET_LVGL is defined.
|
||
*
|
||
* Architecture:
|
||
* el program (el code)
|
||
* → __widget_* C builtins in el_seed.c
|
||
* → el_lvgl_* C functions defined here
|
||
* → lv_obj_t widgets via LVGL v9
|
||
*
|
||
* Target platforms: ESP32, STM32, industrial panels, any system with 256KB+
|
||
* RAM and an LVGL-compatible display driver. No OS required.
|
||
*
|
||
* Widget handles: every widget is assigned an int64_t slot index into
|
||
* g_widgets[]. The el program holds these as opaque Int values.
|
||
* Slot 0 is reserved; -1 = invalid handle.
|
||
*
|
||
* Window model: on embedded there is one screen. __window_create configures
|
||
* lv_scr_act() as the root container. __window_show is a no-op (the screen
|
||
* is always visible). __native_run_loop calls lv_task_handler() in a tight
|
||
* loop — on RTOS this runs inside a dedicated task; on bare metal it IS the
|
||
* main loop. The host application is responsible for initialising the display
|
||
* driver and calling lv_tick_inc() before calling __native_run_loop.
|
||
*
|
||
* Callback dispatch:
|
||
* When EL_LVGL_NO_DLSYM is NOT defined (hosted Linux, testing):
|
||
* dlsym(RTLD_DEFAULT, fn_name) resolves the El function symbol at runtime.
|
||
* When EL_LVGL_NO_DLSYM IS defined (bare-metal ESP32/STM32):
|
||
* The caller must provide:
|
||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||
* which maps function names to function pointers via a compile-time table.
|
||
*
|
||
* Font mapping: LVGL v9 ships Montserrat in discrete sizes. __widget_set_font
|
||
* maps the requested point size to the nearest available Montserrat variant.
|
||
* Bold is approximated by stepping up two sizes (no separate bold face in the
|
||
* default LVGL font set). Define EL_LVGL_CUSTOM_FONT to override font_select().
|
||
*
|
||
* Compile (hosted test build):
|
||
* gcc -DEL_TARGET_LVGL -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||
* # Then link with lvgl.a / lvgl source tree.
|
||
*
|
||
* Compile (bare-metal, no dynamic linker):
|
||
* arm-none-eabi-gcc -DEL_TARGET_LVGL -DEL_LVGL_NO_DLSYM \
|
||
* -I./lvgl el_lvgl.c -c -o el_lvgl.o
|
||
*/
|
||
|
||
#ifdef EL_TARGET_LVGL
|
||
|
||
#include "lvgl/lvgl.h"
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
|
||
#ifndef EL_LVGL_NO_DLSYM
|
||
#include <dlfcn.h>
|
||
#endif
|
||
|
||
#include "el_runtime.h"
|
||
|
||
/* ── Callback dispatch macro ─────────────────────────────────────────────── */
|
||
|
||
#ifdef EL_LVGL_NO_DLSYM
|
||
/*
|
||
* Bare-metal path. The application provides this function:
|
||
* el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||
* It maps string names → function pointers, typically via a switch on a hash
|
||
* or a sorted table of {name, fn_ptr} pairs generated by elc.
|
||
*/
|
||
extern el_val_t el_lvgl_dispatch(const char *fn, el_val_t a, el_val_t b);
|
||
#define EL_LVGL_CALL(fn_name, a, b) el_lvgl_dispatch((fn_name), (a), (b))
|
||
#else
|
||
/*
|
||
* Hosted path. dlsym resolves the El symbol at call time.
|
||
* We use a compound-statement expression (GCC/Clang extension) to avoid
|
||
* executing dlsym more than once per call.
|
||
*/
|
||
#define EL_LVGL_CALL(fn_name, a, b) \
|
||
({ \
|
||
typedef el_val_t (*_el_fn_t)(el_val_t, el_val_t); \
|
||
_el_fn_t _fn = (_el_fn_t)(uintptr_t)dlsym(RTLD_DEFAULT, (fn_name)); \
|
||
_fn ? _fn((a), (b)) : (el_val_t)0; \
|
||
})
|
||
#endif
|
||
|
||
/* ── Widget table ─────────────────────────────────────────────────────────── */
|
||
|
||
#define EL_LVGL_MAX_WIDGETS 4096
|
||
|
||
/*
|
||
* Widget kinds — mirrors AppKit/GTK4 backends so future tooling can stay
|
||
* consistent across all targets.
|
||
*/
|
||
typedef enum {
|
||
EL_LVGL_FREE = 0,
|
||
EL_LVGL_WINDOW = 1,
|
||
EL_LVGL_VSTACK = 2,
|
||
EL_LVGL_HSTACK = 3,
|
||
EL_LVGL_ZSTACK = 4,
|
||
EL_LVGL_SCROLL = 5,
|
||
EL_LVGL_LABEL = 6,
|
||
EL_LVGL_BUTTON = 7, /* lv_btn_create; inner label at slot_btn_label */
|
||
EL_LVGL_TEXTFIELD = 8, /* lv_textarea, one-line */
|
||
EL_LVGL_TEXTAREA = 9, /* lv_textarea, multiline */
|
||
EL_LVGL_IMAGE = 10,
|
||
EL_LVGL_DIVIDER = 11,
|
||
EL_LVGL_SPACER = 12,
|
||
} ElLvglKind;
|
||
|
||
/*
|
||
* Per-slot state. Callback names are stored inline (256 bytes each) to avoid
|
||
* heap allocation on targets with no malloc or fragmented heaps.
|
||
*/
|
||
typedef struct {
|
||
ElLvglKind kind;
|
||
lv_obj_t *obj; /* primary LVGL object */
|
||
lv_obj_t *btn_label; /* for EL_LVGL_BUTTON: inner lv_label child */
|
||
char cb_click[256];
|
||
char cb_change[256];
|
||
char cb_submit[256];
|
||
} ElLvglWidget;
|
||
|
||
static ElLvglWidget g_widgets[EL_LVGL_MAX_WIDGETS];
|
||
|
||
/* ── Slot helpers ─────────────────────────────────────────────────────────── */
|
||
|
||
static int64_t lvgl_slot_alloc(ElLvglKind kind, lv_obj_t *obj) {
|
||
for (int i = 1; i < EL_LVGL_MAX_WIDGETS; i++) {
|
||
if (g_widgets[i].kind == EL_LVGL_FREE) {
|
||
g_widgets[i].kind = kind;
|
||
g_widgets[i].obj = obj;
|
||
g_widgets[i].btn_label = NULL;
|
||
g_widgets[i].cb_click[0] = '\0';
|
||
g_widgets[i].cb_change[0] = '\0';
|
||
g_widgets[i].cb_submit[0] = '\0';
|
||
return (int64_t)i;
|
||
}
|
||
}
|
||
return -1; /* table full */
|
||
}
|
||
|
||
static ElLvglWidget *lvgl_slot_get(int64_t handle) {
|
||
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return NULL;
|
||
if (g_widgets[handle].kind == EL_LVGL_FREE) return NULL;
|
||
return &g_widgets[handle];
|
||
}
|
||
|
||
static void lvgl_slot_free(int64_t handle) {
|
||
if (handle <= 0 || handle >= EL_LVGL_MAX_WIDGETS) return;
|
||
ElLvglWidget *w = &g_widgets[handle];
|
||
w->kind = EL_LVGL_FREE;
|
||
w->obj = NULL;
|
||
w->btn_label = NULL;
|
||
w->cb_click[0] = '\0';
|
||
w->cb_change[0] = '\0';
|
||
w->cb_submit[0] = '\0';
|
||
}
|
||
|
||
/* ── Font selection ───────────────────────────────────────────────────────── */
|
||
/*
|
||
* LVGL ships Montserrat in the following sizes (subset enabled by lv_conf.h):
|
||
* 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48
|
||
*
|
||
* We map the requested size to the nearest available size. Bold is
|
||
* approximated by stepping up two sizes (no separate bold face in the default
|
||
* font set). Define EL_LVGL_CUSTOM_FONT to replace this function entirely.
|
||
*/
|
||
#ifndef EL_LVGL_CUSTOM_FONT
|
||
|
||
static const lv_font_t *font_select(int size, int bold) {
|
||
/* Step up two sizes for bold approximation. */
|
||
if (bold) size += 4;
|
||
|
||
/* Clamp to available range. */
|
||
if (size < 8) size = 8;
|
||
if (size > 48) size = 48;
|
||
|
||
/* Round to nearest even size >= 8. */
|
||
if (size % 2 != 0) size++;
|
||
|
||
switch (size) {
|
||
#if LV_FONT_MONTSERRAT_8
|
||
case 8: return &lv_font_montserrat_8;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_10
|
||
case 10: return &lv_font_montserrat_10;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_12
|
||
case 12: return &lv_font_montserrat_12;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_14
|
||
case 14: return &lv_font_montserrat_14;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_16
|
||
case 16: return &lv_font_montserrat_16;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_18
|
||
case 18: return &lv_font_montserrat_18;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_20
|
||
case 20: return &lv_font_montserrat_20;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_22
|
||
case 22: return &lv_font_montserrat_22;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_24
|
||
case 24: return &lv_font_montserrat_24;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_26
|
||
case 26: return &lv_font_montserrat_26;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_28
|
||
case 28: return &lv_font_montserrat_28;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_30
|
||
case 30: return &lv_font_montserrat_30;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_32
|
||
case 32: return &lv_font_montserrat_32;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_34
|
||
case 34: return &lv_font_montserrat_34;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_36
|
||
case 36: return &lv_font_montserrat_36;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_38
|
||
case 38: return &lv_font_montserrat_38;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_40
|
||
case 40: return &lv_font_montserrat_40;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_42
|
||
case 42: return &lv_font_montserrat_42;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_44
|
||
case 44: return &lv_font_montserrat_44;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_46
|
||
case 46: return &lv_font_montserrat_46;
|
||
#endif
|
||
#if LV_FONT_MONTSERRAT_48
|
||
case 48: return &lv_font_montserrat_48;
|
||
#endif
|
||
default:
|
||
/*
|
||
* Requested size is not compiled in. Fall back to the default
|
||
* theme font, which is guaranteed to be present.
|
||
*/
|
||
return LV_FONT_DEFAULT;
|
||
}
|
||
}
|
||
|
||
#endif /* EL_LVGL_CUSTOM_FONT */
|
||
|
||
/* ── Event callback ───────────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* Single LVGL event callback used for all widget events. The user_data is
|
||
* the slot index cast to (void*) via intptr_t — avoids heap allocation.
|
||
*
|
||
* Three event codes are handled:
|
||
* LV_EVENT_CLICKED → cb_click (buttons, any tappable widget)
|
||
* LV_EVENT_VALUE_CHANGED → cb_change (textarea, checkbox, etc.)
|
||
* LV_EVENT_READY → cb_submit (Enter pressed in textarea one-line mode)
|
||
*/
|
||
static void el_lvgl_event_cb(lv_event_t *e) {
|
||
lv_event_code_t code = lv_event_get_code(e);
|
||
intptr_t slot = (intptr_t)lv_event_get_user_data(e);
|
||
ElLvglWidget *w = lvgl_slot_get((int64_t)slot);
|
||
if (!w) return;
|
||
|
||
if (code == LV_EVENT_CLICKED && w->cb_click[0]) {
|
||
EL_LVGL_CALL(w->cb_click, (el_val_t)slot, (el_val_t)0);
|
||
}
|
||
|
||
if (code == LV_EVENT_VALUE_CHANGED && w->cb_change[0]) {
|
||
/*
|
||
* Retrieve current text for textarea/textfield widgets so the handler
|
||
* receives the updated value as its second argument.
|
||
*/
|
||
const char *txt = "";
|
||
lv_obj_t *target = lv_event_get_target(e);
|
||
if (w->kind == EL_LVGL_TEXTFIELD || w->kind == EL_LVGL_TEXTAREA) {
|
||
txt = lv_textarea_get_text(target);
|
||
if (!txt) txt = "";
|
||
}
|
||
EL_LVGL_CALL(w->cb_change, (el_val_t)slot,
|
||
(el_val_t)(uintptr_t)txt);
|
||
}
|
||
|
||
if (code == LV_EVENT_READY && w->cb_submit[0]) {
|
||
/* LV_EVENT_READY fires when Enter is pressed in a one-line textarea. */
|
||
EL_LVGL_CALL(w->cb_submit, (el_val_t)slot, (el_val_t)0);
|
||
}
|
||
}
|
||
|
||
/* ── Initialisation ───────────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* el_lvgl_init — call lv_init(). The host must have already initialised the
|
||
* display driver and input driver before this, or immediately after. Idempotent.
|
||
*/
|
||
void el_lvgl_init(void) {
|
||
static int done = 0;
|
||
if (done) return;
|
||
done = 1;
|
||
lv_init();
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_run_loop — drive lv_task_handler() indefinitely.
|
||
*
|
||
* On RTOS: this function should run inside a dedicated FreeRTOS/Zephyr task.
|
||
* On bare metal: call this as the last statement of main().
|
||
*
|
||
* The 5 ms delay between handler calls matches the LVGL documentation
|
||
* recommendation for a ~200 Hz refresh budget.
|
||
*
|
||
* On hosted Linux (EL_LVGL_SDL or similar), usleep(5000) is used. On RTOS
|
||
* targets define EL_LVGL_RTOS_DELAY(ms) to map to vTaskDelay/k_sleep/etc.
|
||
*/
|
||
void el_lvgl_run_loop(void) {
|
||
for (;;) {
|
||
lv_task_handler();
|
||
#if defined(EL_LVGL_RTOS_DELAY)
|
||
EL_LVGL_RTOS_DELAY(5);
|
||
#elif defined(__linux__) || defined(__APPLE__)
|
||
{
|
||
/* Hosted test build — usleep available. */
|
||
#include <unistd.h>
|
||
usleep(5000);
|
||
}
|
||
#endif
|
||
/* Bare-metal without a delay macro: the HAL tick increment loop
|
||
* is the caller's responsibility. No sleep needed if lv_tick_inc()
|
||
* is driven from a hardware timer ISR. */
|
||
}
|
||
}
|
||
|
||
/* ── Window ───────────────────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* el_lvgl_window_create — configure lv_scr_act() as a vertical flex container
|
||
* and return a slot handle wrapping it. The title is stored for informational
|
||
* purposes (e.g., a status bar widget the host might create). Width/height
|
||
* are ignored on embedded targets because the screen size is fixed by the
|
||
* display driver; they are accepted for API compatibility with other backends.
|
||
*/
|
||
int64_t el_lvgl_window_create(const char *title, int width, int height,
|
||
int min_width, int min_height) {
|
||
(void)width; (void)height; (void)min_width; (void)min_height;
|
||
|
||
lv_obj_t *scr = lv_scr_act();
|
||
|
||
/* Configure the active screen as a vertical flex container so that
|
||
* widgets added via __widget_add_child stack naturally. */
|
||
lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN);
|
||
lv_obj_set_size(scr, LV_PCT(100), LV_PCT(100));
|
||
|
||
/* Store the window title in a user-data string on the screen object
|
||
* so host code can retrieve it if it wants to render a title bar. */
|
||
if (title && *title) {
|
||
/* lv_obj_set_user_data stores a void* — we cast the string pointer.
|
||
* The string must outlive the screen object; for literals this is
|
||
* always true. For dynamic titles use el_lvgl_window_set_title. */
|
||
lv_obj_set_user_data(scr, (void *)(uintptr_t)title);
|
||
}
|
||
|
||
/* Allocate a slot for the screen object. */
|
||
int64_t h = lvgl_slot_alloc(EL_LVGL_WINDOW, scr);
|
||
return h;
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_window_show — no-op on embedded. The screen is always visible.
|
||
*/
|
||
void el_lvgl_window_show(int64_t handle) {
|
||
(void)handle;
|
||
/* On multi-screen setups, load the screen: */
|
||
/* ElLvglWidget *w = lvgl_slot_get(handle);
|
||
* if (w) lv_scr_load(w->obj); */
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_window_set_title — update the user_data pointer on the screen object.
|
||
* On embedded, "title" is typically only used by a custom host title bar.
|
||
*/
|
||
void el_lvgl_window_set_title(int64_t handle, const char *title) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w || w->kind != EL_LVGL_WINDOW) return;
|
||
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)(title ? title : ""));
|
||
}
|
||
|
||
/* ── Layout containers ────────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* el_lvgl_vstack_create — vertical flex column with inter-item gap = spacing.
|
||
*/
|
||
int64_t el_lvgl_vstack_create(int spacing) {
|
||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||
lv_obj_set_style_pad_row(obj, (lv_coord_t)spacing, 0);
|
||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||
/* Remove default LVGL border and background so containers are transparent
|
||
* by default, matching the AppKit/GTK4 backends. */
|
||
lv_obj_set_style_border_width(obj, 0, 0);
|
||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||
return lvgl_slot_alloc(EL_LVGL_VSTACK, obj);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_hstack_create — horizontal flex row with inter-item gap = spacing.
|
||
*/
|
||
int64_t el_lvgl_hstack_create(int spacing) {
|
||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||
lv_obj_set_style_pad_column(obj, (lv_coord_t)spacing, 0);
|
||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||
lv_obj_set_style_border_width(obj, 0, 0);
|
||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||
return lvgl_slot_alloc(EL_LVGL_HSTACK, obj);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_zstack_create — plain container, children positioned absolutely.
|
||
* No flex flow is set; callers use lv_obj_set_pos() on children directly,
|
||
* or rely on their natural 0,0 origin.
|
||
*/
|
||
int64_t el_lvgl_zstack_create(void) {
|
||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||
lv_obj_set_style_border_width(obj, 0, 0);
|
||
lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0);
|
||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||
return lvgl_slot_alloc(EL_LVGL_ZSTACK, obj);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_scroll_create — vertically scrollable container.
|
||
*/
|
||
int64_t el_lvgl_scroll_create(void) {
|
||
lv_obj_t *obj = lv_obj_create(lv_scr_act());
|
||
lv_obj_set_scroll_dir(obj, LV_DIR_VER);
|
||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
|
||
lv_obj_set_style_border_width(obj, 0, 0);
|
||
lv_obj_set_style_pad_all(obj, 0, 0);
|
||
return lvgl_slot_alloc(EL_LVGL_SCROLL, obj);
|
||
}
|
||
|
||
/* ── Widget factories ─────────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* el_lvgl_label_create — static text label.
|
||
*/
|
||
int64_t el_lvgl_label_create(const char *text) {
|
||
lv_obj_t *obj = lv_label_create(lv_scr_act());
|
||
lv_label_set_text(obj, text ? text : "");
|
||
return lvgl_slot_alloc(EL_LVGL_LABEL, obj);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_button_create — pressable button with a child label.
|
||
*
|
||
* LVGL buttons are containers; text is placed in an inner lv_label child.
|
||
* We store the child label pointer in btn_label so set_text / get_text can
|
||
* reach it without searching the object tree at runtime.
|
||
*/
|
||
int64_t el_lvgl_button_create(const char *label) {
|
||
lv_obj_t *btn = lv_btn_create(lv_scr_act());
|
||
lv_obj_t *lbl = lv_label_create(btn);
|
||
lv_label_set_text(lbl, label ? label : "");
|
||
lv_obj_center(lbl);
|
||
|
||
int64_t h = lvgl_slot_alloc(EL_LVGL_BUTTON, btn);
|
||
if (h >= 0) {
|
||
g_widgets[h].btn_label = lbl;
|
||
/* Register click callback immediately so button responds when a
|
||
* callback name is registered later via __widget_on_click. */
|
||
lv_obj_add_event_cb(btn, el_lvgl_event_cb, LV_EVENT_CLICKED,
|
||
(void *)(intptr_t)h);
|
||
}
|
||
return h;
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_text_field_create — single-line text input.
|
||
*/
|
||
int64_t el_lvgl_text_field_create(const char *placeholder) {
|
||
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
|
||
lv_textarea_set_one_line(obj, true);
|
||
if (placeholder && *placeholder) {
|
||
lv_textarea_set_placeholder_text(obj, placeholder);
|
||
}
|
||
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTFIELD, obj);
|
||
if (h >= 0) {
|
||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||
(void *)(intptr_t)h);
|
||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_READY,
|
||
(void *)(intptr_t)h);
|
||
}
|
||
return h;
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_text_area_create — multi-line text input.
|
||
*/
|
||
int64_t el_lvgl_text_area_create(const char *placeholder) {
|
||
lv_obj_t *obj = lv_textarea_create(lv_scr_act());
|
||
lv_textarea_set_one_line(obj, false);
|
||
if (placeholder && *placeholder) {
|
||
lv_textarea_set_placeholder_text(obj, placeholder);
|
||
}
|
||
int64_t h = lvgl_slot_alloc(EL_LVGL_TEXTAREA, obj);
|
||
if (h >= 0) {
|
||
lv_obj_add_event_cb(obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||
(void *)(intptr_t)h);
|
||
}
|
||
return h;
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_image_create — image widget.
|
||
*
|
||
* On hosted Linux (SDL backend), path is a filesystem path.
|
||
* On embedded with SPIFFS/LittleFS, path is a SPIFFS URI: "S:/image.bin".
|
||
* LVGL image decoders are registered separately by the host application.
|
||
*/
|
||
int64_t el_lvgl_image_create(const char *path_or_name) {
|
||
lv_obj_t *obj = lv_img_create(lv_scr_act());
|
||
if (path_or_name && *path_or_name) {
|
||
lv_img_set_src(obj, path_or_name);
|
||
}
|
||
return lvgl_slot_alloc(EL_LVGL_IMAGE, obj);
|
||
}
|
||
|
||
/* ── Widget property setters ─────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* el_lvgl_widget_set_text — update visible text.
|
||
*
|
||
* Dispatch per kind:
|
||
* LABEL → lv_label_set_text
|
||
* BUTTON → lv_label_set_text on inner btn_label child
|
||
* TEXTFIELD / TEXTAREA → lv_textarea_set_text
|
||
* WINDOW → lv_obj_set_user_data (stores title string)
|
||
*/
|
||
void el_lvgl_widget_set_text(int64_t handle, const char *text) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
const char *t = text ? text : "";
|
||
switch (w->kind) {
|
||
case EL_LVGL_LABEL:
|
||
lv_label_set_text(w->obj, t);
|
||
break;
|
||
case EL_LVGL_BUTTON:
|
||
if (w->btn_label) lv_label_set_text(w->btn_label, t);
|
||
break;
|
||
case EL_LVGL_TEXTFIELD:
|
||
case EL_LVGL_TEXTAREA:
|
||
lv_textarea_set_text(w->obj, t);
|
||
break;
|
||
case EL_LVGL_WINDOW:
|
||
lv_obj_set_user_data(w->obj, (void *)(uintptr_t)t);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_get_text — retrieve visible text.
|
||
*
|
||
* Returns a pointer into LVGL's internal storage — valid until the next LVGL
|
||
* operation that modifies the widget. Callers that need to hold the value
|
||
* across LVGL calls must strdup() it.
|
||
*/
|
||
const char *el_lvgl_widget_get_text(int64_t handle) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return "";
|
||
switch (w->kind) {
|
||
case EL_LVGL_LABEL:
|
||
return lv_label_get_text(w->obj);
|
||
case EL_LVGL_BUTTON:
|
||
return w->btn_label ? lv_label_get_text(w->btn_label) : "";
|
||
case EL_LVGL_TEXTFIELD:
|
||
case EL_LVGL_TEXTAREA:
|
||
return lv_textarea_get_text(w->obj);
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_color — foreground (text) colour.
|
||
*
|
||
* r/g/b are 0.0–1.0 floats bit-cast as el_val_t (see el_runtime.h).
|
||
* LVGL lv_color_make takes uint8_t 0–255 components.
|
||
*/
|
||
void el_lvgl_widget_set_color(int64_t handle,
|
||
float r, float g, float b, float a) {
|
||
(void)a; /* LVGL text colour has no per-glyph alpha channel */
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_color_t c = lv_color_make(
|
||
(uint8_t)(r * 255.0f + 0.5f),
|
||
(uint8_t)(g * 255.0f + 0.5f),
|
||
(uint8_t)(b * 255.0f + 0.5f));
|
||
lv_obj_set_style_text_color(w->obj, c, 0);
|
||
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
|
||
lv_obj_set_style_text_color(w->btn_label, c, 0);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_bg_color — background fill colour + opacity.
|
||
*/
|
||
void el_lvgl_widget_set_bg_color(int64_t handle,
|
||
float r, float g, float b, float a) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_color_t c = lv_color_make(
|
||
(uint8_t)(r * 255.0f + 0.5f),
|
||
(uint8_t)(g * 255.0f + 0.5f),
|
||
(uint8_t)(b * 255.0f + 0.5f));
|
||
lv_opa_t opa = (lv_opa_t)(a * 255.0f + 0.5f);
|
||
lv_obj_set_style_bg_color(w->obj, c, 0);
|
||
lv_obj_set_style_bg_opa(w->obj, opa, 0);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_font — apply font to text-bearing widget.
|
||
*
|
||
* The `family` parameter is accepted for API compatibility but LVGL uses
|
||
* compiled-in fonts only. Only the size and bold flag have effect unless
|
||
* EL_LVGL_CUSTOM_FONT is defined by the host.
|
||
*/
|
||
void el_lvgl_widget_set_font(int64_t handle,
|
||
const char *family, int size, int bold) {
|
||
(void)family; /* ignored; LVGL uses compiled-in Montserrat fonts */
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
const lv_font_t *font = font_select(size, bold);
|
||
if (!font) return;
|
||
lv_obj_set_style_text_font(w->obj, font, 0);
|
||
if (w->kind == EL_LVGL_BUTTON && w->btn_label) {
|
||
lv_obj_set_style_text_font(w->btn_label, font, 0);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_padding — set per-side padding (top/right/bottom/left).
|
||
*/
|
||
void el_lvgl_widget_set_padding(int64_t handle,
|
||
int top, int right, int bottom, int left) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_obj_set_style_pad_top(w->obj, (lv_coord_t)top, 0);
|
||
lv_obj_set_style_pad_right(w->obj, (lv_coord_t)right, 0);
|
||
lv_obj_set_style_pad_bottom(w->obj, (lv_coord_t)bottom, 0);
|
||
lv_obj_set_style_pad_left(w->obj, (lv_coord_t)left, 0);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_width — set explicit pixel width.
|
||
*/
|
||
void el_lvgl_widget_set_width(int64_t handle, int width) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_obj_set_width(w->obj, (lv_coord_t)width);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_height — set explicit pixel height.
|
||
*/
|
||
void el_lvgl_widget_set_height(int64_t handle, int height) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_obj_set_height(w->obj, (lv_coord_t)height);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_flex — set flex grow factor.
|
||
*
|
||
* flex > 0 → lv_obj_set_flex_grow(obj, flex): object expands to fill
|
||
* remaining space proportional to its grow factor.
|
||
* flex == 0 → lv_obj_set_flex_grow(obj, 0): object uses natural size.
|
||
*/
|
||
void el_lvgl_widget_set_flex(int64_t handle, int flex) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_obj_set_flex_grow(w->obj, (uint8_t)(flex > 0 ? flex : 0));
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_corner_radius — set border radius.
|
||
*/
|
||
void el_lvgl_widget_set_corner_radius(int64_t handle, int radius) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_obj_set_style_radius(w->obj, (lv_coord_t)radius, 0);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_disabled — enable/disable interactive state.
|
||
*
|
||
* LV_STATE_DISABLED greys out the widget and prevents input events.
|
||
*/
|
||
void el_lvgl_widget_set_disabled(int64_t handle, int disabled) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
if (disabled) {
|
||
lv_obj_add_state(w->obj, LV_STATE_DISABLED);
|
||
} else {
|
||
lv_obj_clear_state(w->obj, LV_STATE_DISABLED);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_set_hidden — show/hide widget.
|
||
*
|
||
* LV_OBJ_FLAG_HIDDEN hides the widget and removes it from layout flow.
|
||
*/
|
||
void el_lvgl_widget_set_hidden(int64_t handle, int hidden) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
if (hidden) {
|
||
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
|
||
} else {
|
||
lv_obj_clear_flag(w->obj, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
}
|
||
|
||
/* ── Tree operations ──────────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* el_lvgl_widget_add_child — attach child widget to parent.
|
||
*
|
||
* lv_obj_set_parent() reparents the child object inside the LVGL tree.
|
||
* For WINDOW parents we use the screen object itself as the parent, since
|
||
* lv_scr_act() IS the root container.
|
||
*/
|
||
void el_lvgl_widget_add_child(int64_t parent, int64_t child) {
|
||
ElLvglWidget *pw = lvgl_slot_get(parent);
|
||
ElLvglWidget *cw = lvgl_slot_get(child);
|
||
if (!pw || !cw) return;
|
||
lv_obj_set_parent(cw->obj, pw->obj);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_remove_child — detach child from its current parent.
|
||
*
|
||
* LVGL has no explicit "remove from parent without deleting" operation.
|
||
* We reparent the child back to the active screen (making it a root-level
|
||
* floating widget) and then hide it. The widget still occupies a slot and
|
||
* can be re-attached or destroyed later.
|
||
*/
|
||
void el_lvgl_widget_remove_child(int64_t parent, int64_t child) {
|
||
(void)parent;
|
||
ElLvglWidget *cw = lvgl_slot_get(child);
|
||
if (!cw) return;
|
||
/* Move to screen root and hide. */
|
||
lv_obj_set_parent(cw->obj, lv_scr_act());
|
||
lv_obj_add_flag(cw->obj, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
|
||
/*
|
||
* el_lvgl_widget_destroy — delete widget and its children from the LVGL tree,
|
||
* then free the slot.
|
||
*
|
||
* lv_obj_del() recursively deletes the object and all children. After this
|
||
* call the handle is invalid and must not be used.
|
||
*/
|
||
void el_lvgl_widget_destroy(int64_t handle) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
lv_obj_del(w->obj);
|
||
lvgl_slot_free(handle);
|
||
}
|
||
|
||
/* ── Event registration ───────────────────────────────────────────────────── */
|
||
|
||
/*
|
||
* Event registration stores the El function name in the widget slot. The
|
||
* actual lv_obj_add_event_cb() call is made here (or was made in the factory
|
||
* for buttons/textfields where we know the relevant event codes upfront).
|
||
*
|
||
* For widgets that did not register their event callback in the factory (e.g.
|
||
* labels receiving a click handler), we add the LVGL event binding now.
|
||
*/
|
||
|
||
void el_lvgl_widget_on_click(int64_t handle, const char *fn_name) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
strncpy(w->cb_click, fn_name ? fn_name : "", 255);
|
||
w->cb_click[255] = '\0';
|
||
/*
|
||
* Buttons already have LV_EVENT_CLICKED registered in the factory.
|
||
* For other widget kinds (labels, containers used as tap targets), add
|
||
* the click flag and register the callback.
|
||
*/
|
||
if (w->kind != EL_LVGL_BUTTON) {
|
||
lv_obj_add_flag(w->obj, LV_OBJ_FLAG_CLICKABLE);
|
||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_CLICKED,
|
||
(void *)(intptr_t)handle);
|
||
}
|
||
}
|
||
|
||
void el_lvgl_widget_on_change(int64_t handle, const char *fn_name) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
strncpy(w->cb_change, fn_name ? fn_name : "", 255);
|
||
w->cb_change[255] = '\0';
|
||
/*
|
||
* Textfield/textarea factories already register VALUE_CHANGED.
|
||
* For other kinds (e.g. a custom toggle), add the binding now.
|
||
*/
|
||
if (w->kind != EL_LVGL_TEXTFIELD && w->kind != EL_LVGL_TEXTAREA) {
|
||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_VALUE_CHANGED,
|
||
(void *)(intptr_t)handle);
|
||
}
|
||
}
|
||
|
||
void el_lvgl_widget_on_submit(int64_t handle, const char *fn_name) {
|
||
ElLvglWidget *w = lvgl_slot_get(handle);
|
||
if (!w) return;
|
||
strncpy(w->cb_submit, fn_name ? fn_name : "", 255);
|
||
w->cb_submit[255] = '\0';
|
||
/*
|
||
* LV_EVENT_READY fires when Enter is pressed in a one-line textarea.
|
||
* Textfield factories already register READY. For other kinds, add it.
|
||
*/
|
||
if (w->kind != EL_LVGL_TEXTFIELD) {
|
||
lv_obj_add_event_cb(w->obj, el_lvgl_event_cb, LV_EVENT_READY,
|
||
(void *)(intptr_t)handle);
|
||
}
|
||
}
|
||
|
||
#endif /* EL_TARGET_LVGL */
|