ngui: port some LVGL-based UI code from C to zig
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline was successful Details

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.
alex 12 months ago
parent c73867ea4d
commit 4297c139a1
Signed by: x1ddos

@ -12,3 +12,4 @@ BinPackArguments: false
BinPackParameters: false
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortFunctionsOnASingleLine: Empty

@ -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 {"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 {
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),
} };"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|;
@ -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);
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| {
} else |err| {
logger.err("network status timer failed: {any}", .{err});
@ -161,7 +158,8 @@ fn commThread() void {
fn commThreadLoopCycle() !void {
const msg =, 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.
quit = true;
@ -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, .{});
// 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) {
var till_next_ms = lvgl.lv_timer_handler();
var till_next_ms = lvgl.loopCycle();
const do_quit = quit;
const do_state = state;
@ -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});

@ -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 */
/* shutdown aborted or passthrough from confirmed shutdown.
* in the latter case, ui is still running for a brief moment,
* until ngui terminates. */
/* 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_add_event_cb(msgbox, power_halt_btn_callback, LV_EVENT_VALUE_CHANGED, msgbox);
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");

@ -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| {
count += 1;
indev = lvgl.lv_indev_get_next(null);
indev = lvgl.LvIndev.first();
logger.debug("deinited {d} indev(s)", .{count});

@ -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({
/// 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 {;
@ -14,46 +29,770 @@ fn lvglInit() void {
// 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 {
/// 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 {
/// 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 {
/// resets user incativity time, as if a UI interaction happened "now".
pub fn resetIdle() void {
/// 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 {
/// 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;
// uint16_t green_h : 3;
// uint16_t red : 5;
// uint16_t blue : 5;
// uint16_t green_l : 3;
// } 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) {
deep_purple = c.LV_PALETTE_DEEP_PURPLE,
light_blue = c.LV_PALETTE_LIGHT_BLUE,
light_green = c.LV_PALETTE_LIGHT_GREEN,
deep_orange = c.LV_PALETTE_DEEP_ORANGE,
blue_grey = c.LV_PALETTE_BLUE_GREY,
/// 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 {
/// 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);
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
//#define _LV_COORD_TYPE_SHIFT (29U)
//#define _LV_COORD_TYPE_SHIFT (13U)
const _LV_COORD_TYPE_SHIFT = 13; // TODO: comptime switch between 13 and 29?
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) {
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) {
/// flex layout flow variations.
pub const FlexFlow = enum(c.lv_flex_flow_t) {
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 {
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) {;
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;

@ -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 {
defer {
drv.initInput() catch |err| logger.err("drv.initInput: {any}", .{err});
const watcher = drv.InputWatcher() catch |err| {

@ -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 };