Support for generating themes (help offered)

A friend and I were recently talking about implementing/improving theme support for LVGL in our project.

My plan would be to create a CSS-like syntax and generate C code from that. The guideline for this would be Less CSS

Example syntax:

@background-color: #428bca;
@shadow-color: #202020;

btn {
  radius: 16dp;
  bgColor: @background-color;
  shadowColor: @shadow-color;
}

The btn class would refer to lv_style_t btn such as what you can find in lv_theme_default.c
Property names would map to relevant lv_style_set_ functions: bgColor would map to lv_style_set_bg_color()

The parser would:

  • Parse the raw input and convert it to tokens (tokenization)
  • Convert tokens to data structures
  • Convert data structures to C code

The parser would be written in Python because the existing LVGL scripts are largely written in Python and because it’s available on most platforms.

We’re likely going to make this regardless whether it will become an official LVGL feature, but I’d like to have some input on this proposal.

(We’ll name it “LSS”, spoken as “less”. It’s tongue-in-cheek towards “less css” and stands for “LVGL Style Sheets”)

1 Like

Hey,

It sounds interesting! Just to make sure: the goal it generate C files from lss, right?

What is the actual problem that you want to solve?

  • describe complex style hierarchies?
  • allow writing styles faster?
  • something else?

It sounds interesting! Just to make sure: the goal it generate C files from lss, right?

Yep

What is the actual problem that you want to solve?

To make it easier/faster to make a theme. And to make it easier to maintain them as new LVGL versions come along.

I started something here:

It’s got a proper EBNF grammar and related parser. There is a simple templating engine for C and header files.

Can you also add an example lss file and result C file?

Note that this is very much work in progress :sweat_smile:

LSS:

// Colors
@widget-bg-color: #525252;
@widget-fg-color: #c4c4c4;

// Opacity
@opa-cover: 255;

scr {
    bgOpa: @opa-cover;
}

scrollbar {
  bgColor: @widget-bg-color;
  padAll: 7;
  width: 5;
  opa: 40;
}

scrollbar: scrolled {
  opa: 255;
}

button {
  bgOpa: 255;
  bgColor: @background-color;
  shadowColor: @shadow-color;
}

theme {
  name: "test";
}

Code template:

#define LV_USE_PRIVATE_API 1 // for _lv_theme_t

#include "theme.h"
#include <lvgl.h>

