feat(color,Lua): Lvgl support for Lua scripts
As discussed in #4627
Sample scripts attached.
BattAnLvgl
- Rewrite of the BattAnalog script. This will work either with this PR or without. It demonstrates a simple pattern where the UI logic is split and the correct version is loaded when the script is run.
BattAnLvgl.zip
Lvgl
- Simple test widget demonstrating various features.
Lvgl.zip
LvglMeter
- Test widget demonstrating the meter Lvgl object.
LvglMeter.zip
Lvgl Test Tool
- Test tool script demonstrating using the EdgeTX styled UI elements (buttons, toggle switch, edit box, etc).
Lvgl Test Tool.lua.zip
Here is a summary of the API as it currently stands (20th Apr).
The Lvgl system is accessed through ‘lvgl’ object in Lua. A Widget must set ‘useLvgl=true’ in the table returned when the widget is first loaded to enable the Lvgl API.
return { name="Lvgl Test", options=opts, create=create, update=update, refresh=refresh, background=background, useLvgl=true }
To clear the screen and delete all existng Lvgl objects call:
lvgl.clear()
This should be done if the layout changes (e.g. going to/from full screen).
When creating objects some of the properties can be either a static value, or a getter function. If a getter function is supplied the property will be updated whenever the getter function returns a different value.
Creating new Lvgl objects:
- All functions to create a new object expect a single table parameter with the relevant properties
- General parameters (all functions):
- x - x position (relative to top left of widget or parent)
- y - y position (relative to top left of widget or parent)
- color - main color. Can be a color value or a getter function.
- pos - getter function called by object to update position (must return two integer values - x, y)
- size - getter function called by object to update size (must return two integer values - w, h)
- visible - getter function called by object to update visibility (must return a boolean - true = visible, false = hidden)
- Functions:
- label - lvgl.label({parameter table})
- Specific parameters:
- text - string to show on the label. Can be a static string or a getter function.
- w - optional width to set (required if RIGHT justified text is used)
- h - optional height to set
- font - text font
- Specific parameters:
- rectangle - lvgl.rectangle({parameter table})
- Specific parameters:
- w - width
- h - height
- thickness - outline thickness
- filled - true if filled rectangle is required (thickness is not used in this case)
- rounded - controls corner rounding, 0 - no rounding, > 0 applies rounding
- Specific parameters:
- circle - lvgl.circle({parameter table})
- x,y position is the centre of the circle
- Specific parameters:
- radius - radius. Can be a static value or a getter function.
- thickness - outline thickness
- filled - true if filled circle is required (thickness is not used in this case)
- arc - lvgl.arc({parameter table})
- x,y position is the centre of the arc
- Specific parameters:
- radius - radius. Can be a static value or a getter function.
- thickness - outline thickness
- startAngle - start angle for arc 0 - 360 (0 is up). Can be a static value or a getter function.
- endAngle - end angle for arc 0 - 360. Can be a static value or a getter function.
- image - lvgl.image({parameter table})
- The image will be centered in frame defined by x,y,w,h. If the 'fill' property is false the image will be scaled to fit fully within the frame. If the image aspect ratio is not the same as the frame there will be transparent borders as needed to centre the image. If the 'fill' property is true the image will be scaled to fill the frame. The scaled image may be cropped if the aspect ratio does not match the frame.
- Specific parameters:
- file - filename (including path) of file to display
- fill - false to fit image to frame, true to fill frame with image.
- label - lvgl.label({parameter table})
- General parameters (all functions):
Creating new objects using styled EdgeTx controls (only available for standalone scripts):
- All functions to create a new object expect a single table parameter with the relevant properties
- General parameters (all functions):
- x - x position (relative to top left of widget or parent)
- y - y position (relative to top left of widget or parent)
- pos - getter function called by object to update position (must return two integer values - x, y)
- size - getter function called by object to update size (must return two integer values - w, h)
- visible - getter function called by object to update visibility (must return a boolean - true = visible, false = hidden)
- Functions:
- button - lvgl.button({parameter table})
- Specific parameters:
- text - initial string to show on the label
- w - optional width to set
- press - function to call when button is pressed
- Specific parameters:
- toggle switch - lvgl.toggle({parameter table})
- Specific parameters:
- get - function to call to get state of toggle switch (return 0 = off, 1 = on)
- set - function to call when toggle switch state is changed by user (has a single parameter 0 = off, 1 = on)
- Specific parameters:
- slider - lvgl.slider({parameter table})
Note: 'h' property is ignored
- Specific parameters:
- min - minimum (left) value for slider
- max - maximum (right) value for slider
- get - function to call to get the value for the slider position (must be between min and max)
- set - function to call when value is changed by user (has a single parameter of the new value)
- Specific parameters:
- confirmation dialog - lvgl.confirm({parameter table})
- Specific parameters:
- title - popup dialog title
- message - popup dialog message
- confirm - function to call when user selects 'Yes' button on dialog
- cancel - function to call when user selects 'No' button on dialog
- Specific parameters:
- text entry - lvgl.textEdit({‘parameter table})
- Specific parameters:
- maxLen - maximum allowed length of text that can be entered
- value - initial value of the text entry field
- set - function to call when text has been edited by the user (has a single parameter with the new string)
- Specific parameters:
- number entry - lvgl.numberEdit({parameter table})
- Specific parameters:
- min - minimum allowed value
- max - maximum allowed value
- get - function to call to get value to be edited (must return an integer)
- set - function to call when the value is edited by the user (has a single integer parameter with the new value)
- display - function to call to format the way the value is displayed (has a single parameter with the value to display, must return a string with the formatted value)
- Specific parameters:
- popup menu - lvgl.choice({parameter table})
- Specific parameters:
- title - title string for the popup menu
- values - table of string values that can be selected by the user
- get - function to call to get current selected option (must return an integer from 1 to size of values table)
- set - function to call when the user selects an option (has a single parameter with the selected index from 1 to the size of the values table)
- Specific parameters:
- button - lvgl.button({parameter table})
- General parameters (all functions):
All functions return a reference to the Lvgl object that can be used to set attributes later.
local uiLabel = lvgl.label({text=“Hello World!”, x=10, y=10, color=WHITE, font=DBLSIZE})
It is not necessary to save the return value unless you want to change attributes. Object lifetime is managed by the system.
Setting attributes: Attribute values can be updated using the 'set' function. The 'set' function takes a table parameter with the same properties as used when creating the object. Note: not all properties can be changed. The 'set' function can be called via ‘lvgl’ or using the saved reference
local uiLabel = lvgl.label({text=“Hello World!”, x=10, y=10, color=WHITE, font=DBLSIZE})
lvgl.set(uiLabel, {text=“New string”})
uiLabel:set({text=“New string”})
Setting state: UI elements can be hidden and shown as needed using ‘show’ and ‘hide’ functions E.G.:
lvgl.hide(uiLabel)
uiLabel:show()
Grouping ui elements:
- By default the parent of all UI elements is the widget itself
- UI elements can contain other elements. This can be used to show/hide/move a group of objects by simply updating the parent.
- To create a nested group of objects:
-
Create the parent, call lvgl.setParent(parent), create children, call lvgl.setParent(nil)
E.G.
local box = lvgl.rectangle({x=10, y=10, w=50, h=50, thickness=1, color=WHITE})
lvgl.setParent(box)
local lbl = lvgl.label({text=“BOX”, x=10, y=10})
lvgl.setParent(nil)
Creating UI elements in bulk: It is possible to create a complex UI layout by defining all the elements in a Lua table, then calling lvgl.build() to create the entire layout in one operation. E.G. create the layout with a nested table structure:
local lyt = {
{type="label", text="CTR", x=0, y=0, color=wgt.opts.Color, font=MIDSIZE},
{type="label", text="TMR", x=0, y=50, color=wgt.opts.Color, font=MIDSIZE},
{type="label", x=80, y=0, color=wgt.opts.Color, font=MIDSIZE, name="ctr"},
{type="label", x=120, y=0, color=wgt.opts.Color, font=MIDSIZE, name="bgctr"},
{type="label", x=80, y=50, color=wgt.opts.Color, font=MIDSIZE, name="tmr"},
{type="rectangle", x=50, y=80, w=30, h=30, thickness=4, color=COLOR_THEME_ACTIVE, name="box1"},
{type="filledRectangle", x=90, y=80, w=30, h=30, color=COLOR_THEME_EDIT, name="box2", children={
{type="circle", x=15, y=15, radius=13, thickness=2, color=WHITE, children={
{type="label", text="0", x=7, y=1, name="nctr"},
}},
}},
{type="circle", x=65, y=135, radius=15, thickness=4, color=COLOR_THEME_ACTIVE, name="cir1"},
{type="filledCircle", x=105, y=135, radius=15, color=COLOR_THEME_EDIT, name="cir2"},
{type="arc", x=15, y=95, radius=15, thickness=4, startAngle=0, endAngle=210, color=COLOR_THEME_WARNING, name="arc"},
}
Elements can be nested and grouped using the ‘children’ property. Then build the UI elements, lvgl.build returns a table containing the references to all the named elements in the layout. Only named elements can be accessed.
local uiElements = lgvl.build(lyt)
The named elements can be updated later:
uiElements[“ctr”]:set({text=“New string”})
You can also copy the UI element references into other variables to make updating easier
local ctr = uiElements[“ctr”]
ctr:set({color=COLOR_THEME_PRIMARY2})
Added the 'image' widget object to display bitmap images.
Is the proposed API for this stable enough to merge now? I see there hasn't been any discussion on this since you proposed it, so perhaps basically just merge and see what happens? The better performance for widgets and Lua if re-implemented using LVGL directly should be pretty tempting, as well as for things like real input fields and native style controls.
I'd prefer to hold this one until after 5031 is merged if that's ok.
No problem... I'll look at #4933 next instead (which shouldn't conflict since B&W), and then #5031 (probably tomorrow afternoon).
Can you check for the "Lvgl Test Tool" - the last/standalone test script - whether there is an issue with the script or the LVGL input field objects... the number input one seems to be able to scroll a bit, and the text input one is too small.
Exiting via Long RTN or the "Back" button gave me a error when trying to run the script a second time, even after restarting radio. Goes away if I delete the .luac compiled file.
This is otherwise looking great! :)
Fixed the number and text input boxes.
I can't reproduce the syntax error.
Did you try on radio, or simulator only? As it seems this only occurs on radio (only tried TX16S atm, will try T15 shortly also) ;) Still present, and interestingly can interact with the lua still via hard keys, so can exit it via the onscreen button 🤣
Can you try this version of the script. It work on my TX16S without any errors. Lvgl Test Tool.lua.zip
Yup, that version is working perfectly... thanks! :) Nice addition... slider demo as well! :)