From 2522f5da71421aee0613025cd730230cafaab338 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Wed, 7 Oct 2026 00:07:37 +0200 Subject: [PATCH] Theme settings app and memory optimisation --- Modules/lvgl-module/include/lvgl/theme.h | 42 +++ .../lvgl-module/include/lvgl/widgets/chip.h | 20 ++ .../lvgl-module/private/lvgl/theme_private.h | 24 ++ .../lvgl-module/source/devices/devices.cpp | 4 + .../lvgl-module/source/devices/display.cpp | 30 +-- Modules/lvgl-module/source/symbols.c | 8 + Modules/lvgl-module/source/theme.cpp | 124 +++++++++ .../source/themes/lv_theme_material.c | 37 +++ .../source/themes/lv_theme_material_mono.c | 25 ++ Modules/lvgl-module/source/widgets/chip.cpp | 26 ++ Modules/lvgl-module/themes/material.lss | 38 +++ Modules/lvgl-module/themes/material_mono.lss | 26 ++ .../Tactility/settings/AppearanceSettings.h | 16 ++ Tactility/Private/Tactility/lvgl/Theme.h | 13 + .../appearancesettings/AppearanceSettings.cpp | 253 +++++++++++++++++- Tactility/Source/app/boot/BootInit.cpp | 3 + Tactility/Source/lvgl/Theme.cpp | 27 ++ .../Source/settings/AppearanceSettings.cpp | 48 ++++ 18 files changed, 731 insertions(+), 33 deletions(-) create mode 100644 Modules/lvgl-module/include/lvgl/theme.h create mode 100644 Modules/lvgl-module/include/lvgl/widgets/chip.h create mode 100644 Modules/lvgl-module/private/lvgl/theme_private.h create mode 100644 Modules/lvgl-module/source/theme.cpp create mode 100644 Modules/lvgl-module/source/widgets/chip.cpp create mode 100644 Tactility/Private/Tactility/lvgl/Theme.h create mode 100644 Tactility/Source/lvgl/Theme.cpp diff --git a/Modules/lvgl-module/include/lvgl/theme.h b/Modules/lvgl-module/include/lvgl/theme.h new file mode 100644 index 000000000..c5d527049 --- /dev/null +++ b/Modules/lvgl-module/include/lvgl/theme.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#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 diff --git a/Modules/lvgl-module/include/lvgl/widgets/chip.h b/Modules/lvgl-module/include/lvgl/widgets/chip.h new file mode 100644 index 000000000..9b85ef149 --- /dev/null +++ b/Modules/lvgl-module/include/lvgl/widgets/chip.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#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 diff --git a/Modules/lvgl-module/private/lvgl/theme_private.h b/Modules/lvgl-module/private/lvgl/theme_private.h new file mode 100644 index 000000000..b73fca735 --- /dev/null +++ b/Modules/lvgl-module/private/lvgl/theme_private.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#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 diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp index 3ecf5bd5f..27238f7f2 100644 --- a/Modules/lvgl-module/source/devices/devices.cpp +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -13,6 +13,8 @@ #include +#include + #include constexpr auto* TAG = "lvgl"; @@ -268,6 +270,8 @@ void lvgl_devices_detach() { display = lv_disp_get_next(NULL); } + lvgl_theme_deinit(); + lvgl_unlock(); } diff --git a/Modules/lvgl-module/source/devices/display.cpp b/Modules/lvgl-module/source/devices/display.cpp index 4a7326810..3e0ff0ee2 100644 --- a/Modules/lvgl-module/source/devices/display.cpp +++ b/Modules/lvgl-module/source/devices/display.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -11,8 +12,6 @@ #include -#include "../themes/lv_theme_material.h" -#include "../themes/lv_theme_material_mono.h" #include @@ -24,31 +23,10 @@ constexpr auto* TAG = "lvgl_display"; // 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) { -#if defined(LV_THEME_DEFAULT_DARK) && LV_THEME_DEFAULT_DARK - bool is_dark = true; -#else - bool is_dark = false; -#endif - bool is_compact = lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT; + bool is_mono_display = color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8; // Slow-refresh panels (e-paper) can't show animations - bool animations_enabled = !display_has_capability(device, DISPLAY_CAPABILITY_SLOW_REFRESH); - - 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); + 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)); } struct LvglDisplayCtx { diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index 80df6469c..dee5ff830 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -10,7 +10,9 @@ #include #include +#include #include +#include #include #include #include @@ -58,8 +60,14 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lvgl_pointer_add), DEFINE_MODULE_SYMBOL(lvgl_pointer_get_slot_index), 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 DEFINE_MODULE_SYMBOL(lvgl_card_create), + // lvgl_chip + DEFINE_MODULE_SYMBOL(lvgl_chip_create), // lvgl_icon_button DEFINE_MODULE_SYMBOL(lvgl_icon_button_create), DEFINE_MODULE_SYMBOL(lvgl_icon_button_create_variant), diff --git a/Modules/lvgl-module/source/theme.cpp b/Modules/lvgl-module/source/theme.cpp new file mode 100644 index 000000000..6918cd13d --- /dev/null +++ b/Modules/lvgl-module/source/theme.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include + +#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; +} diff --git a/Modules/lvgl-module/source/themes/lv_theme_material.c b/Modules/lvgl-module/source/themes/lv_theme_material.c index 47e34fdd5..d71fcd4c3 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material.c @@ -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_sliderbox_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_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_PAD DENSITY_CONTAINER_PAD #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_OPACITY (LV_OPA_40) #define SCROLLBAR_SCROLLED_OPACITY (LV_OPA_70) @@ -324,6 +332,8 @@ typedef struct { lv_style_t screen; lv_style_t container; lv_style_t card; + lv_style_t chip; + lv_style_t chip_selected; #if LV_USE_BUTTON lv_style_t button; #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_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 style_init_reset(&theme->styles.button); 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; } #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)) { /* card */ lv_obj_add_style(obj, &theme->styles.card, 0); diff --git a/Modules/lvgl-module/source/themes/lv_theme_material_mono.c b/Modules/lvgl-module/source/themes/lv_theme_material_mono.c index e1b8f2db3..acc2264ed 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material_mono.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material_mono.c @@ -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_sliderbox_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 WHITE lv_color_white() @@ -82,6 +83,9 @@ extern const lv_obj_class_t lvgl_card_class; #define CARD_SHAPE SHAPE_CONTAINER #define CARD_PAD DENSITY_CONTAINER_PAD #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_WIDTH (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 3)) #define SCROLLBAR_PAD 1 @@ -209,6 +213,7 @@ typedef struct { lv_style_t screen; lv_style_t container; lv_style_t card; + lv_style_t chip; #if LV_USE_BUTTON || LV_USE_MSGBOX lv_style_t button; #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_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 style_init_reset(&theme->styles.button); 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; } #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)) { /* card */ lv_obj_add_style(obj, &theme->styles.card, 0); diff --git a/Modules/lvgl-module/source/widgets/chip.cpp b/Modules/lvgl-module/source/widgets/chip.cpp new file mode 100644 index 000000000..c544954a4 --- /dev/null +++ b/Modules/lvgl-module/source/widgets/chip.cpp @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration + +#include + +// 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; +} diff --git a/Modules/lvgl-module/themes/material.lss b/Modules/lvgl-module/themes/material.lss index c21f2bc92..4df1ff37a 100644 --- a/Modules/lvgl-module/themes/material.lss +++ b/Modules/lvgl-module/themes/material.lss @@ -27,6 +27,7 @@ @widget icon_button_tonal: lvgl_icon_button_tonal_class; @widget sliderbox: lvgl_sliderbox_class; @widget card: lvgl_card_class; +@widget chip: lvgl_chip_class; // ============================================================ // 1. Base tokens @@ -188,6 +189,15 @@ @card-pad: @density-container-pad; @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-color: @color-outline; @scrollbar-opacity: 40%; @@ -551,6 +561,23 @@ card { 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 { bgColor: @button-container-color; bgOpa: cover; @@ -1353,6 +1380,17 @@ apply(checkbox) { 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) { card: default; scrollbar: part.scrollbar; diff --git a/Modules/lvgl-module/themes/material_mono.lss b/Modules/lvgl-module/themes/material_mono.lss index a8688cac0..eca349205 100644 --- a/Modules/lvgl-module/themes/material_mono.lss +++ b/Modules/lvgl-module/themes/material_mono.lss @@ -28,6 +28,7 @@ @widget icon_button_tonal: lvgl_icon_button_tonal_class; @widget sliderbox: lvgl_sliderbox_class; @widget card: lvgl_card_class; +@widget chip: lvgl_chip_class; // ============================================================ // 1. Base tokens @@ -135,6 +136,11 @@ @card-pad: @density-container-pad; @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-color: @color-on-surface; @scrollbar-width: if($isCompact, 2, 3dp); @@ -381,6 +387,17 @@ card { 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 { bgColor: @button-container-color; bgOpa: cover; @@ -1026,6 +1043,15 @@ apply(checkbox) { 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) { card: default; scrollbar: part.scrollbar; diff --git a/Tactility/Include/Tactility/settings/AppearanceSettings.h b/Tactility/Include/Tactility/settings/AppearanceSettings.h index 437b590cd..5e39c246a 100644 --- a/Tactility/Include/Tactility/settings/AppearanceSettings.h +++ b/Tactility/Include/Tactility/settings/AppearanceSettings.h @@ -1,10 +1,18 @@ #pragma once #include +#include #include namespace tt::settings::appearance { +enum class ThemeMode { + /** Light or dark, as configured for the device */ + DeviceDefault, + Light, + Dark +}; + struct AppearanceSettings { /** Default text font size in pixels, or 0 for the device's default size */ uint16_t fontSize = 0; @@ -12,6 +20,14 @@ struct AppearanceSettings { std::string regularFontPath; /** TrueType font for monospace text, or empty for the system font */ 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 primaryColor; + std::optional secondaryColor; + std::optional errorColor; bool operator==(const AppearanceSettings&) const = default; }; diff --git a/Tactility/Private/Tactility/lvgl/Theme.h b/Tactility/Private/Tactility/lvgl/Theme.h new file mode 100644 index 000000000..09c5c35cd --- /dev/null +++ b/Tactility/Private/Tactility/lvgl/Theme.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +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); + +} diff --git a/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp b/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp index 0c64cf4b2..c73238725 100644 --- a/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp +++ b/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,9 @@ #include #include +#include +#include +#include #include #include @@ -42,6 +46,33 @@ enum class FontSlot { 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 { lv_obj_t* defaultButton = nullptr; lv_obj_t* fileLabel = nullptr; @@ -80,6 +111,13 @@ struct Context { FontRowWidgets regularRow; FontRowWidgets monoRow; 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) { @@ -120,6 +158,47 @@ void updateFontRow(const FontRowWidgets& row, const std::string& path, size_t ch 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. */ void updateWidgets(Context* ctx) { if (ctx->applyButton == nullptr) { @@ -131,6 +210,64 @@ void updateWidgets(Context* ctx) { updateFontRow(ctx->regularRow, ctx->pendingSettings.regularFontPath, ctx->regularCharacterCount); updateFontRow(ctx->monoRow, ctx->pendingSettings.monoFontPath, ctx->monoCharacterCount); 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(lv_event_get_user_data(event)), false); +} + +void onDarkPressed(lv_event_t* event) { + setDark(static_cast(lv_event_get_user_data(event)), true); +} + +void onRegularThemePressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->pendingSettings.monoTheme = false; + updateWidgets(ctx); +} + +void onMonoThemePressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->pendingSettings.monoTheme = true; + updateWidgets(ctx); +} + +void onAnimationsChanged(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* checkbox = static_cast(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(lv_event_get_user_data(event)); + auto* dropdown = static_cast(lv_event_get_target(event)); + auto* color = static_cast*>(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) { @@ -194,20 +331,99 @@ void onDefaultMonoPressed(lv_event_t* event) { 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) { 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_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_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 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_border_width(row, 0, LV_STATE_DEFAULT); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); 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* 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) { auto* button = lv_button_create(parent); 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_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); lv_label_set_text(font_size_label, "Font size"); 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)); createStepButton(font_size_row, LV_SYMBOL_PLUS, onFontSizeIncrementPressed, font_size_spinbox); - ctx->regularRow = createFontRow(content, "Regular font", onSelectRegularPressed, onDefaultRegularPressed, ctx); - ctx->monoRow = createFontRow(content, "Mono font", onSelectMonoPressed, onDefaultMonoPressed, ctx); + ctx->regularRow = createFontRow(fonts_card, "Regular font", onSelectRegularPressed, onDefaultRegularPressed, 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_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."); @@ -296,6 +517,13 @@ void destroyWidgets(void* userData) { ctx->regularRow = {}; ctx->monoRow = {}; 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) { @@ -313,6 +541,8 @@ void cancelApply(Context* ctx) { lvgl_unlock(); } +void finishApply(Context* ctx); + void startApply(Context* ctx) { if (ctx->applying || ctx->applyThread != nullptr) { return; @@ -320,11 +550,19 @@ void startApply(Context* ctx) { lvgl_lock(); ctx->applyConfiguration = toFontConfiguration(ctx->pendingSettings); + const bool fonts_changed = ctx->applyConfiguration != toFontConfiguration(ctx->savedSettings); ctx->applying = true; ctx->applyFailed = false; updateWidgets(ctx); lvgl_unlock(); + // Theme changes don't need new fonts + if (!fonts_changed) { + ctx->cacheUpdateSucceeded = true; + finishApply(ctx); + return; + } + ctx->applyThread = thread_alloc(); if (ctx->applyThread == nullptr) { LOG_E(TAG, "Failed to create thread"); @@ -378,8 +616,9 @@ void finishApply(Context* ctx) { lvgl_unlock(); // 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::configureTheme(applied_settings); lvgl::loadFonts(ctx->applyConfiguration); lvgl::start(); } diff --git a/Tactility/Source/app/boot/BootInit.cpp b/Tactility/Source/app/boot/BootInit.cpp index 9238cfc74..ab92b153e 100644 --- a/Tactility/Source/app/boot/BootInit.cpp +++ b/Tactility/Source/app/boot/BootInit.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include @@ -271,6 +273,7 @@ bool bootInit(TickType_t startTime) { LOG_I(TAG, "Loading fonts"); lvgl::loadFonts(lvgl::loadFontConfiguration()); + lvgl::configureTheme(settings::appearance::loadOrGetDefault()); screen.end(); diff --git a/Tactility/Source/lvgl/Theme.cpp b/Tactility/Source/lvgl/Theme.cpp new file mode 100644 index 000000000..103b5096c --- /dev/null +++ b/Tactility/Source/lvgl/Theme.cpp @@ -0,0 +1,27 @@ +#include + +#include + +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); +} + +} diff --git a/Tactility/Source/settings/AppearanceSettings.cpp b/Tactility/Source/settings/AppearanceSettings.cpp index 968fda084..e3a9ab2f8 100644 --- a/Tactility/Source/settings/AppearanceSettings.cpp +++ b/Tactility/Source/settings/AppearanceSettings.cpp @@ -5,6 +5,8 @@ #include +#include +#include #include #include @@ -13,6 +15,28 @@ namespace tt::settings::appearance { constexpr auto* SETTINGS_KEY_FONT_SIZE = "fontSize"; constexpr auto* SETTINGS_KEY_REGULAR_FONT_PATH = "regularFontPath"; 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 parseColor(const std::map& map, const char* key) { + const auto entry = map.find(key); + if (entry == map.end() || entry->second.empty()) { + return std::nullopt; + } + return static_cast(std::strtoul(entry->second.c_str(), nullptr, 16)) & 0xFFFFFF; +} + +static void putColor(std::map& map, const char* key, std::optional color) { + if (color.has_value()) { + char text[8]; + std::snprintf(text, sizeof(text), "%06" PRIX32, *color); + map[key] = text; + } +} static std::string getSettingsFilePath() { 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()) { 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; } @@ -63,6 +103,14 @@ bool save(const AppearanceSettings& settings) { map[SETTINGS_KEY_FONT_SIZE] = std::to_string(settings.fontSize); map[SETTINGS_KEY_REGULAR_FONT_PATH] = settings.regularFontPath; 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); }