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