Theme settings app and memory optimisation

This commit is contained in:
Ken Van Hoeylandt 2026-10-07 00:07:37 +02:00
parent 7638602201
commit 2522f5da71
18 changed files with 731 additions and 33 deletions

View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
/** User-configurable theme settings */
typedef struct {
bool is_dark;
/** Use the monochrome theme, also on colour displays. Monochrome and greyscale displays always use it. */
bool is_mono;
/** Slow-refresh displays (e.g. e-paper) never show animations */
bool animations_enabled;
lv_color_t color_primary;
lv_color_t color_secondary;
lv_color_t color_error;
} LvglThemeSettings;
/**
* @brief Gets the theme settings, or the defaults when no settings were set.
* @param[out] settings the current theme settings
*/
void lvgl_theme_get_settings(LvglThemeSettings* settings);
/**
* @brief Gets the default theme settings for this device.
* @param[out] settings the default theme settings
*/
void lvgl_theme_get_default_settings(LvglThemeSettings* settings);
/**
* @brief Sets the theme settings. They are used for displays that are added afterwards, e.g. when LVGL (re)starts.
* @param[in] settings the theme settings
*/
void lvgl_theme_set_settings(const LvglThemeSettings* settings);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Creates a chip: a compact button that shows a choice. The theme shows it as selected when it has LV_STATE_CHECKED.
* The chip doesn't toggle by itself, so the app decides which chips are selected (e.g. one of several options).
* @param[in] parent the parent object for the new chip
* @return the created chip
*/
lv_obj_t* lvgl_chip_create(lv_obj_t* parent);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,24 @@
#pragma once
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Initializes the theme for a display. When the display needs the other theme than the active one,
* the active theme is deinitialized first, so only one theme is allocated.
* @param[in] display the display that the theme is for
* @param[in] isMonoDisplay the display can only show monochrome or greyscale
* @param[in] supportsAnimations the display can show animations (e.g. not e-paper)
* @return the theme
*/
lv_theme_t* lvgl_theme_init_for_display(lv_display_t* display, bool isMonoDisplay, bool supportsAnimations);
/** Deinitializes the active theme. LVGL frees its memory when it stops, which includes the theme. */
void lvgl_theme_deinit(void);
#ifdef __cplusplus
}
#endif

View File

@ -13,6 +13,8 @@
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/theme_private.h>
#include <cstddef> #include <cstddef>
constexpr auto* TAG = "lvgl"; constexpr auto* TAG = "lvgl";
@ -268,6 +270,8 @@ void lvgl_devices_detach() {
display = lv_disp_get_next(NULL); display = lv_disp_get_next(NULL);
} }
lvgl_theme_deinit();
lvgl_unlock(); lvgl_unlock();
} }

View File

@ -3,6 +3,7 @@
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
#include <lvgl/ppa.h> #include <lvgl/ppa.h>
#include <lvgl/theme_private.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
@ -11,8 +12,6 @@
#include <lvgl/devices/device_context.h> #include <lvgl/devices/device_context.h>
#include "../themes/lv_theme_material.h"
#include "../themes/lv_theme_material_mono.h"
#include <stdlib.h> #include <stdlib.h>
@ -24,31 +23,10 @@ constexpr auto* TAG = "lvgl_display";
// The themes are generated with LSS from Modules/lvgl-module/themes/*.lss // The themes are generated with LSS from Modules/lvgl-module/themes/*.lss
static void lvgl_display_apply_theme(lv_display_t* disp, struct Device* device, lv_color_format_t color_format) { static void lvgl_display_apply_theme(lv_display_t* disp, struct Device* device, lv_color_format_t color_format) {
#if defined(LV_THEME_DEFAULT_DARK) && LV_THEME_DEFAULT_DARK bool is_mono_display = color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8;
bool is_dark = true;
#else
bool is_dark = false;
#endif
bool is_compact = lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT;
// Slow-refresh panels (e-paper) can't show animations // Slow-refresh panels (e-paper) can't show animations
bool animations_enabled = !display_has_capability(device, DISPLAY_CAPABILITY_SLOW_REFRESH); bool supports_animations = !display_has_capability(device, DISPLAY_CAPABILITY_SLOW_REFRESH);
lv_display_set_theme(disp, lvgl_theme_init_for_display(disp, is_mono_display, supports_animations));
lv_theme_t* theme;
if (color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8) {
lv_theme_material_mono_config_t config;
lv_theme_material_mono_config_init(&config);
config.is_compact = is_compact;
config.animations_enabled = animations_enabled;
theme = lv_theme_material_mono_init(disp, &config);
} else {
lv_theme_material_config_t config;
lv_theme_material_config_init(&config);
config.is_dark = is_dark;
config.is_compact = is_compact;
config.animations_enabled = animations_enabled;
theme = lv_theme_material_init(disp, &config);
}
lv_display_set_theme(disp, theme);
} }
struct LvglDisplayCtx { struct LvglDisplayCtx {

View File

@ -10,7 +10,9 @@
#include <lvgl/devices/pointer.h> #include <lvgl/devices/pointer.h>
#include <lvgl/widgets/sliderbox.h> #include <lvgl/widgets/sliderbox.h>
#include <lvgl/theme.h>
#include <lvgl/widgets/card.h> #include <lvgl/widgets/card.h>
#include <lvgl/widgets/chip.h>
#include <lvgl/widgets/icon_button.h> #include <lvgl/widgets/icon_button.h>
#include <lvgl/widgets/spinner.h> #include <lvgl/widgets/spinner.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
@ -58,8 +60,14 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_pointer_add), DEFINE_MODULE_SYMBOL(lvgl_pointer_add),
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_slot_index), DEFINE_MODULE_SYMBOL(lvgl_pointer_get_slot_index),
DEFINE_MODULE_SYMBOL(lvgl_pointer_remove), DEFINE_MODULE_SYMBOL(lvgl_pointer_remove),
// lvgl_theme
DEFINE_MODULE_SYMBOL(lvgl_theme_get_settings),
DEFINE_MODULE_SYMBOL(lvgl_theme_get_default_settings),
DEFINE_MODULE_SYMBOL(lvgl_theme_set_settings),
// lvgl_card // lvgl_card
DEFINE_MODULE_SYMBOL(lvgl_card_create), DEFINE_MODULE_SYMBOL(lvgl_card_create),
// lvgl_chip
DEFINE_MODULE_SYMBOL(lvgl_chip_create),
// lvgl_icon_button // lvgl_icon_button
DEFINE_MODULE_SYMBOL(lvgl_icon_button_create), DEFINE_MODULE_SYMBOL(lvgl_icon_button_create),
DEFINE_MODULE_SYMBOL(lvgl_icon_button_create_variant), DEFINE_MODULE_SYMBOL(lvgl_icon_button_create_variant),

