ui/lvgl: improve UI element types in zig
ci/woodpecker/push/woodpecker Pipeline was successful Details

this commit improves on the LVGL API wrapping interface in zig by
defining different types where each is associated with either a
particular lv_xxx_t object in C or a logically different UI element.

this makes programming much more pleasant, allows compartmentalizing
different UI concepts and elements, and reduces chances of making a
thanks to stricter type enforcement.

no visual changes in the UI.
pull/24/head
alex 1 year ago
parent 746b179478
commit 7d1ab5cb78
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -141,7 +141,7 @@ 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.createTimer(nm_request_network_status, 1000, null)) |t| {
if (lvgl.LvTimer.new(nm_request_network_status, 1000, null)) |t| {
t.setRepeatCount(1);
} else |err| {
logger.err("network status timer failed: {any}", .{err});
@ -293,8 +293,8 @@ pub fn main() anyerror!void {
// run idle timer indefinitely.
// continue on failure: screen standby won't work at the worst.
_ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| {
logger.err("lvgl.CreateTimer(idle check): {any}", .{err});
_ = lvgl.LvTimer.new(nm_check_idle_time, 2000, null) catch |err| {
logger.err("lvgl.LvTimer.new(idle check): {any}", .{err});
};
{

@ -72,6 +72,11 @@ extern lv_style_t *nm_style_btn_red()
return &style_btn_red;
}
extern lv_style_t *nm_style_title()
{
return &style_title;
}
static void textarea_event_cb(lv_event_t *e)
{
lv_obj_t *textarea = lv_event_get_target(e);

@ -46,6 +46,14 @@ pub fn loopCycle() u32 {
/// represents lv_timer_t in C.
pub const LvTimer = opaque {
/// timer callback signature.
pub const Callback = *const fn (timer: *LvTimer) callconv(.C) void;
/// creates a new timer with indefinite repeat count.
pub fn new(f: Callback, period_ms: u32, userdata: ?*anyopaque) !*LvTimer {
return lv_timer_create(f, period_ms, userdata) orelse error.OutOfMemory;
}
pub fn destroy(self: *LvTimer) void {
lv_timer_del(self);
}
@ -57,14 +65,6 @@ pub const LvTimer = opaque {
}
};
/// 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 {
pub fn first() ?*LvIndev {
@ -80,32 +80,17 @@ pub const LvIndev = opaque {
}
};
/// a zig representation of lv_event_t, required by all event callbacks.
/// represents lv_event_t in C, 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;
pub const Callback = *const fn (e: *LvEvent) callconv(.C) void;
/// event descriptor returned from a callback setup.
pub const LvEventDescr = opaque {};
pub const Descriptor = 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) {
pub const Code = enum(c.lv_event_code_t) {
all = c.LV_EVENT_ALL,
/// input device events
@ -161,21 +146,40 @@ pub const EventCode = enum(c.lv_event_code_t) {
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 {};
/// returns the code of the triggered event.
pub fn code(self: *LvEvent) Code {
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);
}
};
/// represents lv_disp_t in C.
pub const LvDisp = opaque {
/// returns display horizontal resolution.
pub fn displayHoriz() Coord {
/// the display must be already initialized.
pub fn horiz() Coord {
return lv_disp_get_hor_res(null);
}
/// returns display vertical resolution.
pub fn displayVert() Coord {
/// the display must be already initialized.
pub fn vert() Coord {
return lv_disp_get_ver_res(null);
}
};
/// forces redraw of dirty areas.
pub fn displayRedraw() void {
pub fn redraw() void {
lv_refr_now(null);
}
@ -190,39 +194,19 @@ 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 {};
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,
pub const Selector = struct {
part: Part = .main,
state: State = .default,
/// produce an int value suitable for lv_xxx functions.
fn value(self: StyleSelector) c.lv_style_selector_t {
/// produces an int value suitable for lv_xxx functions.
fn value(self: Selector) 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.
@ -299,10 +283,9 @@ pub const Palette = enum(c.lv_palette_t) {
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) {
pub const ModLevel = enum(u8) {
level1 = 1,
level2 = 2,
level3 = 3,
@ -311,190 +294,454 @@ pub const PaletteModLevel = enum(u8) {
};
/// returns main color from the predefined palette.
pub inline fn paletteMain(p: Palette) Color {
pub inline fn main(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 {
pub inline fn lighten(p: Palette, l: ModLevel) 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 {
pub inline fn darken(p: Palette, l: ModLevel) Color {
return lv_palette_darken(@enumToInt(p), @enumToInt(l));
}
};
/// represents lv_obj_t type in C.
pub const LvObj = opaque {
/// a set of methods applicable to every kind of lv_xxx object.
pub const BaseObjMethods = struct {
/// deallocates all resources used by the object, including its children.
/// user data pointers are untouched.
pub fn destroy(self: *LvObj) void {
lv_obj_del(self);
pub fn destroy(self: anytype) void {
lv_obj_del(self.lvobj);
}
/// deallocates all resources used by the object's children.
pub fn deleteChildren(self: *LvObj) void {
lv_obj_clean(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 label text to a new value.
pub fn setLabelText(self: *LvObj, text: [*:0]const u8) void {
lv_label_set_text(self, text);
pub fn deleteChildren(self: anytype) void {
lv_obj_clean(self.lvobj);
}
/// 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)),
pub fn setFlag(self: anytype, v: LvObj.Flag) void {
lv_obj_add_flag(self.lvobj, @enumToInt(v));
}
pub fn clearFlag(self: anytype, v: LvObj.Flag) void {
lv_obj_clear_flag(self.lvobj, @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));
pub fn hasFlag(self: anytype, v: LvObj.Flag) bool {
return lv_obj_has_flag(self.lvobj, @enumToInt(v));
}
/// returns a user data pointer associated with the object.
pub fn userdata(self: *LvObj) ?*anyopaque {
return nm_obj_userdata(self);
pub fn userdata(self: anytype) ?*anyopaque {
return nm_obj_userdata(self.lvobj);
}
/// 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);
pub fn setUserdata(self: anytype, data: ?*const anyopaque) void {
nm_obj_set_userdata(self.lvobj, data);
}
/// 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: anytype, filter: LvEvent.Code, cb: LvEvent.Callback, udata: ?*anyopaque) *LvEvent.Descriptor {
return lv_obj_add_event_cb(self.lvobj, cb, filter, udata);
}
};
/// methods applicable to visible objects like labels, buttons and containers.
pub const WidgetMethods = struct {
/// sets object horizontal length.
pub fn setWidth(self: *LvObj, val: Coord) void {
lv_obj_set_width(self, val);
pub fn setWidth(self: anytype, val: Coord) void {
lv_obj_set_width(self.lvobj, val);
}
/// sets object vertical length.
pub fn setHeight(self: *LvObj, val: Coord) void {
lv_obj_set_height(self, val);
pub fn setHeight(self: anytype, val: Coord) void {
lv_obj_set_height(self.lvobj, val);
}
/// sets object height to its child contents.
pub fn setHeightToContent(self: *LvObj) void {
lv_obj_set_height(self, sizeContent);
pub fn setHeightToContent(self: anytype) void {
lv_obj_set_height(self.lvobj, sizeContent);
}
/// sets both width and height to 100%.
pub fn resizeToMax(self: *LvObj) void {
lv_obj_set_size(self, sizePercent(100), sizePercent(100));
pub fn resizeToMax(self: anytype) void {
lv_obj_set_size(self.lvobj, sizePercent(100), sizePercent(100));
}
/// selects which side to pad in setPad func.
pub const PadSelector = enum { all, 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 {
pub fn setPad(self: anytype, v: Coord, p: PadSelector, sel: LvStyle.Selector) void {
switch (p) {
.all => {
const vsel = sel.value();
lv_obj_set_style_pad_left(self, v, vsel);
lv_obj_set_style_pad_right(self, v, vsel);
lv_obj_set_style_pad_top(self, v, vsel);
lv_obj_set_style_pad_bottom(self, v, vsel);
lv_obj_set_style_pad_left(self.lvobj, v, vsel);
lv_obj_set_style_pad_right(self.lvobj, v, vsel);
lv_obj_set_style_pad_top(self.lvobj, v, vsel);
lv_obj_set_style_pad_bottom(self.lvobj, v, vsel);
},
.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()),
.left => lv_obj_set_style_pad_left(self.lvobj, v, sel.value()),
.right => lv_obj_set_style_pad_right(self.lvobj, v, sel.value()),
.top => lv_obj_set_style_pad_top(self.lvobj, v, sel.value()),
.bottom => lv_obj_set_style_pad_bottom(self.lvobj, v, sel.value()),
.row => lv_obj_set_style_pad_row(self.lvobj, v, sel.value()),
.column => lv_obj_set_style_pad_column(self.lvobj, 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 {
pub fn center(self: anytype) 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));
pub fn posAlign(self: anytype, a: PosAlign, xoffset: Coord, yoffset: Coord) void {
lv_obj_align(self.lvobj, @enumToInt(a), xoffset, yoffset);
}
/// 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));
pub fn flexGrow(self: anytype, val: u8) void {
lv_obj_set_flex_grow(self.lvobj, val);
}
/// 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());
pub fn addStyle(self: anytype, v: *LvStyle, sel: LvStyle.Selector) void {
lv_obj_add_style(self.lvobj, 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());
pub fn removeAllStyle(self: anytype) void {
const sel = LvStyle.Selector{ .part = .any, .state = .any };
lv_obj_remove_style(self.lvobj, 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());
pub fn removeBackgroundStyle(self: anytype) void {
const sel = LvStyle.Selector{ .part = .main, .state = .any };
lv_obj_remove_style(self.lvobj, 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 setBackgroundColor(self: anytype, v: Color, sel: LvStyle.Selector) void {
lv_obj_set_style_bg_color(self.lvobj, v, sel.value());
}
};
/// a base layer object which all the other UI elements are placed onto.
/// there can be only one active screen at a time on a display.
pub const Screen = struct {
lvobj: *LvObj,
/// sets the color of a text, typically a label object.
pub fn setTextColor(self: *LvObj, v: Color, sel: StyleSelector) void {
lv_obj_set_style_text_color(self, v, sel.value());
pub usingnamespace BaseObjMethods;
/// creates a new screen on default display.
/// the display must be already initialized.
pub fn new() !Screen {
const o = lv_obj_create(null) orelse return error.OutOfMemory;
return .{ .lvobj = o };
}
/// returns active screen of the default display.
pub fn active() !Screen {
const o = lv_disp_get_scr_act(null) orelse return error.NoDisplay;
return .{ .lvobj = o };
}
};
pub fn createObject(parent: *LvObj) !*LvObj {
return lv_obj_create(parent) orelse error.OutOfMemory;
/// makes a screen active.
pub fn load(scr: Screen) void {
lv_disp_load_scr(scr.obj);
}
};
/// used as a base parent for many other elements.
pub const Container = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
pub fn createFlexObject(parent: *LvObj, flow: FlexFlow) !*LvObj {
var o = try createObject(parent);
o.flexFlow(flow);
return o;
pub fn new(parent: anytype) !Container {
const o = lv_obj_create(parent.lvobj) orelse return error.OutOfMemory;
return .{ .lvobj = o };
}
pub fn createTopObject() !*LvObj {
/// creates a new container on the top level, above all others.
/// suitable for widgets like a popup window.
pub fn newTop() !Container {
const toplayer = lv_disp_get_layer_top(null);
return lv_obj_create(toplayer) orelse error.OutOfMemory;
return .{ .lvobj = toplayer };
}
/// applies flex layout to the container.
pub fn flex(self: Container, flow: FlexLayout.Flow, opt: FlexLayout.AlignOpt) FlexLayout {
return FlexLayout.adopt(self.lvobj, flow, opt);
}
};
/// same as Container but with flex layout of its children.
pub const FlexLayout = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
/// layout flow variations.
pub const Flow = 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,
};
/// flex layout alignments.
pub const Align = 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,
};
/// cross-direction alignment.
pub const AlignCross = enum(c.lv_flex_align_t) {
start = c.LV_FLEX_ALIGN_START,
end = c.LV_FLEX_ALIGN_END,
center = c.LV_FLEX_ALIGN_CENTER,
};
/// main, cross and track are similar to CSS flex concepts.
pub const AlignOpt = struct {
main: Align = .start,
cross: AlignCross = .start,
track: Align = .start,
all: ?Align = null, // overrides all 3 above
};
/// creates a new object with flex layout and some default padding.
pub fn new(parent: anytype, flow: Flow, opt: AlignOpt) !FlexLayout {
const obj = lv_obj_create(parent.lvobj) orelse return error.OutOfMemory;
// remove background style first. otherwise, it'll also remove flex.
const bgsel = LvStyle.Selector{ .part = .main, .state = .any };
lv_obj_remove_style(obj, null, bgsel.value());
const flex = adopt(obj, flow, opt);
flex.padColumnDefault();
return flex;
}
fn adopt(obj: *LvObj, flow: Flow, opt: AlignOpt) FlexLayout {
lv_obj_set_flex_flow(obj, @enumToInt(flow));
if (opt.all) |a| {
const v = @enumToInt(a);
lv_obj_set_flex_align(obj, v, v, v);
} else {
lv_obj_set_flex_align(obj, @enumToInt(opt.main), @enumToInt(opt.cross), @enumToInt(opt.track));
}
return .{ .lvobj = obj };
}
/// sets flex layout flow on the object.
pub fn setFlow(self: FlexLayout, ff: Flow) void {
lv_obj_set_flex_flow(self.lvobj, @enumToInt(ff));
}
/// sets flex layout alignments.
pub fn setAlign(self: FlexLayout, main: Align, cross: AlignCross, track: Align) void {
lv_obj_set_flex_align(self.lvobj, @enumToInt(main), @enumToInt(cross), @enumToInt(track));
}
/// same as setPad .column but using a default constant to make flex layouts consistent.
pub fn padColumnDefault(self: FlexLayout) void {
self.setPad(20, .column, .{});
}
};
pub const Window = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
pub fn new(parent: anytype, header_height: i16, title: [*:0]const u8) !Window {
const lv_win = lv_win_create(parent.lvobj, header_height) orelse return error.OutOfMemory;
if (lv_win_add_title(lv_win, title) == null) {
return error.OutOfMemory;
}
return .{ .lvobj = lv_win };
}
/// creates a new window on top of all other elements.
pub fn newTop(header_height: i16, title: [*:0]const u8) !Window {
return new(try Screen.active(), header_height, title);
}
/// returns content part of the window, i.e. the main part below title.
pub fn content(self: Window) Container {
return .{ .lvobj = lv_win_get_content(self.lvobj) };
}
};
/// a custom element consisting of a flex container with a title-sized label
/// on top.
pub const Card = struct {
lvobj: *LvObj,
title: Label,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
pub fn new(parent: anytype, title: [*:0]const u8) !Card {
const flex = (try Container.new(parent)).flex(.column, .{});
flex.setHeightToContent();
flex.setWidth(sizePercent(100));
const tl = try Label.new(flex, title, .{});
tl.addStyle(nm_style_title(), .{});
return .{ .lvobj = flex.lvobj, .title = tl };
}
};
/// represents lv_label_t in C, a text label.
pub const Label = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
pub const Opt = struct {
// LVGL defaults to .wrap
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
} = null,
pos: ?PosAlign = null,
recolor: bool = false,
};
/// the text value is copied into a heap-allocated alloc.
pub fn new(parent: anytype, text: [*:0]const u8, opt: Opt) !Label {
var lv_label = lv_label_create(parent.lvobj) orelse return error.OutOfMemory;
//lv_label_set_text_static(lb, text); // static doesn't work with .dot
lv_label_set_text(lv_label, text);
//lv_obj_set_height(lb, sizeContent); // default
if (opt.long_mode) |m| {
lv_label_set_long_mode(lv_label, @enumToInt(m));
}
if (opt.pos) |p| {
lv_obj_align(lv_label, @enumToInt(p), 0, 0);
}
if (opt.recolor) {
lv_label_set_recolor(lv_label, true);
}
return .{ .lvobj = lv_label };
}
/// formats label text using std.fmt.format and the provided buffer.
/// the text is heap-dup'ed so no need to retain the buf.
pub fn newFmt(parent: anytype, buf: []u8, comptime format: []const u8, args: anytype, opt: Opt) !Label {
const text = try std.fmt.bufPrintZ(buf, format, args);
return new(parent, text, opt);
}
/// sets label text to a new value.
/// previous value is dealloc'ed.
pub fn setText(self: Label, text: [*:0]const u8) void {
lv_label_set_text(self.lvobj, text);
}
/// formats a new label text.
/// the buffer can be dropped once the function returns.
pub fn setTextFmt(self: Label, buf: []u8, comptime format: []const u8, args: anytype) !void {
var s = try std.fmt.bufPrintZ(buf, format, args);
self.setText(s);
}
/// sets label text color.
pub fn setColor(self: Label, v: Color, sel: LvStyle.Selector) void {
lv_obj_set_style_text_color(self.lvobj, v, sel.value());
}
};
/// a button with a child Label.
pub const TextButton = struct {
lvobj: *LvObj,
label: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
pub fn new(parent: anytype, text: [*:0]const u8) !TextButton {
const btn = lv_btn_create(parent.lvobj) orelse return error.OutOfMemory;
const label = try Label.new(Container{ .lvobj = btn }, text, .{ .long_mode = .dot, .pos = .center });
return .{ .lvobj = btn, .label = label.lvobj };
}
};
pub const Spinner = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
pub fn new(parent: anytype) !Spinner {
const spin = lv_spinner_create(parent.lvobj, 1000, 60) orelse return error.OutOfMemory;
lv_obj_set_size(spin, 20, 20);
const ind: LvStyle.Selector = .{ .part = .indicator };
lv_obj_set_style_arc_width(spin, 4, ind.value());
return .{ .lvobj = spin };
}
};
/// represents lv_bar_t in C.
/// see https://docs.lvgl.io/8.3/widgets/core/bar.html for details.
pub const Bar = struct {
lvobj: *LvObj,
pub usingnamespace BaseObjMethods;
pub usingnamespace WidgetMethods;
/// creates a horizontal bar with the default 0-100 range.
pub fn new(parent: anytype) !Bar {
const lv_bar = lv_bar_create(parent.lvobj) orelse return error.OutOfMemory;
lv_obj_set_size(lv_bar, 200, 20);
return .{ .lvobj = lv_bar };
}
/// sets a new value of a bar current position.
pub fn setValue(self: Bar, val: i32) void {
lv_bar_set_value(self.lvobj, val, c.LV_ANIM_OFF);
}
/// sets minimum and max values of the bar range.
pub fn setRange(self: Bar, min: i32, max: i32) void {
lv_bar_set_range(self.lvobj, min, max);
}
};
/// represents lv_obj_t type in C.
pub const LvObj = opaque {
/// feature-flags controlling object's behavior.
/// OR'ed values are possible.
pub const ObjFlag = enum(c.lv_obj_flag_t) {
pub const Flag = 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
@ -521,6 +768,7 @@ pub const ObjFlag = enum(c.lv_obj_flag_t) {
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.
@ -614,112 +862,14 @@ pub const PosAlign = enum(c.lv_align_t) {
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 };
}
pub const Window = struct {
winobj: *LvObj,
pub fn content(self: Window) *LvObj {
return lv_win_get_content(self.winobj);
}
};
pub const CreateLabelOpt = struct {
// LVGL defaults to .wrap
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
} = null,
pos: ?PosAlign = null,
};
/// creates a new label object.
/// the text is heap-duplicated for the lifetime of the object and free'ed automatically.
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
if (opt.long_mode) |m| {
lv_label_set_long_mode(lb, @enumToInt(m));
}
if (opt.pos) |p| {
lb.posAlign(p, 0, 0);
}
return lb;
}
/// formats label text using std.fmt.format and the provided buffer.
/// a label object is then created with the resulting text using createLabel.
/// the text is heap-dup'ed so no need to retain buf. see createLabel.
pub fn createLabelFmt(parent: *LvObj, buf: []u8, comptime format: []const u8, args: anytype, opt: CreateLabelOpt) !*LvObj {
const text = try std.fmt.bufPrintZ(buf, format, args);
return createLabel(parent, text, opt);
}
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 = .center });
return btn;
}
/// creates a spinner object with hardcoded dimensions and animation speed
/// used througout the GUI.
pub fn createSpinner(parent: *LvObj) !*LvObj {
const spin = lv_spinner_create(parent, 1000, 60) orelse return error.OutOfMemory;
lv_obj_set_size(spin, 20, 20);
const ind: StyleSelector = .{ .part = .indicator };
lv_obj_set_style_arc_width(spin, 4, ind.value());
return spin;
}
// ==========================================================================
// 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
/// returns a title style with a larger font.
pub extern fn nm_style_title() *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;
@ -744,17 +894,17 @@ extern fn lv_indev_get_next(indev: ?*LvIndev) ?*LvIndev;
/// 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_create(callback: LvTimer.Callback, 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_code(e: *LvEvent) LvEvent.Code;
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;
extern fn lv_obj_add_event_cb(obj: *LvObj, cb: LvEvent.Callback, filter: LvEvent.Code, userdata: ?*anyopaque) *LvEvent.Descriptor;
// display and screen functions ----------------------------------------------
@ -842,6 +992,10 @@ extern fn lv_label_set_recolor(label: *LvObj, enable: bool) void;
extern fn lv_spinner_create(parent: *LvObj, speed_ms: u32, arc_deg: u32) ?*LvObj;
extern fn lv_bar_create(parent: *LvObj) ?*LvObj;
extern fn lv_bar_set_value(bar: *LvObj, value: i32, c.lv_anim_enable_t) void;
extern fn lv_bar_set_range(bar: *LvObj, min: i32, max: i32) 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;

@ -56,7 +56,7 @@ pub fn updateStatus(report: comm.Message.PoweroffProgress) !void {
all_stopped = all_stopped and sv.stopped;
}
if (all_stopped) {
win.status.setLabelText("powering off ...");
win.status.setText("powering off ...");
}
} else {
return error.NoProgressWindow;
@ -67,8 +67,8 @@ pub fn updateStatus(report: comm.Message.PoweroffProgress) !void {
/// the device turns off.
const ProgressWin = struct {
win: lvgl.Window,
status: *lvgl.LvObj, // text status label
svcont: *lvgl.LvObj, // services container
status: lvgl.Label, // text status
svcont: lvgl.FlexLayout, // services container
/// symbol width next to the service name. this aligns all service names vertically.
/// has to be wide enough to accomodate the spinner, but not too wide
@ -76,21 +76,18 @@ const ProgressWin = struct {
const sym_width = 20;
fn create() !ProgressWin {
const win = try lvgl.createWindow(null, 60, " " ++ symbol.Power ++ " SHUTDOWN");
errdefer win.winobj.destroy(); // also deletes all children created below
const wincont = win.content();
wincont.flexFlow(.column);
const win = try lvgl.Window.newTop(60, " " ++ symbol.Power ++ " SHUTDOWN");
errdefer win.destroy(); // also deletes all children created below
const wincont = win.content().flex(.column, .{});
// initial status message
const status = try lvgl.createLabel(wincont, "shutting down services. it may take up to a few minutes.", .{});
const status = try lvgl.Label.new(wincont, "shutting down services. it may take up to a few minutes.", .{});
status.setWidth(lvgl.sizePercent(100));
// prepare a container for services status
const svcont = try lvgl.createObject(wincont);
svcont.removeBackgroundStyle();
svcont.flexFlow(.column);
svcont.flexGrow(1);
svcont.padColumnDefault();
const svcont = try lvgl.FlexLayout.new(wincont, .column, .{});
svcont.setWidth(lvgl.sizePercent(100));
svcont.flexGrow(1);
return .{
.win = win,
@ -104,32 +101,28 @@ const ProgressWin = struct {
}
fn addServiceStatus(self: ProgressWin, name: []const u8, stopped: bool, err: ?[]const u8) !void {
const row = try lvgl.createObject(self.svcont);
row.removeBackgroundStyle();
row.flexFlow(.row);
row.flexAlign(.center, .center, .center);
row.padColumnDefault();
const row = try lvgl.FlexLayout.new(self.svcont, .row, .{ .all = .center });
row.setPad(10, .all, .{});
row.setWidth(lvgl.sizePercent(100));
row.setHeightToContent();
var buf: [100]u8 = undefined;
if (err) |e| {
const sym = try lvgl.createLabelFmt(row, &buf, symbol.Warning, .{}, .{ .long_mode = .clip });
const sym = try lvgl.Label.newFmt(row, &buf, symbol.Warning, .{}, .{ .long_mode = .clip });
sym.setWidth(sym_width);
sym.setTextColor(lvgl.paletteMain(.red), .{});
const lb = try lvgl.createLabelFmt(row, &buf, "{s}: {s}", .{ name, e }, .{ .long_mode = .dot });
lb.setTextColor(lvgl.paletteMain(.red), .{});
sym.setColor(lvgl.Palette.main(.red), .{});
const lb = try lvgl.Label.newFmt(row, &buf, "{s}: {s}", .{ name, e }, .{ .long_mode = .dot });
lb.setColor(lvgl.Palette.main(.red), .{});
lb.flexGrow(1);
} else if (stopped) {
const sym = try lvgl.createLabelFmt(row, &buf, symbol.Ok, .{}, .{ .long_mode = .clip });
const sym = try lvgl.Label.newFmt(row, &buf, symbol.Ok, .{}, .{ .long_mode = .clip });
sym.setWidth(sym_width);
const lb = try lvgl.createLabelFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot });
const lb = try lvgl.Label.newFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot });
lb.flexGrow(1);
} else {
const spin = try lvgl.createSpinner(row);
const spin = try lvgl.Spinner.new(row);
spin.setWidth(sym_width);
const lb = try lvgl.createLabelFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot });
const lb = try lvgl.Label.newFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot });
lb.flexGrow(1);
}
}

@ -27,18 +27,15 @@ pub fn init() !void {
}
export fn nm_create_info_panel(parent: *lvgl.LvObj) c_int {
createInfoPanel(parent) catch |err| {
createInfoPanel(lvgl.Container{ .lvobj = parent }) catch |err| {
logger.err("createInfoPanel: {any}", .{err});
return -1;
};
return 0;
}
fn createInfoPanel(parent: *lvgl.LvObj) !void {
parent.flexFlow(.column);
parent.flexAlign(.start, .start, .start);
fn createInfoPanel(cont: lvgl.Container) !void {
const flex = cont.flex(.column, .{});
var buf: [100]u8 = undefined;
const sver = try std.fmt.bufPrintZ(&buf, "GUI version: {any}", .{buildopts.semver});
_ = try lvgl.createLabel(parent, sver, .{});
_ = try lvgl.Label.newFmt(flex, &buf, "GUI version: {any}", .{buildopts.semver}, .{});
}

@ -10,31 +10,30 @@ const logger = std.log.scoped(.ui);
/// 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
// https://ziglang.org/documentation/master/#Static-Local-Variables
const S = struct {
var lv_obj: ?*lvgl.LvObj = null;
var top: ?lvgl.Container = null;
};
switch (onoff) {
.show => {
if (S.lv_obj != null) {
if (S.top != null) {
return;
}
const o = lvgl.createTopObject() catch |err| {
logger.err("topdrop: lvgl.createTopObject: {any}", .{err});
const top = lvgl.Container.newTop() catch |err| {
logger.err("topdrop: lvgl.Container.newTop: {any}", .{err});
return;
};
o.setFlag(.on, .ignore_layout);
o.resizeToMax();
o.setBackgroundColor(lvgl.Black, .{});
S.lv_obj = o;
lvgl.displayRedraw();
top.setFlag(.ignore_layout);
top.resizeToMax();
top.setBackgroundColor(lvgl.Black, .{});
S.top = top;
lvgl.redraw();
},
.remove => {
if (S.lv_obj) |o| {
o.destroy();
S.lv_obj = null;
if (S.top) |top| {
top.destroy();
S.top = null;
}
},
}
@ -53,51 +52,46 @@ pub const ModalButtonCallbackFn = *const fn (index: usize) void;
///
/// note: the cb callback must have @alignOf(ModalbuttonCallbackFn) alignment.
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(cb);
const win = try lvgl.Window.newTop(60, title);
errdefer win.destroy(); // also deletes all children created below
win.setUserdata(cb);
const wincont = win.content();
wincont.flexFlow(.column);
wincont.flexAlign(.start, .center, .center);
const msg = try lvgl.createLabel(wincont, text, .{ .pos = .center });
msg.setWidth(lvgl.displayHoriz() - 100);
const wincont = win.content().flex(.column, .{ .cross = .center, .track = .center });
const msg = try lvgl.Label.new(wincont, text, .{ .pos = .center });
msg.setWidth(lvgl.LvDisp.horiz() - 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);
// buttons container
const btncont = try lvgl.FlexLayout.new(wincont, .row, .{ .all = .center });
btncont.setWidth(lvgl.LvDisp.horiz() - 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);
const btn = try lvgl.TextButton.new(btncont, btext);
btn.setFlag(.event_bubble);
btn.setFlag(.user1); // .user1 indicates actionable button in callback
btn.setUserdata(@intToPtr(?*anyopaque, i)); // button index in callback
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);
_ = btncont.on(.click, nm_modal_callback, win.lvobj);
}
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
if (e.userdata()) |edata| {
const target = lvgl.Container{ .lvobj = e.target() }; // type doesn't really matter
if (!target.hasFlag(.user1)) { // .user1 is set in modal setup
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();
const win = lvgl.Window{ .lvobj = @ptrCast(*lvgl.LvObj, edata) };
const cb = @ptrCast(ModalButtonCallbackFn, @alignCast(@alignOf(ModalButtonCallbackFn), win.userdata()));
win.destroy();
cb(btn_index);
}
}