#ifdef __cplusplus
extern "C" {
#endif

{{DEFINE_CONSTANTS}}

typedef theme_{{THEME_NAME_LOWER}}_styles theme_t;

typedef struct {
{{STYLE_DECLARATIONS}}
} theme_t;

static void style_init(theme_t* theme) {
{{STYLE_INIT}}
}

lv_theme_t* lv_theme_{{THEME_NAME_LOWER}}_init(
    lv_display_t* disp,
    const lv_font_t* font
) {
    {{INIT_FUNCTION_BODY}}
}

#ifdef __cplusplus
extern "}"
#endif

Generated code:

#define LV_USE_PRIVATE_API 1 // for _lv_theme_t

#include "theme.h"
#include <lvgl.h>

#ifdef __cplusplus
extern "C" {
#endif

#define WIDGET_BG_COLOR lv_color_make(0x52, 0x52, 0x52)
#define WIDGET_FG_COLOR lv_color_make(0xc4, 0xc4, 0xc4)
#define OPA_COVER 255

typedef theme_test_styles theme_t;

typedef struct {
	lv_style_t scr;
	lv_style_t scrollbar;
	lv_style_t button;
} theme_t;

static void style_init(theme_t* theme) {
	// scr
	style_init_reset(&theme->styles.scr);
	lv_style_set_bg_opa(&theme->styles.scr, OPA_COVER);

	// scrollbar
	style_init_reset(&theme->styles.scrollbar);
	lv_style_set_bg_color(&theme->styles.scrollbar, WIDGET_BG_COLOR);
	lv_style_set_pad_all(&theme->styles.scrollbar, 7);
	lv_style_set_width(&theme->styles.scrollbar, 5);
	lv_style_set_opa(&theme->styles.scrollbar, 40);

	// button
	style_init_reset(&theme->styles.button);
	lv_style_set_bg_opa(&theme->styles.button, 255);
	lv_style_set_bg_color(&theme->styles.button, BACKGROUND_COLOR);
	lv_style_set_shadow_color(&theme->styles.button, SHADOW_COLOR);
}

lv_theme_t* lv_theme_test_init(
    lv_display_t* disp,
    const lv_font_t* font
) {
    // TODO
}

#ifdef __cplusplus
extern "}"
#endif

Thank you!

There is a conceptual difference between LVGL and CSS. In CSS you set the pseudo classes and selectors when you define the style sheet. (e.g. scrollbar: scrolled {}). In LVGL you assign styles to parts/states on the given widget.

So I wonder how you export scrollbar: scrolled {}?

Also in LVGL you can define only “classes”. But you can can’t do something like button {} or div{} to target all buttons and divs.

Thanks for the input! I think I confused parts and substates, and what I thought I knew about substates. This means I have to rethink the syntax, so I have the following proposal:

1. Differentiate between class styles and part/substate styles:

Substate/part styles have a period in front of them:

.some_button_substate_or_part_state {
     bgColor: @some-color;
}

Class styles (e.g. for an lv_button) don’t have a period in front:

button {
     bgColor: @primary-color;
}

2. Allow class styles to contain 0, 1 or more part/substate styles:

/* Substate/part styles */
.card { /* ... */ }
.list_bg { /* ... */ }
.scrollbar { /* ... */ }
.scrollbar_scrolled { /* ... */ }

/* lv_list class style */
list {
  radius: 16dp;
  styles: {
     card: DEFAULT;
     list_bg: DEFAULT;
     scrollbar: PART_SCROLLBAR;
     scrollbar_scrolled: PART_SCROLLBAR, LV_STATE_SCROLLED;
  }
}

The list {} style should generate the following:

if (lv_obj_check_type(obj, &lv_list_class)) {
    lv_obj_add_style(obj, &theme->styles.card, 0);
    lv_obj_add_style(obj, &theme->styles.list_bg, 0);
    lv_obj_add_style(obj, &theme->styles.scrollbar, LV_PART_SCROLLBAR);
    lv_obj_add_style(obj, &theme->styles.scrollbar_scrolled, LV_PART_SCROLLBAR | LV_STATE_SCROLLED);
    return;
}

I’ve worked on it a bit more and this is what I came up with.
The below content generates code that relates to static void theme_apply() in lv_theme_default.c

For example:

apply(tabview > * > obj) {
    pad_normal: default;
    rotary_scroll: default;
    scrollbar: part.scrollbar;
    scrollbar_scrolled: part.scrollbar, state.scrolled;
}

Inside the apply() filtering function, it is looking for a widget that originates from a tabview with any child that has a child of type obj.

“*” signifies “a non-null child that is of any type”.

It applies the pad_normal and rotary_scroll styles always.
It applies the scrollbar style when the part is a scrollbar.
It applies scrollbar_scrolled style when the part is a scrollbar and the state is scrolled.

This would generate the following C code:

if (lv_obj_check_type(obj, &lv_obj_class) && lv_obj_check_type(parent_parent, &lv_tabview_class)) {
    lv_obj_add_style(obj, &theme->styles.pad_normal, 0);
    lv_obj_add_style(obj, &theme->styles.rotary_scroll, 0);
    lv_obj_add_style(obj, &theme->styles.scrollbar, LV_PART_SCROLLBAR);
    lv_obj_add_style(obj, &theme->styles.scrollbar_scrolled, LV_PART_SCROLLBAR | LV_STATE_SCROLLED);
    return;
}

What do you think?

More examples:

// Any object with no parent
apply(null > *) {
    scr: default;
    scrollbar: part.scrollbar;
    scrollbar_scrolled: part.scrollbar, state.scrolled;
}

// tabview button containers: an obj at index 0 in a tabview parent
apply(tabview > obj[0]) {
    bg_color_white: default;
    outline_primary: state.focusKey;
    tab_bg_focus: state.focusKey;
}