Themes

This section describes how to use themes to change the appearance of Tangara's UI.

Tangara's firmware includes a few built-in themes. These include a light mode theme, a dark mode theme, and a high contrast theme. To select a new theme, navigate to Settings -> Theme and select the theme to use from the dropdown.

Screenshots showing the main menu with the settings icon selected, the settings menu with the 'Theme' item selected, and the screen showing a dropdown to select a theme

In addition to the built-in themes, custom themes can be loaded from a /.themes directory on the SD card. Any Lua files placed in this directory will be expected to return a valid theme, so if you have any additional Lua scripts that the theme requires, please place them in another folder (e.g. /.themes/scripts).

About theme files

A theme file is a Lua script returning a table that is used to style different elements of the UI. This table should contain key-value pairs where the keys are 'subjects' and the values are arrays of pairs of LVGL selectors and styles.

A very basic example of a valid theme is as follows:

local lvgl = require("lvgl")

return {
    base = {
        {lvgl.PART.MAIN, lvgl.Style {
            text_color = "#FF0000",
        }},
    },
}

Firstly, we must require the LVGL bindings in order to define lvgl.Style and the LVGL selector constants that the theme returns.

We then return a theme that applies to only one 'subject', base, which is a subject that applies to all elements. We apply one style to this subject, using the selector lvgl.PART.MAIN. So all together, we have 'styles' that are applied using 'selectors' to certain 'subjects'.

The above theme will set the text color on all objects to red, leaving everything else unstyled. Without any other styling, it can be hard to know what element is focused, so we can adapt this example to color the text of any focused objects green:

local lvgl = require("lvgl")

return {
    base = {
        {lvgl.PART.MAIN, lvgl.Style {
            text_color = "#FF0000",
        }},
        {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
            text_color = "#00FF00", 
        }},
    },
}

Now we're returning two styles, both of which are applied to the base subject, but with different selectors for each style. The first style will still be applied to all objects, but now, if the element is focused, the second style will be applied as well.

Most likely, you'll want to apply different styles to different kinds of elements on the screen. This is where the different subjects come in. We can, for example, extend this theme so that every Button has a blue background, by providing a subject with the name button.

local lvgl = require("lvgl")

return {
    base = {
        {lvgl.PART.MAIN, lvgl.Style {
            text_color = "#FF0000",
        }},
        {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
            text_color = "#00FF00", 
        }},
    },
    button = {
        {lvgl.PART.MAIN, lvgl.Style {
            bg_color = "#0000FF",
        }},
    },
}

Subjects

In Built Subjects

Here is a non-exhaustive list of some common subjects used to style various parts of Tangara's UI

  • base - Applied to all objects

  • root - Applied to the root object of every screen

  • button - Applied to all lv_button objects

  • listbutton - Applied to all lv_list_button objects

  • bar - Applied to all lv_bar objects

  • slider - Applied to all lv_slider objects

  • switch - Applied to all lv_switch objects

  • dropdown - Applied to all lv_dropdown objects

  • dropdownlist - Applied to all lv_dropdown_list objects

  • header - Applied to the menubar on some screens

  • popup - Applied to pop-up windows (such as the volume pop-up)

  • scrubber - Applied to the track position scrubber

  • database_indicator - Applied to the database indexing indicator in the top bar

  • bluetooth_icon - Applied to the bluetooth icon in the top bar

  • battery - Applied to the battery icon for all battery states

  • battery_charging - Applied to the battery icon when charging

  • battery_0 - Applied to the battery icon when battery is at <20% (see also battery_20, battery_40, battery_60, battery_80 and battery_100 )

  • battery_charge_icon - Applied to the charge overlay on the battery icon

  • icon_enabled - Applied to some icons (such as those on buttons) that can be interacted with

  • icon_disabled - Applied to some icons (such as those on buttons) that are disabled

  • now_playing - Applied to the "now playing" widget on the main menu

  • menu_icon - Applied to main menu icons (such as the settings icon, or file browser icon)

Custom Subjects

In addition to the existing subjects, if you have a custom UI element or would like to style an existing element that doesn't have a subject, you can set the subject of an object by using theme.set_subject(object, subject).

For example:

-- ...
-- Create a new LVGL button and associate it with a subject
local theme = require("theme")
local my_special_button = lvgl.Button()
theme.set_subject(my_special_button, "my_special_subject")
-- ...

-- A theme could then define styles for that subject
local my_theme = {
    my_special_subject = {
        {lvgl.PART.MAIN, lvgl.Style {
            -- ...
        }},
    }
}

You can also use set_subject to apply the default styling of a particular kind of UI element to another kind of UI element:

-- Create a new LVGL label, but style it like a button
local theme = require("theme")
local cool_fake_button = lvgl.Label()
theme.set_subject(cool_fake_button, "button")

Selectors

Selectors are a bitmask representing combinations of LVGL Parts and States.

Most objects can be styled entirely through their lvgl.PART.MAIN part, however more complex UI elements such as switches may have additional parts such as lvgl.PART.INDICATOR or lvgl.PART.KNOB.

Styles

The lvgl.Style type used in theme files is off of LVGL's Styles. Most LVGL Style properties should be supported. You can find a full list of these properties here.

Examples

Set a custom background image

We can also set background images using themes. To load an image for a theme, we need to use lvgl.ImgData and give it the filepath of the image. Images on the SD card will need to be prefixed with "//sd/" in order for Lua to find them.

{
    root = {
        {lvgl.PART.MAIN, lvgl.Style {
            bg_opa = lvgl.OPA(100), -- Make sure the background is opaque
            bg_image_src = lvgl.ImgData("//sd/wallpaper.png"),
        }},
    },
}