From 4297c139a16614b373aba5ca602706095a74b8c5 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 5 Jun 2023 16:39:40 +0200 Subject: [PATCH] ngui: port some LVGL-based UI code from C to zig this commit includes lots of new code in ui/lvgl.zig and ui/widget.zig to work with LVGL directly from zig. in fact, some C code is moved from ui/c/ui.c to ui/xxx.zig. in addition, a new module ui/widget.zig is where all custom UI elements will reside. at the moment it comprises of the ported topdrop and a new modal func. the latter is an alternative to LVGL's lv_msgbox popup. as a practical example, the commit replaces the power off confirmation popup with the new modal window. --- .clang-format | 1 + src/ngui.zig | 48 ++- src/ui/c/ui.c | 73 ++--- src/ui/drv.zig | 6 +- src/ui/lvgl.zig | 805 ++++++++++++++++++++++++++++++++++++++++++++-- src/ui/screen.zig | 6 +- src/ui/symbol.zig | 3 +- src/ui/ui.zig | 48 +-- src/ui/widget.zig | 101 ++++++ 9 files changed, 963 insertions(+), 128 deletions(-) create mode 100644 src/ui/widget.zig diff --git a/.clang-format b/.clang-format index 1516fdf..416d7bb 100644 --- a/.clang-format +++ b/.clang-format @@ -12,3 +12,4 @@ BinPackArguments: false BinPackParameters: false AllowAllArgumentsOnNextLine: true AllowAllParametersOfDeclarationOnNextLine: true +AllowShortFunctionsOnASingleLine: Empty diff --git a/src/ngui.zig b/src/ngui.zig index 5b19a7b..0ae7bf2 100644 --- a/src/ngui.zig +++ b/src/ngui.zig @@ -1,7 +1,5 @@ const std = @import("std"); -const mem = std.mem; const time = std.time; -const Thread = std.Thread; const comm = @import("comm.zig"); const types = @import("types.zig"); @@ -24,11 +22,11 @@ extern "c" fn ui_update_network_status(text: [*:0]const u8, wifi_list: ?[*:0]con /// global heap allocator used throughout the gui program. /// TODO: thread-safety? -var gpa: mem.Allocator = undefined; +var gpa: std.mem.Allocator = undefined; /// the mutex must be held before any call reaching into lv_xxx functions. /// all nm_xxx functions assume it is the case since they are invoked from lvgl c code. -var ui_mutex: Thread.Mutex = .{}; +var ui_mutex: std.Thread.Mutex = .{}; /// the program runs until quit is true. var quit: bool = false; var state: enum { @@ -43,7 +41,7 @@ var state: enum { /// user attention. /// safe for concurrent use except wakeup.reset() is UB during another thread /// wakeup.wait()'ing or timedWait'ing. -var wakeup = Thread.ResetEvent{}; +var wakeup = std.Thread.ResetEvent{}; /// a monotonic clock for reporting elapsed ticks to LVGL. /// the timer runs throughout the whole duration of the UI program. @@ -62,14 +60,13 @@ export fn nm_get_curr_tick() u32 { export fn nm_check_idle_time(_: *lvgl.LvTimer) void { const standby_idle_ms = 60000; // 60sec - const idle_ms = lvgl.lv_disp_get_inactive_time(null); - logger.debug("idle: {d}", .{idle_ms}); + const idle_ms = lvgl.idleTime(); if (idle_ms > standby_idle_ms and state != .alert) { state = .standby; } } -/// initiate system shutdown. +/// initiate system shutdown leading to power off. export fn nm_sys_shutdown() void { logger.info("initiating system shutdown", .{}); const msg = comm.Message.poweroff; @@ -84,7 +81,7 @@ export fn nm_tab_settings_active() void { } export fn nm_request_network_status(t: *lvgl.LvTimer) void { - lvgl.lv_timer_del(t); + t.destroy(); const msg: comm.Message = .{ .get_network_report = .{ .scan = false } }; comm.write(gpa, stdout, msg) catch |err| logger.err("nm_request_network_status: {any}", .{err}); } @@ -92,8 +89,8 @@ export fn nm_request_network_status(t: *lvgl.LvTimer) void { /// ssid and password args must not outlive this function. export fn nm_wifi_start_connect(ssid: [*:0]const u8, password: [*:0]const u8) void { const msg = comm.Message{ .wifi_connect = .{ - .ssid = mem.span(ssid), - .password = mem.span(password), + .ssid = std.mem.span(ssid), + .password = std.mem.span(password), } }; logger.info("connect to wifi [{s}]", .{msg.wifi_connect.ssid}); comm.write(gpa, stdout, msg) catch |err| { @@ -108,7 +105,7 @@ fn updateNetworkStatus(report: comm.Message.NetworkReport) !void { var wifi_list: ?[:0]const u8 = null; var wifi_list_ptr: ?[*:0]const u8 = null; if (report.wifi_scan_networks.len > 0) { - wifi_list = try mem.joinZ(gpa, "\n", report.wifi_scan_networks); + wifi_list = try std.mem.joinZ(gpa, "\n", report.wifi_scan_networks); wifi_list_ptr = wifi_list.?.ptr; } defer if (wifi_list) |v| gpa.free(v); @@ -124,7 +121,7 @@ fn updateNetworkStatus(report: comm.Message.NetworkReport) !void { } if (report.ipaddrs.len > 0) { - const ipaddrs = try mem.join(gpa, "\n", report.ipaddrs); + const ipaddrs = try std.mem.join(gpa, "\n", report.ipaddrs); defer gpa.free(ipaddrs); try w.print("\n\nIP addresses:\n{s}", .{ipaddrs}); } @@ -137,10 +134,10 @@ fn updateNetworkStatus(report: comm.Message.NetworkReport) !void { // can happen with a fresh connection while dhcp is still in progress. if (report.wifi_ssid != null and report.ipaddrs.len == 0) { // TODO: sometimes this is too fast, not all ip addrs are avail (ipv4 vs ipv6) - if (lvgl.lv_timer_create(nm_request_network_status, 1000, null)) |t| { - lvgl.lv_timer_set_repeat_count(t, 1); - } else { - logger.err("lv_timer_create network status failed: OOM?", .{}); + if (lvgl.createTimer(nm_request_network_status, 1000, null)) |t| { + t.setRepeatCount(1); + } else |err| { + logger.err("network status timer failed: {any}", .{err}); } } } @@ -161,7 +158,8 @@ fn commThread() void { fn commThreadLoopCycle() !void { const msg = comm.read(gpa, stdin) catch |err| { if (err == error.EndOfStream) { - // pointless to continue running if comms is broken + // pointless to continue running if comms is broken. + // a parent/supervisor is expected to restart ngui. ui_mutex.lock(); quit = true; ui_mutex.unlock(); @@ -199,21 +197,19 @@ pub fn main() anyerror!void { }; // start comms with daemon in a seaparate thread. - const th = try Thread.spawn(.{}, commThread, .{}); + const th = try std.Thread.spawn(.{}, commThread, .{}); th.detach(); // run idle timer indefinitely - if (lvgl.lv_timer_create(nm_check_idle_time, 2000, null)) |t| { - lvgl.lv_timer_set_repeat_count(t, -1); - } else { - logger.err("lv_timer_create idle failed: OOM?", .{}); - } + _ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| { + logger.err("idle timer: lvgl.CreateTimer failed: {any}", .{err}); + }; // main UI thread; must never block unless in idle/sleep mode // TODO: handle sigterm while (true) { ui_mutex.lock(); - var till_next_ms = lvgl.lv_timer_handler(); + var till_next_ms = lvgl.loopCycle(); const do_quit = quit; const do_state = state; ui_mutex.unlock(); @@ -235,7 +231,7 @@ pub fn main() anyerror!void { comm.write(gpa, stdout, comm.Message.wakeup) catch |err| { logger.err("comm.write wakeup: {any}", .{err}); }; - lvgl.lv_disp_trig_activity(null); + lvgl.resetIdle(); } ui_mutex.unlock(); continue; diff --git a/src/ui/c/ui.c b/src/ui/c/ui.c index e72724d..a1a256c 100644 --- a/src/ui/c/ui.c +++ b/src/ui/c/ui.c @@ -1,6 +1,6 @@ /** * function declarations with nm_ prefix are typically defined in zig, - * while extern'ed function definitions with nm_ prefix are typically called from zig. + * while function definitions with nm_ prefix are extern'ed to call from zig. */ #define _DEFAULT_SOURCE /* needed for usleep() */ @@ -26,6 +26,11 @@ void nm_tab_settings_active(); */ int nm_wifi_start_connect(const char *ssid, const char *password); +/** + * callback fn when "power off" button is pressed. + */ +void nm_poweroff_btn_callback(lv_event_t *e); + static lv_style_t style_title; static lv_style_t style_text_muted; static lv_style_t style_btn_red; @@ -33,16 +38,33 @@ static const lv_font_t *font_large; static lv_obj_t *virt_keyboard; static lv_obj_t *tabview; /* main tabs content parent; lv_tabview_create */ -lv_obj_t *nm_make_topdrop() +/** + * returns user-managed data previously set on an object with nm_obj_set_userdata. + * the returned value may be NULL. + */ +extern void *nm_obj_userdata(lv_obj_t *obj) { - lv_obj_t *topdrop = lv_obj_create(lv_layer_top()); - if (!topdrop) { - return NULL; - } - lv_obj_set_style_bg_color(topdrop, lv_color_black(), 0); - lv_obj_clear_flag(topdrop, LV_OBJ_FLAG_IGNORE_LAYOUT); - lv_obj_set_size(topdrop, LV_PCT(100), LV_PCT(100)); - return topdrop; + return obj->user_data; +} + +/** + * set or "attach" user-managed data to an object. + * the data pointer may be NULL. + */ +extern void nm_obj_set_userdata(lv_obj_t *obj, void *data) +{ + obj->user_data = data; +} + +/** + * returns a "red" style for a button. useful to attract particular attention + * to a potentially "dangerous" operation. + * + * the returned value is static. available only after nm_ui_init. + */ +extern lv_style_t *nm_style_btn_red() +{ + return &style_btn_red; } static void textarea_event_cb(lv_event_t *e) @@ -125,35 +147,6 @@ static void wifi_connect_btn_callback(lv_event_t *e) nm_wifi_start_connect(buf, lv_textarea_get_text(settings.wifi_pwd_obj)); } -static void power_halt_btn_callback(lv_event_t *e) -{ - if (e->user_data) { /* ptr to msgbox */ - lv_obj_t *msgbox = e->user_data; - /* first button is "proceed", do shutdown */ - if (lv_msgbox_get_active_btn(msgbox) == 0) { - /* shutdown confirmed */ - nm_sys_shutdown(); - } - /* shutdown aborted or passthrough from confirmed shutdown. - * in the latter case, ui is still running for a brief moment, - * until ngui terminates. */ - lv_msgbox_close(msgbox); - return; - } - - /* first button must always be a "proceed", do shutdown; - * text is irrelevant */ - static const char *btns[] = {"PROCEED", "ABORT", NULL}; - lv_obj_t *msgbox = lv_msgbox_create(NULL, /* modal */ - "SHUTDOWN", /* title */ - "are you sure?", /* text */ - btns, /* */ - false /* close btn */); - lv_obj_center(msgbox); - lv_obj_add_event_cb(msgbox, power_halt_btn_callback, LV_EVENT_VALUE_CHANGED, msgbox); - return; -} - static void create_settings_panel(lv_obj_t *parent) { /******************** @@ -221,7 +214,7 @@ static void create_settings_panel(lv_obj_t *parent) settings.power_halt_btn_obj = power_halt_btn; lv_obj_set_height(power_halt_btn, LV_SIZE_CONTENT); lv_obj_add_style(power_halt_btn, &style_btn_red, 0); - lv_obj_add_event_cb(power_halt_btn, power_halt_btn_callback, LV_EVENT_CLICKED, NULL); + lv_obj_add_event_cb(power_halt_btn, nm_poweroff_btn_callback, LV_EVENT_CLICKED, NULL); lv_obj_t *power_halt_btn_label = lv_label_create(power_halt_btn); lv_label_set_text_static(power_halt_btn_label, "SHUTDOWN"); lv_obj_center(power_halt_btn_label); diff --git a/src/ui/drv.zig b/src/ui/drv.zig index 7770398..290d314 100644 --- a/src/ui/drv.zig +++ b/src/ui/drv.zig @@ -29,12 +29,12 @@ pub fn initInput() !void { /// deactivate and remove all input devices. pub fn deinitInput() void { - var indev = lvgl.lv_indev_get_next(null); + var indev: ?*lvgl.LvIndev = lvgl.LvIndev.first(); var count: usize = 0; while (indev) |d| { - lvgl.lv_indev_delete(d); + d.destroy(); count += 1; - indev = lvgl.lv_indev_get_next(null); + indev = lvgl.LvIndev.first(); } logger.debug("deinited {d} indev(s)", .{count}); } diff --git a/src/ui/lvgl.zig b/src/ui/lvgl.zig index 983a70c..2f805ef 100644 --- a/src/ui/lvgl.zig +++ b/src/ui/lvgl.zig @@ -1,8 +1,23 @@ -///! LVGL types in zig +///! LVGL types in zig. +/// +/// lv_xxx functions are directly linked against LVGL lib. +/// other functions, without the lv_ prefix are defined in zig and provide type safety +/// and extra functionality, sometimes composed of multiple calls to lv_xxx. +/// +/// nm_xxx functions defined here are exported with "c" convention to allow +/// calls from C code. +/// +/// the module usage must be started with a call to init. const std = @import("std"); +const c = @cImport({ + @cInclude("lvgl/lvgl.h"); +}); -/// initalize LVGL internals. -/// must be called before any other UI functions. +// logs LV_LOG_xxx messages from LVGL lib. +const logger = std.log.scoped(.lvgl); + +/// initalizes LVGL internals. must be called before any other UI functionality +/// is invoked, typically at program startup. pub fn init() void { init_once.call(); } @@ -14,46 +29,770 @@ fn lvglInit() void { lv_init(); } -// logs LV_LOG_xxx messages from LVGL lib -const lvgl_logger = std.log.scoped(.lvgl); - export fn nm_lvgl_log(msg: [*:0]const u8) void { const s = std.mem.span(msg); - // info level log messages are by default printed only in Debug and ReleaseSafe build modes. - lvgl_logger.debug("{s}", .{std.mem.trimRight(u8, s, "\n")}); + // info level log messages are by default printed only in Debug + // and ReleaseSafe build modes. + logger.debug("{s}", .{std.mem.trimRight(u8, s, "\n")}); } -extern "c" fn lv_init() void; -extern "c" fn lv_log_register_print_cb(*const fn (msg: [*:0]const u8) callconv(.C) void) void; +/// the busy-wait loop cycle wrapper for LVGL. +/// a program main loop must call this periodically. +/// returns the period after which it is to be called again, in ms. +pub fn loopCycle() u32 { + return lv_timer_handler(); +} /// represents lv_timer_t in C. -pub const LvTimer = opaque {}; -pub const LvTimerCallback = *const fn (timer: *LvTimer) callconv(.C) void; -/// the timer handler is the LVGL busy-wait loop iteration. -pub extern "c" fn lv_timer_handler() u32; -pub extern "c" fn lv_timer_create(callback: LvTimerCallback, period_ms: u32, userdata: ?*anyopaque) ?*LvTimer; -pub extern "c" fn lv_timer_del(timer: *LvTimer) void; -pub extern "c" fn lv_timer_set_repeat_count(timer: *LvTimer, n: i32) void; +pub const LvTimer = opaque { + pub fn destroy(self: *LvTimer) void { + lv_timer_del(self); + } -/// represents lv_obj_t type in C. -pub const LvObj = opaque {}; -/// delete and deallocate an object and all its children from UI tree. -pub extern "c" fn lv_obj_del(obj: *LvObj) void; + /// after the repeat count is reached, the timer is destroy'ed automatically. + /// to run the timer indefinitely, use -1 for repeat count. + pub fn setRepeatCount(self: *LvTimer, n: i32) void { + lv_timer_set_repeat_count(self, n); + } +}; + +/// creates a new timer with indefinite repeat count. +pub fn createTimer(f: TimerCallback, period_ms: u32, userdata: ?*anyopaque) !*LvTimer { + return lv_timer_create(f, period_ms, userdata) orelse error.OutOfMemory; +} + +/// a timer callback signature. +pub const TimerCallback = *const fn (timer: *LvTimer) callconv(.C) void; /// represents lv_indev_t in C, an input device such as touchscreen or a keyboard. -pub const LvIndev = opaque {}; -/// deallocate and delete an input device from LVGL registry. -pub extern "c" fn lv_indev_delete(indev: *LvIndev) void; -/// return next device in the list or head if indev is null. -pub extern "c" fn lv_indev_get_next(indev: ?*LvIndev) ?*LvIndev; +pub const LvIndev = opaque { + pub fn first() ?*LvIndev { + return lv_indev_get_next(null); + } + + pub fn next(self: *LvIndev) ?*LvIndev { + return lv_indev_get_next(self); + } + + pub fn destroy(self: *LvIndev) void { + lv_indev_delete(self); + } +}; + +/// a zig representation of lv_event_t, required by all event callbacks. +pub const LvEvent = opaque { + pub fn code(self: *LvEvent) EventCode { + return lv_event_get_code(self); + } + + /// returns the original event target irrespective of ObjFlag.event_bubble flag + /// on the object which generated the event. + pub fn target(self: *LvEvent) *LvObj { + return lv_event_get_target(self); + } + + /// returns user data provided at the time of a callback setup, for example LvObj.on. + pub fn userdata(self: *LvEvent) ?*anyopaque { + return lv_event_get_user_data(self); + } +}; + +/// event callback, equivalent of lv_event_cb_t. +pub const LvEventCallback = *const fn (e: *LvEvent) callconv(.C) void; +/// event descriptor returned from a callback setup. +pub const LvEventDescr = opaque {}; + +/// all possible codes for an event to trigger a function call, +/// equivalent to lv_event_code_t. +pub const EventCode = enum(c.lv_event_code_t) { + all = c.LV_EVENT_ALL, + + /// input device events + press = c.LV_EVENT_PRESSED, // the object has been pressed + pressing = c.LV_EVENT_PRESSING, // the object is being pressed (called continuously while pressing) + press_lost = c.LV_EVENT_PRESS_LOST, // the object is still being pressed but slid cursor/finger off of the object + short_click = c.LV_EVENT_SHORT_CLICKED, // the object was pressed for a short period of time, then released it. not called if scrolled. + long_press = c.LV_EVENT_LONG_PRESSED, // object has been pressed for at least `long_press_time`. not called if scrolled. + long_press_repeat = c.LV_EVENT_LONG_PRESSED_REPEAT, // called after `long_press_time` in every `long_press_repeat_time` ms. not called if scrolled. + click = c.LV_EVENT_CLICKED, // called on release if not scrolled (regardless to long press) + release = c.LV_EVENT_RELEASED, // called in every cases when the object has been released + scroll_begin = c.LV_EVENT_SCROLL_BEGIN, // scrolling begins. the event parameter is a pointer to the animation of the scroll. can be modified + scroll_end = c.LV_EVENT_SCROLL_END, // scrolling ends + scroll = c.LV_EVENT_SCROLL, // scrolling + gesture = c.LV_EVENT_GESTURE, // a gesture is detected. get the gesture with lv_indev_get_gesture_dir(lv_indev_get_act()) + key = c.LV_EVENT_KEY, // a key is sent to the object. get the key with lv_indev_get_key(lv_indev_get_act()) + focus = c.LV_EVENT_FOCUSED, // the object is focused + defocus = c.LV_EVENT_DEFOCUSED, // the object is defocused + leave = c.LV_EVENT_LEAVE, // the object is defocused but still selected + hit_test = c.LV_EVENT_HIT_TEST, // perform advanced hit-testing + + /// drawing events + cover_check = c.LV_EVENT_COVER_CHECK, // check if the object fully covers an area. the event parameter is lv_cover_check_info_t * + refr_ext_draw_size = c.LV_EVENT_REFR_EXT_DRAW_SIZE, // get the required extra draw area around the object (e.g. for shadow). the event parameter is lv_coord_t * to store the size. + draw_main_begin = c.LV_EVENT_DRAW_MAIN_BEGIN, // starting the main drawing phase + draw_main = c.LV_EVENT_DRAW_MAIN, // perform the main drawing + draw_main_end = c.LV_EVENT_DRAW_MAIN_END, // finishing the main drawing phase + draw_post_begin = c.LV_EVENT_DRAW_POST_BEGIN, // starting the post draw phase (when all children are drawn) + draw_post = c.LV_EVENT_DRAW_POST, // perform the post draw phase (when all children are drawn) + draw_post_end = c.LV_EVENT_DRAW_POST_END, // finishing the post draw phase (when all children are drawn) + draw_part_begin = c.LV_EVENT_DRAW_PART_BEGIN, // starting to draw a part. the event parameter is lv_obj_draw_dsc_t * + draw_part_end = c.LV_EVENT_DRAW_PART_END, // finishing to draw a part. the event parameter is lv_obj_draw_dsc_t * + + /// special events + value_changed = c.LV_EVENT_VALUE_CHANGED, // the object's value has changed (i.e. slider moved) + insert = c.LV_EVENT_INSERT, // a text is inserted to the object. the event data is char * being inserted. + refresh = c.LV_EVENT_REFRESH, // notify the object to refresh something on it (for the user) + ready = c.LV_EVENT_READY, // a process has finished + cancel = c.LV_EVENT_CANCEL, // a process has been cancelled + + /// other events + delete = c.LV_EVENT_DELETE, // object is being deleted + child_changed = c.LV_EVENT_CHILD_CHANGED, // child was removed, added, or its size, position were changed + child_created = c.LV_EVENT_CHILD_CREATED, // child was created, always bubbles up to all parents + child_deleted = c.LV_EVENT_CHILD_DELETED, // child was deleted, always bubbles up to all parents + screen_unload_start = c.LV_EVENT_SCREEN_UNLOAD_START, // a screen unload started, fired immediately when scr_load is called + screen_load_start = c.LV_EVENT_SCREEN_LOAD_START, // a screen load started, fired when the screen change delay is expired + screen_loaded = c.LV_EVENT_SCREEN_LOADED, // a screen was loaded + screen_unloaded = c.LV_EVENT_SCREEN_UNLOADED, // a screen was unloaded + size_changed = c.LV_EVENT_SIZE_CHANGED, // object coordinates/size have changed + style_changed = c.LV_EVENT_STYLE_CHANGED, // object's style has changed + layout_changed = c.LV_EVENT_LAYOUT_CHANGED, // the children position has changed due to a layout recalculation + get_self_size = c.LV_EVENT_GET_SELF_SIZE, // get the internal size of a widget +}; /// represents lv_disp_t in C. pub const LvDisp = opaque {}; -/// return elapsed time since last user activity on a specific display -/// or any if disp is null. -pub extern "c" fn lv_disp_get_inactive_time(disp: ?*LvDisp) u32; -/// make it so as if a user activity happened. + +/// returns display horizontal resolution. +pub fn displayHoriz() Coord { + return lv_disp_get_hor_res(null); +} + +/// returns display vertical resolution. +pub fn displayVert() Coord { + return lv_disp_get_ver_res(null); +} + +/// forces redraw of dirty areas. +pub fn displayRedraw() void { + lv_refr_now(null); +} + +/// resets user incativity time, as if a UI interaction happened "now". +pub fn resetIdle() void { + lv_disp_trig_activity(null); +} + +/// returns user inactivity time in ms. +/// inactivity is the time elapsed since the last UI action. +pub fn idleTime() u32 { + return lv_disp_get_inactive_time(null); +} + +/// returns active screen of the default display. +pub fn activeScreen() !*LvObj { + return lv_disp_get_scr_act(null) orelse error.NoDisplay; +} + +/// creates a new screen on default display. essentially same as lv_obj_create with null parent. +/// the display must be already initialized. +pub fn createScreen() !*LvObj { + if (lv_obj_create(null)) |o| { + return o; + } else { + return error.OutOfMemory; + } +} + +/// makes a screen s active. +pub fn loadScreen(s: *LvObj) void { + lv_disp_load_scr(s); +} + +/// represents lv_style_t in C. +pub const LvStyle = opaque {}; + +/// indicates which parts and in which states to apply a style to an object. +pub const StyleSelector = struct { + state: State = .default, + part: Part = .main, + + /// produce an int value suitable for lv_xxx functions. + fn value(self: StyleSelector) c.lv_style_selector_t { + return @enumToInt(self.part) | @enumToInt(self.state); + } +}; + +/// a simplified color type compatible with LVGL which defines lv_color_t +/// as a union containing an bit-fields struct unsupported in zig cImport. +pub const Color = u16; // originally c.lv_color_t; TODO: comptime switch for u32 + +//typedef union { +// struct { +//#if LV_COLOR_16_SWAP == 0 +// uint16_t blue : 5; +// uint16_t green : 6; +// uint16_t red : 5; +//#else +// uint16_t green_h : 3; +// uint16_t red : 5; +// uint16_t blue : 5; +// uint16_t green_l : 3; +//#endif +// } ch; +// uint16_t full; +//} lv_color16_t; +// +//# define LV_COLOR_MAKE16(r8, g8, b8) {{(uint8_t)((b8 >> 3) & 0x1FU), (uint8_t)((g8 >> 2) & 0x3FU), (uint8_t)((r8 >> 3) & 0x1FU)}} +// +// TODO: RGB32 +//typedef union { +// struct { +// uint8_t blue; +// uint8_t green; +// uint8_t red; +// uint8_t alpha; +// } ch; +// uint32_t full; +//} lv_color32_t; +const RGB16 = packed struct { + b: u5, + g: u6, + r: u5, +}; + +/// rgb produces a Color value base on the red, green and blue components. +pub inline fn rgb(r: u8, g: u8, b: u8) Color { + const c16 = RGB16{ + .b = @truncate(u5, b >> 3), + .g = @truncate(u6, g >> 2), + .r = @truncate(u5, r >> 3), + }; + return @bitCast(Color, c16); +} + +/// black color +pub const Black = rgb(0, 0, 0); +/// white color +pub const White = rgb(0xff, 0xff, 0xff); + +/// Palette defines a set of colors, typically used in a theme. +pub const Palette = enum(c.lv_palette_t) { + red = c.LV_PALETTE_RED, + pink = c.LV_PALETTE_PINK, + purple = c.LV_PALETTE_PURPLE, + deep_purple = c.LV_PALETTE_DEEP_PURPLE, + indigo = c.LV_PALETTE_INDIGO, + blue = c.LV_PALETTE_BLUE, + light_blue = c.LV_PALETTE_LIGHT_BLUE, + cyan = c.LV_PALETTE_CYAN, + teal = c.LV_PALETTE_TEAL, + green = c.LV_PALETTE_GREEN, + light_green = c.LV_PALETTE_LIGHT_GREEN, + lime = c.LV_PALETTE_LIME, + yellow = c.LV_PALETTE_YELLOW, + amber = c.LV_PALETTE_AMBER, + orange = c.LV_PALETTE_ORANGE, + deep_orange = c.LV_PALETTE_DEEP_ORANGE, + brown = c.LV_PALETTE_BROWN, + blue_grey = c.LV_PALETTE_BLUE_GREY, + grey = c.LV_PALETTE_GREY, + none = c.LV_PALETTE_NONE, +}; + +/// lightening or darkening levels of the main palette colors. +pub const PaletteModLevel = enum(u8) { + level1 = 1, + level2 = 2, + level3 = 3, + level4 = 4, + level5 = 5, +}; + +/// returns main color from the predefined palette. +pub inline fn paletteMain(p: Palette) Color { + return lv_palette_main(@enumToInt(p)); +} + +/// makes the main color from the predefined palette lighter according to the +/// specified level. +pub inline fn paletteLighten(p: Palette, l: PaletteModLevel) Color { + return lv_palette_lighten(@enumToInt(p), @enumToInt(l)); +} + +/// makes the main color from the predefined palette darker according to the +/// specified level. +pub inline fn paletteDarken(p: Palette, l: PaletteModLevel) Color { + return lv_palette_darken(@enumToInt(p), @enumToInt(l)); +} + +/// represents lv_obj_t type in C. +pub const LvObj = opaque { + /// deallocates all the resources used by the object, including its children. + /// user data pointers are untouched. + pub fn destroy(self: *LvObj) void { + lv_obj_del(self); + } + + /// creates a new event handler where cb is called upon event with the filter code. + /// to make cb called on any event, use EventCode.all filter. + /// multiple event handlers are called in the same order as they were added. + /// the user data pointer udata is available in a handler using LvEvent.userdata fn. + pub fn on(self: *LvObj, filter: EventCode, cb: LvEventCallback, udata: ?*anyopaque) *LvEventDescr { + return lv_obj_add_event_cb(self, cb, filter, udata); + } + + /// sets or clears an object flag. + pub fn setFlag(self: *LvObj, onoff: enum { on, off }, v: ObjFlag) void { + switch (onoff) { + .on => lv_obj_add_flag(self, @enumToInt(v)), + .off => lv_obj_clear_flag(self, @enumToInt(v)), + } + } + + /// reports whether the object has v flag set. + pub fn hasFlag(self: *LvObj, v: ObjFlag) bool { + return lv_obj_has_flag(self, @enumToInt(v)); + } + + /// returns a user data pointer associated with the object. + pub fn userdata(self: *LvObj) ?*anyopaque { + return nm_obj_userdata(self); + } + + /// associates an arbitrary data with the object. + /// the pointer can be accessed using LvObj.userdata fn. + pub fn setUserdata(self: *LvObj, data: ?*const anyopaque) void { + nm_obj_set_userdata(self, data); + } + + /// sets object horizontal length. + pub fn setWidth(self: *LvObj, val: Coord) void { + lv_obj_set_width(self, val); + } + + /// sets object vertical length. + pub fn setHeight(self: *LvObj, val: Coord) void { + lv_obj_set_height(self, val); + } + + /// sets object height to its child contents. + pub fn setHeightToContent(self: *LvObj) void { + lv_obj_set_height(self, sizeContent); + } + + /// sets both width and height to 100%. + pub fn resizeToMax(self: *LvObj) void { + lv_obj_set_size(self, sizePercent(100), sizePercent(100)); + } + + /// selects which side to pad in setPad func. + pub const PadSelector = enum { left, right, top, bottom, row, column }; + + /// adds a padding style to the object. + pub fn setPad(self: *LvObj, v: Coord, p: PadSelector, sel: StyleSelector) void { + switch (p) { + .left => lv_obj_set_style_pad_left(self, v, sel.value()), + .right => lv_obj_set_style_pad_right(self, v, sel.value()), + .top => lv_obj_set_style_pad_top(self, v, sel.value()), + .bottom => lv_obj_set_style_pad_bottom(self, v, sel.value()), + .row => lv_obj_set_style_pad_row(self, v, sel.value()), + .column => lv_obj_set_style_pad_column(self, v, sel.value()), + } + } + + /// same as setPad .column but using a default constant to make flex layouts consistent. + pub fn padColumnDefault(self: *LvObj) void { + self.setPad(20, .column, .{}); + } + + /// aligns object to the center of its parent. + pub fn center(self: *LvObj) void { + self.posAlign(.center, 0, 0); + } + + /// aligns object position. the offset is relative to the specified alignment a. + pub fn posAlign(self: *LvObj, a: PosAlign, xoffset: Coord, yoffset: Coord) void { + lv_obj_align(self, @enumToInt(a), xoffset, yoffset); + } + + /// sets flex layout flow on the object. + pub fn flexFlow(self: *LvObj, ff: FlexFlow) void { + lv_obj_set_flex_flow(self, @enumToInt(ff)); + } + + /// sets flex layout growth property; same meaning as in CSS flex. + pub fn flexGrow(self: *LvObj, val: u8) void { + lv_obj_set_flex_grow(self, val); + } + + /// sets flex layout alignments. + pub fn flexAlign(self: *LvObj, main: FlexAlign, cross: FlexAlignCross, track: FlexAlign) void { + lv_obj_set_flex_align(self, @enumToInt(main), @enumToInt(cross), @enumToInt(track)); + } + + /// adds a style to the object or one of its parts/states based on the selector. + pub fn addStyle(self: *LvObj, v: *LvStyle, sel: StyleSelector) void { + lv_obj_add_style(self, v, sel.value()); + } + + /// removes all styles from the object. + pub fn removeAllStyle(self: *LvObj) void { + const sel = StyleSelector{ .part = .any, .state = .any }; + lv_obj_remove_style(self, null, sel.value()); + } + + /// removes only the background styles from the object. + pub fn removeBackgroundStyle(self: *LvObj) void { + const sel = StyleSelector{ .part = .main, .state = .any }; + lv_obj_remove_style(self, null, sel.value()); + } + + /// sets a desired background color to objects parts/states. + pub fn setBackgroundColor(self: *LvObj, v: Color, sel: StyleSelector) void { + lv_obj_set_style_bg_color(self, v, sel.value()); + } +}; + +pub fn createObject(parent: *LvObj) !*LvObj { + return lv_obj_create(parent) orelse error.OutOfMemory; +} + +pub fn createFlexObject(parent: *LvObj, flow: FlexFlow) !*LvObj { + var o = try createObject(parent); + o.flexFlow(flow); + return o; +} + +pub fn createTopObject() !*LvObj { + const toplayer = lv_disp_get_layer_top(null); + return lv_obj_create(toplayer) orelse error.OutOfMemory; +} + +/// feature-flags controlling object's behavior. +/// OR'ed values are possible. +pub const ObjFlag = enum(c.lv_obj_flag_t) { + hidden = c.LV_OBJ_FLAG_HIDDEN, // make the object hidden, like it wasn't there at all + clickable = c.LV_OBJ_FLAG_CLICKABLE, // make the object clickable by the input devices + focusable = c.LV_OBJ_FLAG_CLICK_FOCUSABLE, // add focused state to the object when clicked + checkable = c.LV_OBJ_FLAG_CHECKABLE, // toggle checked state when the object is clicked + scrollable = c.LV_OBJ_FLAG_SCROLLABLE, // make the object scrollable + scroll_elastic = c.LV_OBJ_FLAG_SCROLL_ELASTIC, // allow scrolling inside but with slower speed + scroll_momentum = c.LV_OBJ_FLAG_SCROLL_MOMENTUM, // make the object scroll further when "thrown" + scroll_one = c.LV_OBJ_FLAG_SCROLL_ONE, // allow scrolling only one snappable children + scroll_chain_hor = c.LV_OBJ_FLAG_SCROLL_CHAIN_HOR, // allow propagating the horizontal scroll to a parent + scroll_chain_ver = c.LV_OBJ_FLAG_SCROLL_CHAIN_VER, // allow propagating the vertical scroll to a parent + scroll_chain = c.LV_OBJ_FLAG_SCROLL_CHAIN, + scroll_on_focus = c.LV_OBJ_FLAG_SCROLL_ON_FOCUS, // automatically scroll object to make it visible when focused + scroll_with_arrow = c.LV_OBJ_FLAG_SCROLL_WITH_ARROW, // allow scrolling the focused object with arrow keys + snappable = c.LV_OBJ_FLAG_SNAPPABLE, // if scroll snap is enabled on the parent it can snap to this object + press_lock = c.LV_OBJ_FLAG_PRESS_LOCK, // keep the object pressed even if the press slid from the object + event_bubble = c.LV_OBJ_FLAG_EVENT_BUBBLE, // propagate the events to the parent too + gesture_bubble = c.LV_OBJ_FLAG_GESTURE_BUBBLE, // propagate the gestures to the parent + ignore_layout = c.LV_OBJ_FLAG_IGNORE_LAYOUT, // make the object position-able by the layouts + floating = c.LV_OBJ_FLAG_FLOATING, // do not scroll the object when the parent scrolls and ignore layout + overflow_visible = c.LV_OBJ_FLAG_OVERFLOW_VISIBLE, // do not clip the children's content to the parent's boundary + + user1 = c.LV_OBJ_FLAG_USER_1, // custom flag, free to use by user + user2 = c.LV_OBJ_FLAG_USER_2, // custom flag, free to use by user + user3 = c.LV_OBJ_FLAG_USER_3, // custom flag, free to use by user + user4 = c.LV_OBJ_FLAG_USER_4, // custom flag, free to use by user +}; + +/// possible states of a widget, equivalent to lv_state_t in C. +/// OR'ed values are possible. +pub const State = enum(c.lv_state_t) { + default = c.LV_STATE_DEFAULT, + checked = c.LV_STATE_CHECKED, + focused = c.LV_STATE_FOCUSED, + focuse_key = c.LV_STATE_FOCUS_KEY, + edited = c.LV_STATE_EDITED, + hovered = c.LV_STATE_HOVERED, + pressed = c.LV_STATE_PRESSED, + scrolled = c.LV_STATE_SCROLLED, + disabled = c.LV_STATE_DISABLED, + + user1 = c.LV_STATE_USER_1, + user2 = c.LV_STATE_USER_2, + user3 = c.LV_STATE_USER_3, + user4 = c.LV_STATE_USER_4, + + any = c.LV_STATE_ANY, // special value can be used in some functions to target all states +}; + +/// possible parts of a widget, equivalent to lv_part_t in C. +/// a part is an internal building block of a widget. for example, a slider +/// consists of background, an indicator and a knob. +pub const Part = enum(c.lv_part_t) { + main = c.LV_PART_MAIN, // a background like rectangle + scrollbar = c.LV_PART_SCROLLBAR, + indicator = c.LV_PART_INDICATOR, // an indicator for slider, bar, switch, or the tick box of the checkbox + knob = c.LV_PART_KNOB, // like a handle to grab to adjust the value + selected = c.LV_PART_SELECTED, // the currently selected option or section + item = c.LV_PART_ITEMS, // used when the widget has multiple similar elements like table cells + ticks = c.LV_PART_TICKS, // ticks on a scale like in a chart or a meter + cursor = c.LV_PART_CURSOR, // a specific place in text areas or a chart + + custom1 = c.LV_PART_CUSTOM_FIRST, // extension point for custom widgets + + any = c.LV_PART_ANY, // special value can be used in some functions to target all parts +}; + +/// Coord is a pixel unit for all x/y coordinates and dimensions. +pub const Coord = c.lv_coord_t; + +/// converts a percentage value between 0 and 1000 to a regular unit. +/// equivalent to LV_PCT in C. +pub inline fn sizePercent(v: Coord) Coord { + return if (v < 0) LV_COORD_SET_SPEC(1000 - v) else LV_COORD_SET_SPEC(v); +} + +/// a special constant setting a [part of] widget to its contents. +/// equivalent to LV_SIZE_CONTENT in C. +pub const sizeContent = LV_COORD_SET_SPEC(2001); + +// from lv_area.h +//#if LV_USE_LARGE_COORD +//#define _LV_COORD_TYPE_SHIFT (29U) +//#else +//#define _LV_COORD_TYPE_SHIFT (13U) +//#endif +const _LV_COORD_TYPE_SHIFT = 13; // TODO: comptime switch between 13 and 29? +const _LV_COORD_TYPE_SPEC = 1 << _LV_COORD_TYPE_SHIFT; + +inline fn LV_COORD_SET_SPEC(x: Coord) Coord { + return x | _LV_COORD_TYPE_SPEC; +} + +/// object position alignments. +pub const PosAlign = enum(c.lv_align_t) { + default = c.LV_ALIGN_DEFAULT, + top_left = c.LV_ALIGN_TOP_LEFT, + top_mid = c.LV_ALIGN_TOP_MID, + top_right = c.LV_ALIGN_TOP_RIGHT, + bottom_left = c.LV_ALIGN_BOTTOM_LEFT, + bottom_mid = c.LV_ALIGN_BOTTOM_MID, + bottom_right = c.LV_ALIGN_BOTTOM_RIGHT, + left_mid = c.LV_ALIGN_LEFT_MID, + right_mid = c.LV_ALIGN_RIGHT_MID, + center = c.LV_ALIGN_CENTER, + + out_top_left = c.LV_ALIGN_OUT_TOP_LEFT, + out_top_mid = c.LV_ALIGN_OUT_TOP_MID, + out_top_right = c.LV_ALIGN_OUT_TOP_RIGHT, + out_bottom_left = c.LV_ALIGN_OUT_BOTTOM_LEFT, + out_bottom_mid = c.LV_ALIGN_OUT_BOTTOM_MID, + out_bottom_right = c.LV_ALIGN_OUT_BOTTOM_RIGHT, + out_left_top = c.LV_ALIGN_OUT_LEFT_TOP, + out_left_mid = c.LV_ALIGN_OUT_LEFT_MID, + out_left_bottom = c.LV_ALIGN_OUT_LEFT_BOTTOM, + out_right_top = c.LV_ALIGN_OUT_RIGHT_TOP, + out_right_mid = c.LV_ALIGN_OUT_RIGHT_MID, + out_right_bottom = c.LV_ALIGN_OUT_RIGHT_BOTTOM, +}; + +/// flex layout alignments. +pub const FlexAlign = enum(c.lv_flex_align_t) { + start = c.LV_FLEX_ALIGN_START, + end = c.LV_FLEX_ALIGN_END, + center = c.LV_FLEX_ALIGN_CENTER, + space_evenly = c.LV_FLEX_ALIGN_SPACE_EVENLY, + space_around = c.LV_FLEX_ALIGN_SPACE_AROUND, + space_between = c.LV_FLEX_ALIGN_SPACE_BETWEEN, +}; + +/// flex layout cross-axis alignments. +pub const FlexAlignCross = enum(c.lv_flex_align_t) { + start = c.LV_FLEX_ALIGN_START, + end = c.LV_FLEX_ALIGN_END, + center = c.LV_FLEX_ALIGN_CENTER, +}; + +/// flex layout flow variations. +pub const FlexFlow = enum(c.lv_flex_flow_t) { + row = c.LV_FLEX_FLOW_ROW, + column = c.LV_FLEX_FLOW_COLUMN, + row_wrap = c.LV_FLEX_FLOW_ROW_WRAP, + row_reverse = c.LV_FLEX_FLOW_ROW_REVERSE, + row_wrap_reverse = c.LV_FLEX_FLOW_ROW_WRAP_REVERSE, + column_wrap = c.LV_FLEX_FLOW_COLUMN_WRAP, + column_reverse = c.LV_FLEX_FLOW_COLUMN_REVERSE, + column_wrap_reverse = c.LV_FLEX_FLOW_COLUMN_WRAP_REVERSE, +}; + +/// if parent is null, uses lv_scr_act. +pub fn createWindow(parent: ?*LvObj, header_height: i16, title: [*:0]const u8) !Window { + const pobj = parent orelse try activeScreen(); + const winobj = lv_win_create(pobj, header_height) orelse return error.OutOfMemory; + if (lv_win_add_title(winobj, title) == null) { + return error.OutOfMemory; + } + return .{ .winobj = winobj }; +} + +const Window = struct { + winobj: *LvObj, + + pub fn content(self: Window) *LvObj { + return lv_win_get_content(self.winobj); + } +}; + +pub const CreateLabelOpt = struct { + long_mode: enum(c.lv_label_long_mode_t) { + wrap = c.LV_LABEL_LONG_WRAP, // keep the object width, wrap the too long lines and expand the object height + dot = c.LV_LABEL_LONG_DOT, // keep the size and write dots at the end if the text is too long + scroll = c.LV_LABEL_LONG_SCROLL, // keep the size and roll the text back and forth + scroll_circular = c.LV_LABEL_LONG_SCROLL_CIRCULAR, // keep the size and roll the text circularly + clip = c.LV_LABEL_LONG_CLIP, // keep the size and clip the text out of it + }, + pos: enum { + none, + centered, + }, +}; + +pub fn createLabel(parent: *LvObj, text: [*:0]const u8, opt: CreateLabelOpt) !*LvObj { + var lb = lv_label_create(parent) orelse return error.OutOfMemory; + //lv_label_set_text_static(lb, text); // static doesn't work with .dot + lv_label_set_text(lb, text); + lv_label_set_recolor(lb, true); + //lv_obj_set_height(lb, sizeContent); // default + lv_label_set_long_mode(lb, @enumToInt(opt.long_mode)); + if (opt.pos == .centered) { + lb.center(); + } + return lb; +} + +pub fn createButton(parent: *LvObj, label: [*:0]const u8) !*LvObj { + const btn = lv_btn_create(parent) orelse return error.OutOfMemory; + _ = try createLabel(btn, label, .{ .long_mode = .dot, .pos = .centered }); + return btn; +} + +// ========================================================================== +// imports from nakamochi custom C code that extends LVGL +// ========================================================================== + +/// returns a red button style. +pub extern fn nm_style_btn_red() *LvStyle; // TODO: make it private + +// the "native" lv_obj_set/get user_data are static inline, so make our own funcs. +extern "c" fn nm_obj_userdata(obj: *LvObj) ?*anyopaque; +extern "c" fn nm_obj_set_userdata(obj: *LvObj, data: ?*const anyopaque) void; + +// ========================================================================== +// imports from LVGL C code +// ========================================================================== + +extern fn lv_init() void; +extern fn lv_log_register_print_cb(*const fn (msg: [*:0]const u8) callconv(.C) void) void; + +// input devices ------------------------------------------------------------ + +/// deallocate and delete an input device from LVGL registry. +extern fn lv_indev_delete(indev: *LvIndev) void; +/// return next device in the list or head if indev is null. +extern fn lv_indev_get_next(indev: ?*LvIndev) ?*LvIndev; + +// timers ------------------------------------------------------------------- + +/// timer handler is the busy-wait loop in LVGL. +/// returns period after which it is to be run again, in ms. +extern fn lv_timer_handler() u32; +extern fn lv_timer_create(callback: TimerCallback, period_ms: u32, userdata: ?*anyopaque) ?*LvTimer; +extern fn lv_timer_del(timer: *LvTimer) void; +extern fn lv_timer_set_repeat_count(timer: *LvTimer, n: i32) void; + +// events -------------------------------------------------------------------- + +extern fn lv_event_get_code(e: *LvEvent) EventCode; +extern fn lv_event_get_current_target(e: *LvEvent) *LvObj; +extern fn lv_event_get_target(e: *LvEvent) *LvObj; +extern fn lv_event_get_user_data(e: *LvEvent) ?*anyopaque; +extern fn lv_obj_add_event_cb(obj: *LvObj, cb: LvEventCallback, filter: EventCode, userdata: ?*anyopaque) *LvEventDescr; + +// display and screen functions ---------------------------------------------- + +/// returns pointer to the default display. +extern fn lv_disp_get_default() *LvDisp; +/// returns elapsed time since last user activity on a specific display or any if disp is null. +extern fn lv_disp_get_inactive_time(disp: ?*LvDisp) u32; +/// makes it so as if a user activity happened. /// this resets an internal counter in lv_disp_get_inactive_time. -pub extern "c" fn lv_disp_trig_activity(disp: ?*LvDisp) void; -/// force redraw dirty areas. -pub extern "c" fn lv_refr_now(disp: ?*LvDisp) void; +extern fn lv_disp_trig_activity(disp: ?*LvDisp) void; +/// forces redraw of dirty areas. +extern fn lv_refr_now(disp: ?*LvDisp) void; + +extern fn lv_disp_get_hor_res(disp: ?*LvDisp) c.lv_coord_t; +extern fn lv_disp_get_ver_res(disp: ?*LvDisp) c.lv_coord_t; + +/// returns a pointer to the active screen on a given display +/// or default if null. if no display is registered, returns null. +extern fn lv_disp_get_scr_act(disp: ?*LvDisp) ?*LvObj; +/// returns the top layer on a given display or default if null. +/// top layer is the same on every screen, above the normal screen layer. +extern fn lv_disp_get_layer_top(disp: ?*LvDisp) *LvObj; +/// makes a screen active without animation. +extern fn lv_disp_load_scr(scr: *LvObj) void; + +// styling and colors -------------------------------------------------------- + +/// initalizes a style struct. must be called only once per style. +extern fn lv_style_init(style: *LvStyle) void; +extern fn lv_style_set_bg_color(style: *LvStyle, color: Color) void; + +extern fn lv_obj_add_style(obj: *LvObj, style: *LvStyle, sel: c.lv_style_selector_t) void; +extern fn lv_obj_remove_style(obj: *LvObj, style: ?*LvStyle, sel: c.lv_style_selector_t) void; +extern fn lv_obj_remove_style_all(obj: *LvObj) void; +extern fn lv_obj_set_style_bg_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_pad_left(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_pad_right(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_pad_top(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_pad_bottom(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_pad_row(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; +extern fn lv_obj_set_style_pad_column(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void; + +// TODO: port these to zig +extern fn lv_palette_main(c.lv_palette_t) Color; +extern fn lv_palette_lighten(c.lv_palette_t, level: u8) Color; +extern fn lv_palette_darken(c.lv_palette_t, level: u8) Color; + +// objects and widgets ------------------------------------------------------- + +/// creates a new base object at the specific parent or a new screen if the parent is null. +extern fn lv_obj_create(parent: ?*LvObj) ?*LvObj; +/// deletes and deallocates an object and all its children from UI tree. +extern fn lv_obj_del(obj: *LvObj) void; + +extern fn lv_obj_add_flag(obj: *LvObj, v: c.lv_obj_flag_t) void; +extern fn lv_obj_clear_flag(obj: *LvObj, v: c.lv_obj_flag_t) void; +extern fn lv_obj_has_flag(obj: *LvObj, v: c.lv_obj_flag_t) bool; + +extern fn lv_obj_align(obj: *LvObj, a: c.lv_align_t, x: c.lv_coord_t, y: c.lv_coord_t) void; +extern fn lv_obj_set_height(obj: *LvObj, h: c.lv_coord_t) void; +extern fn lv_obj_set_width(obj: *LvObj, w: c.lv_coord_t) void; +extern fn lv_obj_set_size(obj: *LvObj, w: c.lv_coord_t, h: c.lv_coord_t) void; + +extern fn lv_obj_set_flex_flow(obj: *LvObj, flow: c.lv_flex_flow_t) void; +extern fn lv_obj_set_flex_grow(obj: *LvObj, val: u8) void; +extern fn lv_obj_set_flex_align(obj: *LvObj, main: c.lv_flex_align_t, cross: c.lv_flex_align_t, track: c.lv_flex_align_t) void; + +extern fn lv_btn_create(parent: *LvObj) ?*LvObj; + +extern fn lv_btnmatrix_create(parent: *LvObj) ?*LvObj; +extern fn lv_btnmatrix_set_selected_btn(obj: *LvObj, id: u16) void; +extern fn lv_btnmatrix_set_map(obj: *LvObj, map: [*]const [*:0]const u8) void; +extern fn lv_btnmatrix_set_btn_ctrl(obj: *LvObj, id: u16, ctrl: c.lv_btnmatrix_ctrl_t) void; +extern fn lv_btnmatrix_set_btn_ctrl_all(obj: *LvObj, ctrl: c.lv_btnmatrix_ctrl_t) void; + +extern fn lv_label_create(parent: *LvObj) ?*LvObj; +extern fn lv_label_set_text(label: *LvObj, text: [*:0]const u8) void; +extern fn lv_label_set_text_static(label: *LvObj, text: [*:0]const u8) void; +extern fn lv_label_set_long_mode(label: *LvObj, mode: c.lv_label_long_mode_t) void; +extern fn lv_label_set_recolor(label: *LvObj, enable: bool) void; + +extern fn lv_win_create(parent: *LvObj, header_height: c.lv_coord_t) ?*LvObj; +extern fn lv_win_add_title(win: *LvObj, title: [*:0]const u8) ?*LvObj; +extern fn lv_win_get_content(win: *LvObj) *LvObj; diff --git a/src/ui/screen.zig b/src/ui/screen.zig index 16894d0..c5bf992 100644 --- a/src/ui/screen.zig +++ b/src/ui/screen.zig @@ -4,7 +4,7 @@ const Thread = std.Thread; const lvgl = @import("lvgl.zig"); const drv = @import("drv.zig"); -const ui = @import("ui.zig"); +const widget = @import("widget.zig"); const logger = std.log.scoped(.screen); @@ -14,10 +14,10 @@ const logger = std.log.scoped(.screen); /// a touch event triggers no accidental action. pub fn sleep(wake: *const Thread.ResetEvent) void { drv.deinitInput(); - ui.topdrop(.show); + widget.topdrop(.show); defer { drv.initInput() catch |err| logger.err("drv.initInput: {any}", .{err}); - ui.topdrop(.remove); + widget.topdrop(.remove); } const watcher = drv.InputWatcher() catch |err| { diff --git a/src/ui/symbol.zig b/src/ui/symbol.zig index 0dcd0ce..e830295 100644 --- a/src/ui/symbol.zig +++ b/src/ui/symbol.zig @@ -1,3 +1,4 @@ ///! see lv_symbols_def.h -pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 }; pub const Ok = &[_]u8{ 0xef, 0x80, 0x8c }; +pub const Power = &[_]u8{ 0xef, 0x80, 0x91 }; +pub const Warning = &[_]u8{ 0xef, 0x81, 0xb1 }; diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 75f8938..043938e 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -2,12 +2,13 @@ const std = @import("std"); const lvgl = @import("lvgl.zig"); const drv = @import("drv.zig"); +const symbol = @import("symbol.zig"); +const widget = @import("widget.zig"); const logger = std.log.scoped(.ui); extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int; -extern "c" fn nm_make_topdrop() ?*lvgl.LvObj; -extern "c" fn nm_remove_topdrop() void; +extern fn nm_sys_shutdown() void; pub fn init() !void { lvgl.init(); @@ -23,26 +24,29 @@ pub fn init() !void { } } -/// unsafe for concurrent use. -pub fn topdrop(onoff: enum { show, remove }) void { - // a static construct: there can be only one global topdrop. - // see https://ziglang.org/documentation/master/#Static-Local-Variables - const S = struct { - var lv_obj: ?*lvgl.LvObj = null; +/// called when "power off" button is pressed. +export fn nm_poweroff_btn_callback(e: *lvgl.LvEvent) void { + _ = e; + const proceed: [*:0]const u8 = "PROCEED"; + const abort: [*:0]const u8 = "CANCEL"; + const title = " " ++ symbol.Power ++ " SHUTDOWN"; + const text = + \\ARE YOU SURE? + \\ + \\once shut down, + \\payments cannot go through via bitcoin or lightning networks + \\until the node is powered back on. + ; + widget.modal(title, text, &.{ proceed, abort }, poweroffModalCallback) catch |err| { + logger.err("shutdown btn: modal: {any}", .{err}); }; - switch (onoff) { - .show => { - if (S.lv_obj != null) { - return; - } - S.lv_obj = nm_make_topdrop(); - lvgl.lv_refr_now(null); - }, - .remove => { - if (S.lv_obj) |v| { - lvgl.lv_obj_del(v); - S.lv_obj = null; - } - }, +} + +fn poweroffModalCallback(btn_idx: usize) void { + // proceed = 0, cancel = 1 + if (btn_idx != 0) { + return; } + // proceed with shutdown + nm_sys_shutdown(); } diff --git a/src/ui/widget.zig b/src/ui/widget.zig new file mode 100644 index 0000000..1338d04 --- /dev/null +++ b/src/ui/widget.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const lvgl = @import("lvgl.zig"); + +const logger = std.log.scoped(.ui); + +/// creates an opposite of a backdrop: a plain black square on the top layer +/// covering the whole screen. useful for standby/sleep mode on systems where +/// cutting screen power is unsupported. +/// +/// unsafe for concurrent use. +pub fn topdrop(onoff: enum { show, remove }) void { + // a static construct: there can be only one global topdrop. + // see https://ziglang.org/documentation/master/#Static-Local-Variables + const S = struct { + var lv_obj: ?*lvgl.LvObj = null; + }; + switch (onoff) { + .show => { + if (S.lv_obj != null) { + return; + } + + const o = lvgl.createTopObject() catch |err| { + logger.err("topdrop: lvgl.createTopObject: {any}", .{err}); + return; + }; + o.setFlag(.on, .ignore_layout); + o.resizeToMax(); + o.setBackgroundColor(lvgl.Black, .{}); + + S.lv_obj = o; + lvgl.displayRedraw(); + }, + .remove => { + if (S.lv_obj) |o| { + o.destroy(); + S.lv_obj = null; + } + }, + } +} + +/// modal callback func type. it receives 0-based index of a button item +/// provided as btns arg to modal. +pub const ModalButtonCallbackFn = *const fn (index: usize) void; + +/// shows a non-dismissible window using the whole screen real estate; +/// for use in place of lv_msgbox_create. +/// +/// while all heap-alloc'ed resources are free'd automatically right before cb is called, +/// the value of title, text and btns args must live at least as long as cb; they are +/// memory-managed by the callers. +pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const u8, cb: ModalButtonCallbackFn) !void { + const win = try lvgl.createWindow(null, 60, title); + errdefer win.winobj.destroy(); // also deletes all children created below + win.winobj.setUserdata(@ptrCast(?*const anyopaque, cb)); + + const wincont = win.content(); + wincont.flexFlow(.column); + wincont.flexAlign(.start, .center, .center); + const msg = try lvgl.createLabel(wincont, text, .{ .long_mode = .wrap, .pos = .centered }); + msg.setWidth(lvgl.displayHoriz() - 100); + msg.flexGrow(1); + + const btncont = try lvgl.createFlexObject(wincont, .row); + btncont.removeBackgroundStyle(); + btncont.padColumnDefault(); + btncont.flexAlign(.center, .center, .center); + btncont.setWidth(lvgl.displayHoriz() - 40); + btncont.setHeightToContent(); + + // leave 5% as an extra spacing. + const btnwidth = lvgl.sizePercent(try std.math.divFloor(i16, 95, @truncate(u8, btns.len))); + for (btns) |btext, i| { + const btn = try lvgl.createButton(btncont, btext); + btn.setWidth(btnwidth); + btn.setFlag(.on, .event_bubble); + btn.setFlag(.on, .user1); // .user1 indicates actionable button in callback + if (i == 0) { + btn.addStyle(lvgl.nm_style_btn_red(), .{}); + } + btn.setUserdata(@intToPtr(?*anyopaque, i)); // button index in callback + } + _ = btncont.on(.click, nm_modal_callback, win.winobj); +} + +export fn nm_modal_callback(e: *lvgl.LvEvent) void { + if (e.userdata()) |event_data| { + const target = e.target(); + if (!target.hasFlag(.user1)) { // .user1 is set by modal fn + return; + } + + const btn_index = @ptrToInt(target.userdata()); + const winobj = @ptrCast(*lvgl.LvObj, event_data); + // guaranteed to be aligned due to cb arg in modal fn. + const cb = @ptrCast(ModalButtonCallbackFn, @alignCast(@alignOf(ModalButtonCallbackFn), winobj.userdata())); + winobj.destroy(); + cb(btn_index); + } +}