View File

@ -0,0 +1,124 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl/theme.h>
#include <lvgl/theme_private.h>
#include <lvgl/lvgl.h>
#include <tactility/log.h>
#include "themes/lv_theme_material.h"
#include "themes/lv_theme_material_mono.h"
constexpr auto* TAG = "lvgl_theme";
enum class ActiveTheme {
None,
Material,
MaterialMono
};
static LvglThemeSettings theme_settings;
static bool theme_settings_set = false;
static ActiveTheme active_theme = ActiveTheme::None;
void lvgl_theme_get_default_settings(LvglThemeSettings* settings) {
lv_theme_material_config_t material_config;
lv_theme_material_config_init(&material_config);
#if defined(LV_THEME_DEFAULT_DARK) && LV_THEME_DEFAULT_DARK
settings->is_dark = true;
#else
settings->is_dark = false;
#endif
settings->is_mono = false;
settings->animations_enabled = true;
settings->color_primary = material_config.color_primary;
settings->color_secondary = material_config.color_secondary;
settings->color_error = material_config.color_error;
}
void lvgl_theme_get_settings(LvglThemeSettings* settings) {
if (theme_settings_set) {
*settings = theme_settings;
} else {
lvgl_theme_get_default_settings(settings);
}
}
void lvgl_theme_set_settings(const LvglThemeSettings* settings) {
theme_settings = *settings;
theme_settings_set = true;
}
static lv_theme_t* get_active_theme() {
switch (active_theme) {
case ActiveTheme::Material:
return lv_theme_material_get();
case ActiveTheme::MaterialMono:
return lv_theme_material_mono_get();
default:
return nullptr;
}
}
void lvgl_theme_deinit() {
switch (active_theme) {
case ActiveTheme::Material:
lv_theme_material_deinit();
break;
case ActiveTheme::MaterialMono:
lv_theme_material_mono_deinit();
break;
default:
break;
}
active_theme = ActiveTheme::None;
}
lv_theme_t* lvgl_theme_init_for_display(lv_display_t* display, bool isMonoDisplay, bool supportsAnimations) {
LvglThemeSettings settings;
lvgl_theme_get_settings(&settings);
const bool is_compact = lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT;
const bool animations_enabled = settings.animations_enabled && supportsAnimations;
const ActiveTheme required_theme = (isMonoDisplay || settings.is_mono) ? ActiveTheme::MaterialMono : ActiveTheme::Material;
// Only one theme is allocated at a time
lv_theme_t* replaced_theme = nullptr;
if (active_theme != ActiveTheme::None && active_theme != required_theme) {
replaced_theme = get_active_theme();
lvgl_theme_deinit();
}
lv_theme_t* theme;
if (required_theme == ActiveTheme::MaterialMono) {
lv_theme_material_mono_config_t config;
lv_theme_material_mono_config_init(&config);
config.is_dark = settings.is_dark;
config.is_compact = is_compact;
config.animations_enabled = animations_enabled;
theme = lv_theme_material_mono_init(display, &config);
} else {
lv_theme_material_config_t config;
lv_theme_material_config_init(&config);
config.is_dark = settings.is_dark;
config.is_compact = is_compact;
config.animations_enabled = animations_enabled;
config.color_primary = settings.color_primary;
config.color_secondary = settings.color_secondary;
config.color_error = settings.color_error;
theme = lv_theme_material_init(display, &config);
}
active_theme = required_theme;
// Other displays mustn't keep the deinitialized theme. Their widgets keep the old styles until they're recreated.
if (replaced_theme != nullptr) {
LOG_W(TAG, "Displays need different themes: switching all displays to the new theme");
lv_display_t* other_display = lv_display_get_next(nullptr);
while (other_display != nullptr) {
if (lv_display_get_theme(other_display) == replaced_theme) {
lv_display_set_theme(other_display, theme);
}
other_display = lv_display_get_next(other_display);
}
}
return theme;
}

View File

