In the thread about the wishlist for UI, @JazzPlayer mentioned LVGL. I wasn't aware about this library so I went to LVGL.io and it catched my attention.
It can work embedded (for example with ESP32), but you can also make it work in Linux and even on the web as shown here. For the ESP32, we also have nesper which I'd love to try in the future.
So I went on experimenting with LVGL in Linux. I created some bindings with nimterop and I managed to show a first screen:
ISSUE 1
I am trying to get the button example working. My issue is getting the callback just being called back. My code is here: ex03_button.nim.
Line 41 sets the callback. When I compile and run the file, I get 17 times the word pressed (without me pressing the button). I get the expected screen:
but when I press the button, nothing occurs (I would expect at least pressed being displayed due to line 23).
Am I missing something to get the callback being called back?
ISSUE 2
Besides, in order to get the full example working, they use this line that I tried to implement in Nim with a closure. But when I do that, I get into troubles with the function signature expected by lv_obj_add_event. It expects the signature of the callback to be lv_event_cb_t:
lv_event_cb_t* {.importc, implvglHdr.} = proc (e: ptr lv_event_t) {.cdecl.}
I tried a closure like this:
let btn_event_cb = block:
var cnt = 0
(proc (e:ptr lv_event_t) {.cdecl.} =
echo now().utc, " callback"
let code = lv_event_get_code(e)
let btn = cast[ptr lv_obj_t](lv_event_get_target(e))
if code == LV_EVENT_CLICKED:
echo now().utc, " clicked"
cnt += 1
# Get the first child of the button which is the label and change its text*/
let label = lv_obj_get_child(btn, 0)
lv_label_set_text_fmt(label, "Button: %d", cnt) )
It behaves as above (not being called when the button is pressed).
I also tried this format (more familiar to me):
proc gen():proc =
var cnt = 0
return proc (e:ptr lv_event_t) {.cdecl.} =
echo now().utc, " callback"
let code = lv_event_get_code(e)
let btn = cast[ptr lv_obj_t](lv_event_get_target(e))
if code == LV_EVENT_CLICKED:
echo now().utc, " clicked"
cnt += 1
# Get the first child of the button which is the label and change its text*/
let label = lv_obj_get_child(btn, 0)
lv_label_set_text_fmt(label, "Button: %d", cnt)
but I get: Error: illegal capture 'cnt' because ':anonymous' has the calling convention: <cdecl>.
What would be the right approach in this context?
Well C doesn't have closures so .cdecl cannot support it either. Usually a void* env parameter is used in C so that you can pass user data to callbacks. But in this library's event.h it's declared as:
typedef void (*lv_event_cb_t)(lv_event_t * e);
/**********************
* GLOBAL PROTOTYPES
**********************/
void _lv_event_push(lv_event_t * e);
void _lv_event_pop(lv_event_t * e);
lv_res_t lv_event_send(lv_event_list_t * list, lv_event_t * e, bool preprocess);
void lv_event_add(lv_event_list_t * list, lv_event_cb_t cb, lv_event_code_t filter, void * user_data);
Even though lv_event_add has this void* user_data it is absent in the declaration of lv_event_cb_t. You should aks on the project's issue tracker of why that is, looks like a big design mistake to me but what do I know.
There's a pragma named global which might be useful.
As for
I also tried this format (more familiar to me):
You cannot use it, because a {.cdecl.} can't be a {.closure.}. They conflict not only at high-level (each proc can have exactly one calling conversion) but also at low-level (cdecl implemented as a C function or C function pointer, and closure as an object/struct). That is, marking a proc as cdecl makes it lose the ability of catching local variables
I don't know where your other programs went wrong, but I think it has something to do with types and pointers (which is usually the case.)
I finally used for the example:
let btn_event_cb = block:
var cnt = 0
(proc (e:ptr lv_event_t) {.cdecl.} =
let code = lv_event_get_code(e)
let btn = cast[ptr lv_obj_t](lv_event_get_target(e))
if code == LV_EVENT_CLICKED:
echo now().utc, " clicked"
cnt += 1
# Get the first child of the button which is the label and change its text*/
let label = lv_obj_get_child(btn, 0)
lv_label_set_text_fmt(label, "Button: %d", cnt) )
which works. It seems to store cnt globally, but I don't know well how to interpret the C generated code:
...
N_LIB_PRIVATE NI cnt__ex485195button_4 = ((NI) 0);
...
I have to try to create that variable as user data. In this thread they suggest using lv_obj_set_user_data.
The issue was the loop at the bottom:
while true:
## Periodically call the lv_task handler.
## It could be done in a timer interrupt or an OS task too.
discard lv_timer_handler()
# sleep(5 * 1000) #<--- this was too big
sleep(5 * 10)
I understand that the library was unable to catch the clicks due to this. I created some bindings with nimterop
I'm to lazy to make a proper PR, but to make the examples compile out-of-the box on any system but yours I suggest the following changes:
diff --git a/src/lvgl.nim b/src/lvgl.nim
index 8d0e392..c8ee0b4 100644
--- a/src/lvgl.nim
+++ b/src/lvgl.nim
@@ -9,7 +9,8 @@ const wayland = gorge("pkg-config --libs wayland-client")
const xkbcommon = gorge("pkg-config --libs xkbcommon")
{.passL:"-lSDL2 -lm " & wayland & " " & xkbcommon & " -lpthread".}
-import wrapper/[lvgl,compiles]
+include wrapper/compiles
+import wrapper/lvgl
export lvgl
diff --git a/src/wrapper/lvgl.nim b/src/wrapper/lvgl.nim
index 403a531..08d2ae9 100644
--- a/src/wrapper/lvgl.nim
+++ b/src/wrapper/lvgl.nim
@@ -34,7 +34,7 @@
# const 'LV_IMG_ZOOM_NONE' has unsupported value 'LV_ZOOM_NONE'
# const 'LV_COLOR_SIZE' has unsupported value 'LV_COLOR_DEPTH'
{.push hint[ConvFromXtoItselfNotNeeded]: off.}
-import macros
+import macros, os
macro defineEnum(typ: untyped): untyped =
result = newNimNode(nnkStmtList)
@@ -79,11 +79,11 @@ macro defineEnum(typ: untyped): untyped =
type va_list* {.importc, header:"<stdarg.h>".} = object
+const srcDir = currentSourcePath.parentDir().parentDir()
-{.pragma: implvglHdr,
- header: "/home/jose/src/nimlang/lvgl.nim/src/components/lvgl/lvgl.h".}
+{.pragma: implvglHdr, header: srcDir & "/components/lvgl/lvgl.h".}
{.experimental: "codeReordering".}
-{.passC: "-I../components/lvgl".}
+{.passC: "-I" & srcDir & "/components/lvgl".}
defineEnum(lv_res_t) ## ```
## *******************
## TYPEDEFS
proc btn_clicked_cb(e: ptr lv_event_t) {.cdecl.} =
let btn = cast[ptr lv_obj_t](lv_event_get_target(e))
var cnt = cast[ptr int](lv_event_get_user_data(e))
echo now().utc, " clicked"
cnt[] += 1
let label = lv_obj_get_child(btn, 0)
lv_label_set_text_fmt(label, "Button: %d", cnt[])
Add it with:
var cnt = create int
btn.lv_obj_add_event(btn_clicked_cb, LV_EVENT_CLICKED, cnt)
I think you can just do:
proc btn_event_cb(e:ptr lv_event_t) {.cdecl.} =
var cnt {.global.}: int = 0
let code = lv_event_get_code(e)
let btn = cast[ptr lv_obj_t](lv_event_get_target(e))
if code == LV_EVENT_CLICKED:
echo now().utc, " clicked"
cnt += 1
let label = lv_obj_get_child(btn, 0)
lv_label_set_text_fmt(label, "Button: %d", cnt)
This is similar to your first implementation.