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. // can happen with a fresh connection while dhcp is still in progress.
if (report.wifi_ssid != null and report.ipaddrs.len == 0) { 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) // 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); t.setRepeatCount(1);
} else |err| { } else |err| {
logger.err("network status timer failed: {any}", .{err}); logger.err("network status timer failed: {any}", .{err});
@ -293,8 +293,8 @@ pub fn main() anyerror!void {
// run idle timer indefinitely. // run idle timer indefinitely.
// continue on failure: screen standby won't work at the worst. // continue on failure: screen standby won't work at the worst.
_ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| { _ = lvgl.LvTimer.new(nm_check_idle_time, 2000, null) catch |err| {
logger.err("lvgl.CreateTimer(idle check): {any}", .{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; return &style_btn_red;
} }
extern lv_style_t *nm_style_title()
{
return &style_title;
}
static void textarea_event_cb(lv_event_t *e) static void textarea_event_cb(lv_event_t *e)
{ {
lv_obj_t *textarea = lv_event_get_target(e); lv_obj_t *textarea = lv_event_get_target(e);

File diff suppressed because it is too large Load Diff

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

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

@ -10,31 +10,30 @@ const logger = std.log.scoped(.ui);
/// unsafe for concurrent use. /// unsafe for concurrent use.
pub fn topdrop(onoff: enum { show, remove }) void { pub fn topdrop(onoff: enum { show, remove }) void {
// a static construct: there can be only one global topdrop. // 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 { const S = struct {
var lv_obj: ?*lvgl.LvObj = null; var top: ?lvgl.Container = null;
}; };
switch (onoff) { switch (onoff) {
.show => { .show => {
if (S.lv_obj != null) { if (S.top != null) {
return; return;
} }
const o = lvgl.createTopObject() catch |err| { const top = lvgl.Container.newTop() catch |err| {
logger.err("topdrop: lvgl.createTopObject: {any}", .{err}); logger.err("topdrop: lvgl.Container.newTop: {any}", .{err});
return; return;
}; };
o.setFlag(.on, .ignore_layout); top.setFlag(.ignore_layout);
o.resizeToMax(); top.resizeToMax();
o.setBackgroundColor(lvgl.Black, .{}); top.setBackgroundColor(lvgl.Black, .{});
S.top = top;
S.lv_obj = o; lvgl.redraw();
lvgl.displayRedraw();
}, },
.remove => { .remove => {
if (S.lv_obj) |o| { if (S.top) |top| {
o.destroy(); top.destroy();
S.lv_obj = null; S.top = null;
} }
}, },
} }
@ -53,51 +52,46 @@ pub const ModalButtonCallbackFn = *const fn (index: usize) void;
/// ///
/// note: the cb callback must have @alignOf(ModalbuttonCallbackFn) alignment. /// 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 { 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); const win = try lvgl.Window.newTop(60, title);
errdefer win.winobj.destroy(); // also deletes all children created below errdefer win.destroy(); // also deletes all children created below
win.winobj.setUserdata(cb); win.setUserdata(cb);
const wincont = win.content(); const wincont = win.content().flex(.column, .{ .cross = .center, .track = .center });
wincont.flexFlow(.column); const msg = try lvgl.Label.new(wincont, text, .{ .pos = .center });
wincont.flexAlign(.start, .center, .center); msg.setWidth(lvgl.LvDisp.horiz() - 100);
const msg = try lvgl.createLabel(wincont, text, .{ .pos = .center });
msg.setWidth(lvgl.displayHoriz() - 100);
msg.flexGrow(1); msg.flexGrow(1);
const btncont = try lvgl.createFlexObject(wincont, .row); // buttons container
btncont.removeBackgroundStyle(); const btncont = try lvgl.FlexLayout.new(wincont, .row, .{ .all = .center });
btncont.padColumnDefault(); btncont.setWidth(lvgl.LvDisp.horiz() - 40);
btncont.flexAlign(.center, .center, .center);
btncont.setWidth(lvgl.displayHoriz() - 40);
btncont.setHeightToContent(); btncont.setHeightToContent();
// leave 5% as an extra spacing. // leave 5% as an extra spacing.
const btnwidth = lvgl.sizePercent(try std.math.divFloor(i16, 95, @truncate(u8, btns.len))); const btnwidth = lvgl.sizePercent(try std.math.divFloor(i16, 95, @truncate(u8, btns.len)));
for (btns) |btext, i| { 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.setWidth(btnwidth);
btn.setFlag(.on, .event_bubble);
btn.setFlag(.on, .user1); // .user1 indicates actionable button in callback
if (i == 0) { if (i == 0) {
btn.addStyle(lvgl.nm_style_btn_red(), .{}); 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 { export fn nm_modal_callback(e: *lvgl.LvEvent) void {
if (e.userdata()) |event_data| { if (e.userdata()) |edata| {
const target = e.target(); const target = lvgl.Container{ .lvobj = e.target() }; // type doesn't really matter
if (!target.hasFlag(.user1)) { // .user1 is set by modal fn if (!target.hasFlag(.user1)) { // .user1 is set in modal setup
return; return;
} }
const btn_index = @ptrToInt(target.userdata()); const btn_index = @ptrToInt(target.userdata());
const winobj = @ptrCast(*lvgl.LvObj, event_data); const win = lvgl.Window{ .lvobj = @ptrCast(*lvgl.LvObj, edata) };
// guaranteed to be aligned due to cb arg in modal fn. const cb = @ptrCast(ModalButtonCallbackFn, @alignCast(@alignOf(ModalButtonCallbackFn), win.userdata()));
const cb = @ptrCast(ModalButtonCallbackFn, @alignCast(@alignOf(ModalButtonCallbackFn), winobj.userdata())); win.destroy();
winobj.destroy();
cb(btn_index); cb(btn_index);
} }
} }