@ -14,6 +14,7 @@ extern const lv_obj_class_t lvgl_icon_button_filled_class;
extern const lv_obj_class_t lvgl_icon_button_tonal_class; extern const lv_obj_class_t lvgl_icon_button_tonal_class;
extern const lv_obj_class_t lvgl_sliderbox_class; extern const lv_obj_class_t lvgl_sliderbox_class;
extern const lv_obj_class_t lvgl_card_class; extern const lv_obj_class_t lvgl_card_class;
extern const lv_obj_class_t lvgl_chip_class;
#define NEUTRAL_4 lv_color_hex(0x0E0E0E) #define NEUTRAL_4 lv_color_hex(0x0E0E0E)
#define NEUTRAL_6 lv_color_hex(0x131313) #define NEUTRAL_6 lv_color_hex(0x131313)
@ -128,6 +129,13 @@ extern const lv_obj_class_t lvgl_card_class;
#define CARD_SHAPE SHAPE_CONTAINER #define CARD_SHAPE SHAPE_CONTAINER
#define CARD_PAD DENSITY_CONTAINER_PAD #define CARD_PAD DENSITY_CONTAINER_PAD
#define CARD_GAP DENSITY_CONTAINER_GAP #define CARD_GAP DENSITY_CONTAINER_GAP
#define CHIP_LABEL_COLOR COLOR_ON_SURFACE_VARIANT
#define CHIP_BORDER_COLOR COLOR_OUTLINE
#define CHIP_SELECTED_COLOR COLOR_SECONDARY_CONTAINER
#define CHIP_SELECTED_LABEL_COLOR COLOR_ON_SECONDARY_CONTAINER
#define CHIP_SHAPE (theme->config.is_compact ? CORNER_COMPACT : CORNER_SMALL)
#define CHIP_PAD_HOR (theme->config.is_compact ? 3 : SPACE_4)
#define CHIP_PAD_VER (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 6))
#define SCROLLBAR_COLOR COLOR_OUTLINE #define SCROLLBAR_COLOR COLOR_OUTLINE
#define SCROLLBAR_OPACITY (LV_OPA_40) #define SCROLLBAR_OPACITY (LV_OPA_40)
#define SCROLLBAR_SCROLLED_OPACITY (LV_OPA_70) #define SCROLLBAR_SCROLLED_OPACITY (LV_OPA_70)
@ -324,6 +332,8 @@ typedef struct {
lv_style_t screen; lv_style_t screen;
lv_style_t container; lv_style_t container;
lv_style_t card; lv_style_t card;
lv_style_t chip;
lv_style_t chip_selected;
#if LV_USE_BUTTON #if LV_USE_BUTTON
lv_style_t button; lv_style_t button;
#endif #endif
@ -747,6 +757,21 @@ static void style_init(my_theme_t * theme)
lv_style_set_pad_all(&theme->styles.card, CARD_PAD); lv_style_set_pad_all(&theme->styles.card, CARD_PAD);
lv_style_set_pad_gap(&theme->styles.card, CARD_GAP); lv_style_set_pad_gap(&theme->styles.card, CARD_GAP);
style_init_reset(&theme->styles.chip);
lv_style_set_bg_opa(&theme->styles.chip, LV_OPA_TRANSP);
lv_style_set_text_color(&theme->styles.chip, CHIP_LABEL_COLOR);
lv_style_set_border_color(&theme->styles.chip, CHIP_BORDER_COLOR);
lv_style_set_border_width(&theme->styles.chip, DENSITY_BORDER);
lv_style_set_radius(&theme->styles.chip, CHIP_SHAPE);
lv_style_set_pad_hor(&theme->styles.chip, CHIP_PAD_HOR);
lv_style_set_pad_ver(&theme->styles.chip, CHIP_PAD_VER);
style_init_reset(&theme->styles.chip_selected);
lv_style_set_bg_color(&theme->styles.chip_selected, CHIP_SELECTED_COLOR);
lv_style_set_bg_opa(&theme->styles.chip_selected, LV_OPA_COVER);
lv_style_set_text_color(&theme->styles.chip_selected, CHIP_SELECTED_LABEL_COLOR);
lv_style_set_border_color(&theme->styles.chip_selected, CHIP_SELECTED_COLOR);
#if LV_USE_BUTTON #if LV_USE_BUTTON
style_init_reset(&theme->styles.button); style_init_reset(&theme->styles.button);
lv_style_set_bg_color(&theme->styles.button, BUTTON_CONTAINER_COLOR); lv_style_set_bg_color(&theme->styles.button, BUTTON_CONTAINER_COLOR);
@ -1828,6 +1853,18 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
return; return;
} }
#endif #endif
if(lv_obj_check_type(obj, &lvgl_chip_class)) {
/* chip */
lv_obj_add_style(obj, &theme->styles.chip, 0);
lv_obj_add_style(obj, &theme->styles.transition_exit, 0);
lv_obj_add_style(obj, &theme->styles.state_pressed, LV_STATE_PRESSED);
lv_obj_add_style(obj, &theme->styles.transition_enter, LV_STATE_PRESSED);
lv_obj_add_style(obj, &theme->styles.chip_selected, LV_STATE_CHECKED);
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
lv_obj_add_style(obj, &theme->styles.edit_ring, LV_STATE_EDITED);
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
return;
}
if(lv_obj_check_type(obj, &lvgl_card_class)) { if(lv_obj_check_type(obj, &lvgl_card_class)) {
/* card */ /* card */
lv_obj_add_style(obj, &theme->styles.card, 0); lv_obj_add_style(obj, &theme->styles.card, 0);

View File

@ -14,6 +14,7 @@ extern const lv_obj_class_t lvgl_icon_button_filled_class;
extern const lv_obj_class_t lvgl_icon_button_tonal_class; extern const lv_obj_class_t lvgl_icon_button_tonal_class;
extern const lv_obj_class_t lvgl_sliderbox_class; extern const lv_obj_class_t lvgl_sliderbox_class;
extern const lv_obj_class_t lvgl_card_class; extern const lv_obj_class_t lvgl_card_class;
extern const lv_obj_class_t lvgl_chip_class;
#define BLACK lv_color_black() #define BLACK lv_color_black()
#define WHITE lv_color_white() #define WHITE lv_color_white()
@ -82,6 +83,9 @@ extern const lv_obj_class_t lvgl_card_class;
#define CARD_SHAPE SHAPE_CONTAINER #define CARD_SHAPE SHAPE_CONTAINER
#define CARD_PAD DENSITY_CONTAINER_PAD #define CARD_PAD DENSITY_CONTAINER_PAD
#define CARD_GAP DENSITY_CONTAINER_GAP #define CARD_GAP DENSITY_CONTAINER_GAP
#define CHIP_SHAPE (theme->config.is_compact ? CORNER_COMPACT : CORNER_SMALL)
#define CHIP_PAD_HOR (theme->config.is_compact ? 3 : SPACE_4)
#define CHIP_PAD_VER (theme->config.is_compact ? 2 : SPACE_2)
#define SCROLLBAR_COLOR COLOR_ON_SURFACE #define SCROLLBAR_COLOR COLOR_ON_SURFACE
#define SCROLLBAR_WIDTH (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 3)) #define SCROLLBAR_WIDTH (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 3))
#define SCROLLBAR_PAD 1 #define SCROLLBAR_PAD 1
@ -209,6 +213,7 @@ typedef struct {
lv_style_t screen; lv_style_t screen;
lv_style_t container; lv_style_t container;
lv_style_t card; lv_style_t card;
lv_style_t chip;
#if LV_USE_BUTTON || LV_USE_MSGBOX #if LV_USE_BUTTON || LV_USE_MSGBOX
lv_style_t button; lv_style_t button;
#endif #endif
@ -541,6 +546,16 @@ static void style_init(my_theme_t * theme)
lv_style_set_pad_all(&theme->styles.card, CARD_PAD); lv_style_set_pad_all(&theme->styles.card, CARD_PAD);
lv_style_set_pad_gap(&theme->styles.card, CARD_GAP); lv_style_set_pad_gap(&theme->styles.card, CARD_GAP);
style_init_reset(&theme->styles.chip);
lv_style_set_bg_color(&theme->styles.chip, COLOR_SURFACE);
lv_style_set_bg_opa(&theme->styles.chip, LV_OPA_COVER);
lv_style_set_text_color(&theme->styles.chip, COLOR_ON_SURFACE);
lv_style_set_border_color(&theme->styles.chip, COLOR_OUTLINE);
lv_style_set_border_width(&theme->styles.chip, BORDER);
lv_style_set_radius(&theme->styles.chip, CHIP_SHAPE);
lv_style_set_pad_hor(&theme->styles.chip, CHIP_PAD_HOR);
lv_style_set_pad_ver(&theme->styles.chip, CHIP_PAD_VER);
#if LV_USE_BUTTON || LV_USE_MSGBOX #if LV_USE_BUTTON || LV_USE_MSGBOX
style_init_reset(&theme->styles.button); style_init_reset(&theme->styles.button);
lv_style_set_bg_color(&theme->styles.button, BUTTON_CONTAINER_COLOR); lv_style_set_bg_color(&theme->styles.button, BUTTON_CONTAINER_COLOR);
@ -1431,6 +1446,16 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
return; return;
} }
#endif #endif
if(lv_obj_check_type(obj, &lvgl_chip_class)) {
/* chip */
lv_obj_add_style(obj, &theme->styles.chip, 0);
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_CHECKED);
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
lv_obj_add_style(obj, &theme->styles.edit_ring, LV_STATE_EDITED);
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
return;
}
if(lv_obj_check_type(obj, &lvgl_card_class)) { if(lv_obj_check_type(obj, &lvgl_card_class)) {
/* card */ /* card */
lv_obj_add_style(obj, &theme->styles.card, 0); lv_obj_add_style(obj, &theme->styles.card, 0);

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0
#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration
#include <lvgl/widgets/chip.h>
// Not static: the theme styles chips by this class
extern "C" const lv_obj_class_t lvgl_chip_class = {
.base_class = &lv_button_class,
.constructor_cb = nullptr,
.destructor_cb = nullptr,
.event_cb = nullptr,
.user_data = nullptr,
.name = "chip",
.width_def = 0,
.height_def = 0,
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
.instance_size = 0,
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
};
lv_obj_t* lvgl_chip_create(lv_obj_t* parent) {
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_chip_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}

View File

@ -27,6 +27,7 @@
@widget icon_button_tonal: lvgl_icon_button_tonal_class; @widget icon_button_tonal: lvgl_icon_button_tonal_class;
@widget sliderbox: lvgl_sliderbox_class; @widget sliderbox: lvgl_sliderbox_class;
@widget card: lvgl_card_class; @widget card: lvgl_card_class;
@widget chip: lvgl_chip_class;
// ============================================================ // ============================================================
// 1. Base tokens // 1. Base tokens
@ -188,6 +189,15 @@
@card-pad: @density-container-pad; @card-pad: @density-container-pad;
@card-gap: @density-container-gap; @card-gap: @density-container-gap;
// Chip (lvgl_chip_create): Material 3 filter chip, selected when checked
@chip-label-color: @color-on-surface-variant;
@chip-border-color: @color-outline;
@chip-selected-color: @color-secondary-container;
@chip-selected-label-color: @color-on-secondary-container;
@chip-shape: if($isCompact, @corner-compact, @corner-small);
@chip-pad-hor: if($isCompact, 3, @space-4);
@chip-pad-ver: if($isCompact, 2, 6dp);
// Scrollbar // Scrollbar
@scrollbar-color: @color-outline; @scrollbar-color: @color-outline;
@scrollbar-opacity: 40%; @scrollbar-opacity: 40%;
@ -551,6 +561,23 @@ card {
padGap: @card-gap; padGap: @card-gap;
} }
chip {
bgOpa: transp;
textColor: @chip-label-color;
borderColor: @chip-border-color;
borderWidth: @density-border;
radius: @chip-shape;
padHor: @chip-pad-hor;
padVer: @chip-pad-ver;
}
chip_selected {
bgColor: @chip-selected-color;
bgOpa: cover;
textColor: @chip-selected-label-color;
borderColor: @chip-selected-color;
}
button { button {
bgColor: @button-container-color; bgColor: @button-container-color;
bgOpa: cover; bgOpa: cover;
@ -1353,6 +1380,17 @@ apply(checkbox) {
disabled: state.disabled; disabled: state.disabled;
} }
apply(chip) {
chip: default;
transition_exit: default;
state_pressed: state.pressed;
transition_enter: state.pressed;
chip_selected: state.checked;
focus_ring: state.focusKey;
edit_ring: state.edited;
disabled: state.disabled;
}
apply(card) { apply(card) {
card: default; card: default;
scrollbar: part.scrollbar; scrollbar: part.scrollbar;

View File

@ -28,6 +28,7 @@
@widget icon_button_tonal: lvgl_icon_button_tonal_class; @widget icon_button_tonal: lvgl_icon_button_tonal_class;
@widget sliderbox: lvgl_sliderbox_class; @widget sliderbox: lvgl_sliderbox_class;
@widget card: lvgl_card_class; @widget card: lvgl_card_class;
@widget chip: lvgl_chip_class;
// ============================================================ // ============================================================
// 1. Base tokens // 1. Base tokens
@ -135,6 +136,11 @@
@card-pad: @density-container-pad; @card-pad: @density-container-pad;
@card-gap: @density-container-gap; @card-gap: @density-container-gap;
// Chip (lvgl_chip_create): outlined, inverted when selected
@chip-shape: if($isCompact, @corner-compact, @corner-small);
@chip-pad-hor: if($isCompact, 3, @space-4);
@chip-pad-ver: if($isCompact, 2, @space-2);
// Scrollbar // Scrollbar
@scrollbar-color: @color-on-surface; @scrollbar-color: @color-on-surface;
@scrollbar-width: if($isCompact, 2, 3dp); @scrollbar-width: if($isCompact, 2, 3dp);
@ -381,6 +387,17 @@ card {
padGap: @card-gap; padGap: @card-gap;
} }
chip {
bgColor: @color-surface;
bgOpa: cover;
textColor: @color-on-surface;
borderColor: @color-outline;
borderWidth: @border;
radius: @chip-shape;
padHor: @chip-pad-hor;
padVer: @chip-pad-ver;
}
button { button {
bgColor: @button-container-color; bgColor: @button-container-color;
bgOpa: cover; bgOpa: cover;
@ -1026,6 +1043,15 @@ apply(checkbox) {
disabled: part.indicator, state.disabled; disabled: part.indicator, state.disabled;
} }
apply(chip) {
chip: default;
inverted: state.checked;
inverted: state.pressed;
focus_ring: state.focusKey;
edit_ring: state.edited;
disabled: state.disabled;
}
apply(card) { apply(card) {
card: default; card: default;
scrollbar: part.scrollbar; scrollbar: part.scrollbar;

View File

@ -1,10 +1,18 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
namespace tt::settings::appearance { namespace tt::settings::appearance {
enum class ThemeMode {
/** Light or dark, as configured for the device */
DeviceDefault,
Light,
Dark
};
struct AppearanceSettings { struct AppearanceSettings {
/** Default text font size in pixels, or 0 for the device's default size */ /** Default text font size in pixels, or 0 for the device's default size */
uint16_t fontSize = 0; uint16_t fontSize = 0;
@ -12,6 +20,14 @@ struct AppearanceSettings {
std::string regularFontPath; std::string regularFontPath;
/** TrueType font for monospace text, or empty for the system font */ /** TrueType font for monospace text, or empty for the system font */
std::string monoFontPath; std::string monoFontPath;
ThemeMode themeMode = ThemeMode::DeviceDefault;
/** Use the monochrome theme, also on colour displays */
bool monoTheme = false;
bool animationsEnabled = true;
/** Theme colours as 0xRRGGBB, or empty for the theme's default colour */
std::optional<uint32_t> primaryColor;
std::optional<uint32_t> secondaryColor;
std::optional<uint32_t> errorColor;
bool operator==(const AppearanceSettings&) const = default; bool operator==(const AppearanceSettings&) const = default;
}; };

View File

@ -0,0 +1,13 @@
#pragma once
#include <Tactility/settings/AppearanceSettings.h>
namespace tt::lvgl {
/**
* Sets the theme settings from the appearance settings.
* They take effect for displays that are added afterwards, so call this before LVGL (re)starts.
*/
void configureTheme(const settings::appearance::AppearanceSettings& settings);
}

View File

@ -2,6 +2,7 @@
#include <Tactility/file/File.h> #include <Tactility/file/File.h>
#include <Tactility/lvgl/Fonts.h> #include <Tactility/lvgl/Fonts.h>
#include <Tactility/lvgl/Lvgl.h> #include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/Theme.h>
#include <Tactility/settings/AppearanceSettings.h> #include <Tactility/settings/AppearanceSettings.h>
#include <app/event.h> #include <app/event.h>
@ -19,6 +20,9 @@
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/lvgl.h> #include <lvgl/lvgl.h>
#include <lvgl/theme.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/chip.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <atomic> #include <atomic>
@ -42,6 +46,33 @@ enum class FontSlot {
Mono Mono
}; };
struct NamedColor {
const char* name;
lv_palette_t palette;
};
constexpr NamedColor PALETTE_COLORS[] = {
{ "Red", LV_PALETTE_RED },
{ "Pink", LV_PALETTE_PINK },
{ "Purple", LV_PALETTE_PURPLE },
{ "Deep purple", LV_PALETTE_DEEP_PURPLE },
{ "Indigo", LV_PALETTE_INDIGO },
{ "Blue", LV_PALETTE_BLUE },
{ "Light blue", LV_PALETTE_LIGHT_BLUE },
{ "Cyan", LV_PALETTE_CYAN },
{ "Teal", LV_PALETTE_TEAL },
{ "Green", LV_PALETTE_GREEN },
{ "Light green", LV_PALETTE_LIGHT_GREEN },
{ "Lime", LV_PALETTE_LIME },
{ "Yellow", LV_PALETTE_YELLOW },
{ "Amber", LV_PALETTE_AMBER },
{ "Orange", LV_PALETTE_ORANGE },
{ "Deep orange", LV_PALETTE_DEEP_ORANGE },
{ "Brown", LV_PALETTE_BROWN },
{ "Blue grey", LV_PALETTE_BLUE_GREY },
{ "Grey", LV_PALETTE_GREY },
};
struct FontRowWidgets { struct FontRowWidgets {
lv_obj_t* defaultButton = nullptr; lv_obj_t* defaultButton = nullptr;
lv_obj_t* fileLabel = nullptr; lv_obj_t* fileLabel = nullptr;
@ -80,6 +111,13 @@ struct Context {
FontRowWidgets regularRow; FontRowWidgets regularRow;
FontRowWidgets monoRow; FontRowWidgets monoRow;
lv_obj_t* errorLabel = nullptr; lv_obj_t* errorLabel = nullptr;
lv_obj_t* lightChip = nullptr;
lv_obj_t* darkChip = nullptr;
lv_obj_t* regularThemeChip = nullptr;
lv_obj_t* monoThemeChip = nullptr;
lv_obj_t* colorRows[3] = {};
/** The display can only show the mono theme */
bool isMonoDisplay = false;
}; };
lvgl::FontConfiguration toFontConfiguration(const settings::appearance::AppearanceSettings& settings) { lvgl::FontConfiguration toFontConfiguration(const settings::appearance::AppearanceSettings& settings) {
@ -120,6 +158,47 @@ void updateFontRow(const FontRowWidgets& row, const std::string& path, size_t ch
setHidden(row.warningLabel, !large); setHidden(row.warningLabel, !large);
} }
bool isDeviceDefaultDark() {
LvglThemeSettings defaults;
lvgl_theme_get_default_settings(&defaults);
return defaults.is_dark;
}
bool isDark(const settings::appearance::AppearanceSettings& settings) {
switch (settings.themeMode) {
case settings::appearance::ThemeMode::Light:
return false;
case settings::appearance::ThemeMode::Dark:
return true;
default:
return isDeviceDefaultDark();
}
}
void setChecked(lv_obj_t* object, bool checked) {
if (checked) {
lv_obj_add_state(object, LV_STATE_CHECKED);
} else {
lv_obj_remove_state(object, LV_STATE_CHECKED);
}
}
void updateThemeWidgets(Context* ctx) {
const bool dark = isDark(ctx->pendingSettings);
setChecked(ctx->lightChip, !dark);
setChecked(ctx->darkChip, dark);
if (ctx->regularThemeChip != nullptr) {
setChecked(ctx->regularThemeChip, !ctx->pendingSettings.monoTheme);
setChecked(ctx->monoThemeChip, ctx->pendingSettings.monoTheme);
}
// The mono theme doesn't use colours
for (auto* row : ctx->colorRows) {
if (row != nullptr) {
setHidden(row, ctx->pendingSettings.monoTheme);
}
}
}
/** Updates the widgets to the pending settings. Requires the LVGL lock. */ /** Updates the widgets to the pending settings. Requires the LVGL lock. */
void updateWidgets(Context* ctx) { void updateWidgets(Context* ctx) {
if (ctx->applyButton == nullptr) { if (ctx->applyButton == nullptr) {
@ -131,6 +210,64 @@ void updateWidgets(Context* ctx) {
updateFontRow(ctx->regularRow, ctx->pendingSettings.regularFontPath, ctx->regularCharacterCount); updateFontRow(ctx->regularRow, ctx->pendingSettings.regularFontPath, ctx->regularCharacterCount);
updateFontRow(ctx->monoRow, ctx->pendingSettings.monoFontPath, ctx->monoCharacterCount); updateFontRow(ctx->monoRow, ctx->pendingSettings.monoFontPath, ctx->monoCharacterCount);
setHidden(ctx->errorLabel, !ctx->applyFailed); setHidden(ctx->errorLabel, !ctx->applyFailed);
updateThemeWidgets(ctx);
}
void setDark(Context* ctx, bool dark) {
// The device default is kept when it matches, so it doesn't count as a change
if (dark == isDeviceDefaultDark()) {
ctx->pendingSettings.themeMode = settings::appearance::ThemeMode::DeviceDefault;
} else {
ctx->pendingSettings.themeMode = dark ? settings::appearance::ThemeMode::Dark : settings::appearance::ThemeMode::Light;
}
updateWidgets(ctx);
}
void onLightPressed(lv_event_t* event) {
setDark(static_cast<Context*>(lv_event_get_user_data(event)), false);
}
void onDarkPressed(lv_event_t* event) {
setDark(static_cast<Context*>(lv_event_get_user_data(event)), true);
}
void onRegularThemePressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
ctx->pendingSettings.monoTheme = false;
updateWidgets(ctx);
}
void onMonoThemePressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
ctx->pendingSettings.monoTheme = true;
updateWidgets(ctx);
}
void onAnimationsChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* checkbox = static_cast<lv_obj_t*>(lv_event_get_target(event));
ctx->pendingSettings.animationsEnabled = lv_obj_has_state(checkbox, LV_STATE_CHECKED);
updateWidgets(ctx);
}
uint32_t getPaletteColor(lv_palette_t palette) {
return lv_color_to_u32(lv_palette_main(palette)) & 0xFFFFFF;
}
constexpr size_t PALETTE_COLOR_COUNT = sizeof(PALETTE_COLORS) / sizeof(PALETTE_COLORS[0]);
void onColorChanged(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto* color = static_cast<std::optional<uint32_t>*>(lv_obj_get_user_data(dropdown));
const uint32_t selected = lv_dropdown_get_selected(dropdown);
if (selected == 0) {
color->reset();
} else if (selected <= PALETTE_COLOR_COUNT) {
*color = getPaletteColor(PALETTE_COLORS[selected - 1].palette);
}
// Any other option is the custom colour from the settings file, which stays unchanged
updateWidgets(ctx);
} }
void onBackPressed(lv_event_t* event) { void onBackPressed(lv_event_t* event) {
@ -194,20 +331,99 @@ void onDefaultMonoPressed(lv_event_t* event) {
updateWidgets(ctx); updateWidgets(ctx);
} }
/** An unstyled row, so it doesn't paint over the card. Its items are spaced like the card's content. */
lv_obj_t* createRow(lv_obj_t* parent) { lv_obj_t* createRow(lv_obj_t* parent) {
auto* row = lv_obj_create(parent); auto* row = lv_obj_create(parent);
lv_obj_remove_style_all(row);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_column(row, lv_obj_get_style_pad_column(parent, LV_PART_MAIN), LV_STATE_DEFAULT);
// Room for the focus outline of the buttons, which is clipped to the row in compact mode // Room for the focus outline of the buttons, which is clipped to the row in compact mode
lv_obj_set_style_pad_ver(row, 3, LV_STATE_DEFAULT); lv_obj_set_style_pad_ver(row, 3, LV_STATE_DEFAULT);
lv_obj_set_style_pad_hor(row, 3, LV_STATE_DEFAULT); lv_obj_set_style_pad_hor(row, 3, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
return row; return row;
} }
/** A title above a card, and the card */
lv_obj_t* createSection(lv_obj_t* parent, const char* title) {
auto* title_label = lv_label_create(parent);
lv_label_set_text(title_label, title);
auto* card = lvgl_card_create(parent);
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
return card;
}
lv_obj_t* createLabeledRow(lv_obj_t* parent, const char* text) {
auto* row = createRow(parent);
auto* label = lv_label_create(row);
lv_label_set_text(label, text);
lv_obj_set_flex_grow(label, 1);
return row;
}
lv_obj_t* createChip(lv_obj_t* parent, const char* text, lv_event_cb_t callback, Context* ctx) {
auto* chip = lvgl_chip_create(parent);
auto* label = lv_label_create(chip);
lv_label_set_text(label, text);
lv_obj_add_event_cb(chip, callback, LV_EVENT_SHORT_CLICKED, ctx);
return chip;
}
lv_obj_t* createColorRow(lv_obj_t* parent, const char* text, std::optional<uint32_t>* color, Context* ctx) {
auto* row = createLabeledRow(parent, text);
auto* dropdown = lv_dropdown_create(row);
std::string options = "Default";
uint32_t selected = 0;
for (size_t i = 0; i < PALETTE_COLOR_COUNT; i++) {
options += "\n";
options += PALETTE_COLORS[i].name;
if (color->has_value() && **color == getPaletteColor(PALETTE_COLORS[i].palette)) {
selected = i + 1;
}
}
// A colour that isn't in the palette was set in the settings file
if (color->has_value() && selected == 0) {
options += "\nCustom";
selected = PALETTE_COLOR_COUNT + 1;
}
lv_dropdown_set_options(dropdown, options.c_str());
lv_dropdown_set_selected(dropdown, selected);
lv_obj_set_user_data(dropdown, color);
lv_obj_add_event_cb(dropdown, onColorChanged, LV_EVENT_VALUE_CHANGED, ctx);
return row;
}
void createThemeCard(lv_obj_t* parent, Context* ctx) {
auto* card = createSection(parent, "Theme");
auto* mode_row = createLabeledRow(card, "Mode");
ctx->lightChip = createChip(mode_row, "Light", onLightPressed, ctx);
ctx->darkChip = createChip(mode_row, "Dark", onDarkPressed, ctx);
// Monochrome and greyscale displays always use the mono theme, and it doesn't use colours
if (!ctx->isMonoDisplay) {
auto* style_row = createLabeledRow(card, "Style");
ctx->regularThemeChip = createChip(style_row, "Regular", onRegularThemePressed, ctx);
ctx->monoThemeChip = createChip(style_row, "Mono", onMonoThemePressed, ctx);
}
auto* animations_checkbox = lv_checkbox_create(card);
lv_checkbox_set_text(animations_checkbox, "Animations");
setChecked(animations_checkbox, ctx->pendingSettings.animationsEnabled);
lv_obj_add_event_cb(animations_checkbox, onAnimationsChanged, LV_EVENT_VALUE_CHANGED, ctx);
if (!ctx->isMonoDisplay) {
ctx->colorRows[0] = createColorRow(card, "Primary color", &ctx->pendingSettings.primaryColor, ctx);
ctx->colorRows[1] = createColorRow(card, "Secondary color", &ctx->pendingSettings.secondaryColor, ctx);
ctx->colorRows[2] = createColorRow(card, "Error color", &ctx->pendingSettings.errorColor, ctx);
}
}
lv_obj_t* createButton(lv_obj_t* parent, const char* text, lv_event_cb_t callback, Context* ctx) { lv_obj_t* createButton(lv_obj_t* parent, const char* text, lv_event_cb_t callback, Context* ctx) {
auto* button = lv_button_create(parent); auto* button = lv_button_create(parent);
auto* label = lv_label_create(button); auto* label = lv_label_create(button);
@ -257,7 +473,12 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_border_width(content, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(content, 0, LV_STATE_DEFAULT);
auto* font_size_row = createRow(content); const auto color_format = lv_display_get_color_format(lv_obj_get_display(parent));
ctx->isMonoDisplay = color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8;
createThemeCard(content, ctx);
auto* fonts_card = createSection(content, "Fonts");
auto* font_size_row = createRow(fonts_card);
auto* font_size_label = lv_label_create(font_size_row); auto* font_size_label = lv_label_create(font_size_row);
lv_label_set_text(font_size_label, "Font size"); lv_label_set_text(font_size_label, "Font size");
lv_obj_set_flex_grow(font_size_label, 1); lv_obj_set_flex_grow(font_size_label, 1);
@ -277,10 +498,10 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_move_to_index(decrement_button, lv_obj_get_index(font_size_spinbox)); lv_obj_move_to_index(decrement_button, lv_obj_get_index(font_size_spinbox));
createStepButton(font_size_row, LV_SYMBOL_PLUS, onFontSizeIncrementPressed, font_size_spinbox); createStepButton(font_size_row, LV_SYMBOL_PLUS, onFontSizeIncrementPressed, font_size_spinbox);
ctx->regularRow = createFontRow(content, "Regular font", onSelectRegularPressed, onDefaultRegularPressed, ctx); ctx->regularRow = createFontRow(fonts_card, "Regular font", onSelectRegularPressed, onDefaultRegularPressed, ctx);
ctx->monoRow = createFontRow(content, "Mono font", onSelectMonoPressed, onDefaultMonoPressed, ctx); ctx->monoRow = createFontRow(fonts_card, "Mono font", onSelectMonoPressed, onDefaultMonoPressed, ctx);
ctx->errorLabel = lv_label_create(content); ctx->errorLabel = lv_label_create(fonts_card);
lv_obj_set_width(ctx->errorLabel, LV_PCT(100)); lv_obj_set_width(ctx->errorLabel, LV_PCT(100));
lv_label_set_long_mode(ctx->errorLabel, LV_LABEL_LONG_MODE_WRAP); lv_label_set_long_mode(ctx->errorLabel, LV_LABEL_LONG_MODE_WRAP);
lv_label_set_text(ctx->errorLabel, "Failed to create the fonts. Check that the selected files are TrueType fonts."); lv_label_set_text(ctx->errorLabel, "Failed to create the fonts. Check that the selected files are TrueType fonts.");
@ -296,6 +517,13 @@ void destroyWidgets(void* userData) {
ctx->regularRow = {}; ctx->regularRow = {};
ctx->monoRow = {}; ctx->monoRow = {};
ctx->errorLabel = nullptr; ctx->errorLabel = nullptr;
ctx->lightChip = nullptr;
ctx->darkChip = nullptr;
ctx->regularThemeChip = nullptr;
ctx->monoThemeChip = nullptr;
for (auto*& row : ctx->colorRows) {
row = nullptr;
}
} }
int32_t applyThreadMain(void* context) { int32_t applyThreadMain(void* context) {
@ -313,6 +541,8 @@ void cancelApply(Context* ctx) {
lvgl_unlock(); lvgl_unlock();
} }
void finishApply(Context* ctx);
void startApply(Context* ctx) { void startApply(Context* ctx) {
if (ctx->applying || ctx->applyThread != nullptr) { if (ctx->applying || ctx->applyThread != nullptr) {
return; return;
@ -320,11 +550,19 @@ void startApply(Context* ctx) {
lvgl_lock(); lvgl_lock();
ctx->applyConfiguration = toFontConfiguration(ctx->pendingSettings); ctx->applyConfiguration = toFontConfiguration(ctx->pendingSettings);
const bool fonts_changed = ctx->applyConfiguration != toFontConfiguration(ctx->savedSettings);
ctx->applying = true; ctx->applying = true;
ctx->applyFailed = false; ctx->applyFailed = false;
updateWidgets(ctx); updateWidgets(ctx);
lvgl_unlock(); lvgl_unlock();
// Theme changes don't need new fonts
if (!fonts_changed) {
ctx->cacheUpdateSucceeded = true;
finishApply(ctx);
return;
}
ctx->applyThread = thread_alloc(); ctx->applyThread = thread_alloc();
if (ctx->applyThread == nullptr) { if (ctx->applyThread == nullptr) {
LOG_E(TAG, "Failed to create thread"); LOG_E(TAG, "Failed to create thread");
@ -378,8 +616,9 @@ void finishApply(Context* ctx) {
lvgl_unlock(); lvgl_unlock();
// The window is rebuilt from the saved settings when LVGL starts again // The window is rebuilt from the saved settings when LVGL starts again
LOG_I(TAG, "Restarting LVGL with the new fonts"); LOG_I(TAG, "Restarting LVGL with the new settings");
lvgl::stop(); lvgl::stop();
lvgl::configureTheme(applied_settings);
lvgl::loadFonts(ctx->applyConfiguration); lvgl::loadFonts(ctx->applyConfiguration);
lvgl::start(); lvgl::start();
} }

View File

@ -23,6 +23,8 @@
#include <Tactility/hal/usb/Usb.h> #include <Tactility/hal/usb/Usb.h>
#include <Tactility/lvgl/Fonts.h> #include <Tactility/lvgl/Fonts.h>
#include <Tactility/lvgl/Lvgl.h> #include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/Theme.h>
#include <Tactility/settings/AppearanceSettings.h>
#include <Tactility/settings/BootSettings.h> #include <Tactility/settings/BootSettings.h>
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
@ -271,6 +273,7 @@ bool bootInit(TickType_t startTime) {
LOG_I(TAG, "Loading fonts"); LOG_I(TAG, "Loading fonts");
lvgl::loadFonts(lvgl::loadFontConfiguration()); lvgl::loadFonts(lvgl::loadFontConfiguration());
lvgl::configureTheme(settings::appearance::loadOrGetDefault());
screen.end(); screen.end();

View File

@ -0,0 +1,27 @@
#include <Tactility/lvgl/Theme.h>
#include <lvgl/theme.h>
namespace tt::lvgl {
void configureTheme(const settings::appearance::AppearanceSettings& settings) {
LvglThemeSettings theme_settings;
lvgl_theme_get_default_settings(&theme_settings);
if (settings.themeMode != settings::appearance::ThemeMode::DeviceDefault) {
theme_settings.is_dark = settings.themeMode == settings::appearance::ThemeMode::Dark;
}
theme_settings.is_mono = settings.monoTheme;
theme_settings.animations_enabled = settings.animationsEnabled;
if (settings.primaryColor.has_value()) {
theme_settings.color_primary = lv_color_hex(*settings.primaryColor);
}
if (settings.secondaryColor.has_value()) {
theme_settings.color_secondary = lv_color_hex(*settings.secondaryColor);
}
if (settings.errorColor.has_value()) {
theme_settings.color_error = lv_color_hex(*settings.errorColor);
}
lvgl_theme_set_settings(&theme_settings);
}
}

View File

@ -5,6 +5,8 @@
#include <app/paths.h> #include <app/paths.h>
#include <cinttypes>
#include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <map> #include <map>
@ -13,6 +15,28 @@ namespace tt::settings::appearance {
constexpr auto* SETTINGS_KEY_FONT_SIZE = "fontSize"; constexpr auto* SETTINGS_KEY_FONT_SIZE = "fontSize";
constexpr auto* SETTINGS_KEY_REGULAR_FONT_PATH = "regularFontPath"; constexpr auto* SETTINGS_KEY_REGULAR_FONT_PATH = "regularFontPath";
constexpr auto* SETTINGS_KEY_MONO_FONT_PATH = "monoFontPath"; constexpr auto* SETTINGS_KEY_MONO_FONT_PATH = "monoFontPath";
constexpr auto* SETTINGS_KEY_THEME_MODE = "themeMode";
constexpr auto* SETTINGS_KEY_MONO_THEME = "monoTheme";
constexpr auto* SETTINGS_KEY_ANIMATIONS = "animations";
constexpr auto* SETTINGS_KEY_PRIMARY_COLOR = "primaryColor";
constexpr auto* SETTINGS_KEY_SECONDARY_COLOR = "secondaryColor";
constexpr auto* SETTINGS_KEY_ERROR_COLOR = "errorColor";
static std::optional<uint32_t> parseColor(const std::map<std::string, std::string>& map, const char* key) {
const auto entry = map.find(key);
if (entry == map.end() || entry->second.empty()) {
return std::nullopt;
}
return static_cast<uint32_t>(std::strtoul(entry->second.c_str(), nullptr, 16)) & 0xFFFFFF;
}
static void putColor(std::map<std::string, std::string>& map, const char* key, std::optional<uint32_t> color) {
if (color.has_value()) {
char text[8];
std::snprintf(text, sizeof(text), "%06" PRIX32, *color);
map[key] = text;
}
}
static std::string getSettingsFilePath() { static std::string getSettingsFilePath() {
char path[256]; char path[256];
@ -43,6 +67,22 @@ bool load(AppearanceSettings& settings) {
if (const auto entry = map.find(SETTINGS_KEY_MONO_FONT_PATH); entry != map.end()) { if (const auto entry = map.find(SETTINGS_KEY_MONO_FONT_PATH); entry != map.end()) {
settings.monoFontPath = entry->second; settings.monoFontPath = entry->second;
} }
if (const auto entry = map.find(SETTINGS_KEY_THEME_MODE); entry != map.end()) {
if (entry->second == "light") {
settings.themeMode = ThemeMode::Light;
} else if (entry->second == "dark") {
settings.themeMode = ThemeMode::Dark;
}
}
if (const auto entry = map.find(SETTINGS_KEY_MONO_THEME); entry != map.end()) {
settings.monoTheme = entry->second == "true";
}
if (const auto entry = map.find(SETTINGS_KEY_ANIMATIONS); entry != map.end()) {
settings.animationsEnabled = entry->second != "false";
}
settings.primaryColor = parseColor(map, SETTINGS_KEY_PRIMARY_COLOR);
settings.secondaryColor = parseColor(map, SETTINGS_KEY_SECONDARY_COLOR);
settings.errorColor = parseColor(map, SETTINGS_KEY_ERROR_COLOR);
return true; return true;
} }
@ -63,6 +103,14 @@ bool save(const AppearanceSettings& settings) {
map[SETTINGS_KEY_FONT_SIZE] = std::to_string(settings.fontSize); map[SETTINGS_KEY_FONT_SIZE] = std::to_string(settings.fontSize);
map[SETTINGS_KEY_REGULAR_FONT_PATH] = settings.regularFontPath; map[SETTINGS_KEY_REGULAR_FONT_PATH] = settings.regularFontPath;
map[SETTINGS_KEY_MONO_FONT_PATH] = settings.monoFontPath; map[SETTINGS_KEY_MONO_FONT_PATH] = settings.monoFontPath;
if (settings.themeMode != ThemeMode::DeviceDefault) {
map[SETTINGS_KEY_THEME_MODE] = settings.themeMode == ThemeMode::Dark ? "dark" : "light";
}
map[SETTINGS_KEY_MONO_THEME] = settings.monoTheme ? "true" : "false";
map[SETTINGS_KEY_ANIMATIONS] = settings.animationsEnabled ? "true" : "false";
putColor(map, SETTINGS_KEY_PRIMARY_COLOR, settings.primaryColor);
putColor(map, SETTINGS_KEY_SECONDARY_COLOR, settings.secondaryColor);
putColor(map, SETTINGS_KEY_ERROR_COLOR, settings.errorColor);
return file::savePropertiesFile(settings_path, map); return file::savePropertiesFile(settings_path, map);
} }