Remove default theme

This commit is contained in:
Ken Van Hoeylandt 2026-10-07 08:39:52 +02:00
parent 2522f5da71
commit 60133ddda6
22 changed files with 300 additions and 122 deletions

View File

@ -22,5 +22,3 @@ lvgl.colorDepth=8
lvgl.fontSize=24
lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0

View File

@ -23,11 +23,7 @@ display.shape=rectangle
display.dpi=128
lvgl.colorDepth=8
# Monochrome theme: the default colour theme renders accent-coloured UI that
# thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO).
lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
cdn.warningMessage=Display not reliably working. Use at your own risk.
sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0

View File

@ -22,11 +22,7 @@ display.shape=rectangle
display.dpi=128
lvgl.colorDepth=8
# Monochrome theme: the default colour theme renders accent-coloured UI that
# thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO).
lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
cdn.warningMessage=Only for hardware revision 1.0! Display not reliably working. Use at your own risk.
sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0

View File

@ -25,4 +25,3 @@ lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
sdkconfig.CONFIG_EPD_DISPLAY_TYPE_ED047TC2=y
sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0

View File

@ -29,6 +29,15 @@ else ()
set(ui_density_variable "LVGL_UI_DENSITY_DEFAULT")
message("UI density set to default: ${ui_density}")
endif ()
# Read the theme: whether the device uses dark mode by default
GET_VALUE_FROM_MAP(device_properties "lvgl.theme" lvgl_theme)
if (NOT lvgl_theme OR lvgl_theme STREQUAL "DefaultDark")
set(theme_dark 1)
elseif (lvgl_theme STREQUAL "DefaultLight" OR lvgl_theme STREQUAL "Mono")
set(theme_dark 0)
else ()
message(FATAL_ERROR "Invalid lvgl.theme: '${lvgl_theme}'. Must be 'DefaultDark', 'DefaultLight' or 'Mono'")
endif ()
# ####################################
# Create module
@ -74,4 +83,6 @@ target_compile_definitions(${MODULE_NAME} PUBLIC
"-DLV_LVGL_H_INCLUDE_SIMPLE"
# UiDensity
"-DTT_LVGL_UI_DENSITY=${ui_density_variable}"
# Default theme mode: 1 for dark, 0 for light
"-DTT_LVGL_THEME_DARK=${theme_dark}"
)

View File

@ -31,6 +31,12 @@ void lvgl_theme_get_settings(LvglThemeSettings* settings);
*/
void lvgl_theme_get_default_settings(LvglThemeSettings* settings);
/**
* @brief Checks whether the active theme is the monochrome theme: on monochrome and greyscale displays, or when selected in the settings.
* @return true when the monochrome theme is active
*/
bool lvgl_theme_is_mono(void);
/**
* @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

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 badge: a small dot that marks its parent (e.g. a favourite item).
* The theme determines its size and colour. It ignores the parent's layout, so the app aligns it (e.g. LV_ALIGN_TOP_RIGHT).
* @param[in] parent the object to mark
* @return the created badge
*/
lv_obj_t* lvgl_badge_create(lv_obj_t* parent);
#ifdef __cplusplus
}
#endif

View File

@ -11,6 +11,7 @@
#include <lvgl/widgets/sliderbox.h>
#include <lvgl/theme.h>
#include <lvgl/widgets/badge.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/chip.h>
#include <lvgl/widgets/icon_button.h>
@ -64,6 +65,9 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_theme_get_settings),
DEFINE_MODULE_SYMBOL(lvgl_theme_get_default_settings),
DEFINE_MODULE_SYMBOL(lvgl_theme_set_settings),
DEFINE_MODULE_SYMBOL(lvgl_theme_is_mono),
// lvgl_badge
DEFINE_MODULE_SYMBOL(lvgl_badge_create),
// lvgl_card
DEFINE_MODULE_SYMBOL(lvgl_card_create),
// lvgl_chip

View File

@ -23,11 +23,7 @@ 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_dark = TT_LVGL_THEME_DARK;
settings->is_mono = false;
settings->animations_enabled = true;
settings->color_primary = material_config.color_primary;
@ -48,6 +44,10 @@ void lvgl_theme_set_settings(const LvglThemeSettings* settings) {
theme_settings_set = true;
}
bool lvgl_theme_is_mono() {
return active_theme == ActiveTheme::MaterialMono;
}
static lv_theme_t* get_active_theme() {
switch (active_theme) {
case ActiveTheme::Material:

View File

@ -15,6 +15,7 @@ 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;
extern const lv_obj_class_t lvgl_badge_class;
#define NEUTRAL_4 lv_color_hex(0x0E0E0E)
#define NEUTRAL_6 lv_color_hex(0x131313)
@ -136,6 +137,8 @@ extern const lv_obj_class_t lvgl_chip_class;
#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 BADGE_COLOR COLOR_PRIMARY
#define BADGE_SIZE (theme->config.is_compact ? 4 : 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)
@ -159,7 +162,6 @@ extern const lv_obj_class_t lvgl_chip_class;
#define TEXT_BUTTON_PRESSED_CONTAINER_COLOR COLOR_PRIMARY
#define TEXT_BUTTON_PAD_HOR (theme->config.is_compact ? 3 : SPACE_3)
#define ICON_BUTTON_COLOR COLOR_ON_SURFACE_VARIANT
#define ICON_BUTTON_SELECTED_COLOR COLOR_PRIMARY
#define ICON_BUTTON_CONTAINER_SHAPE (theme->config.is_compact ? CORNER_COMPACT : CORNER_LARGE)
#define ICON_BUTTON_FILLED_COLOR COLOR_PRIMARY
#define ICON_BUTTON_FILLED_ICON_COLOR COLOR_ON_PRIMARY
@ -332,6 +334,7 @@ typedef struct {
lv_style_t screen;
lv_style_t container;
lv_style_t card;
lv_style_t badge;
lv_style_t chip;
lv_style_t chip_selected;
#if LV_USE_BUTTON
@ -354,7 +357,6 @@ typedef struct {
lv_style_t text_button_pressed;
#endif
lv_style_t icon_button;
lv_style_t icon_button_selected;
lv_style_t icon_button_filled;
lv_style_t icon_button_filled_pressed;
lv_style_t icon_button_tonal;
@ -757,6 +759,13 @@ 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.badge);
lv_style_set_width(&theme->styles.badge, BADGE_SIZE);
lv_style_set_height(&theme->styles.badge, BADGE_SIZE);
lv_style_set_radius(&theme->styles.badge, LV_RADIUS_CIRCLE);
lv_style_set_bg_color(&theme->styles.badge, BADGE_COLOR);
lv_style_set_bg_opa(&theme->styles.badge, LV_OPA_COVER);
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);
@ -827,9 +836,6 @@ static void style_init(my_theme_t * theme)
lv_style_set_radius(&theme->styles.icon_button, BUTTON_SHAPE);
lv_style_set_pad_all(&theme->styles.icon_button, BUTTON_PAD_VER);
style_init_reset(&theme->styles.icon_button_selected);
lv_style_set_text_color(&theme->styles.icon_button_selected, ICON_BUTTON_SELECTED_COLOR);
style_init_reset(&theme->styles.icon_button_filled);
lv_style_set_bg_color(&theme->styles.icon_button_filled, ICON_BUTTON_FILLED_COLOR);
lv_style_set_bg_opa(&theme->styles.icon_button_filled, LV_OPA_COVER);
@ -1713,7 +1719,6 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
}
/* icon_button */
lv_obj_add_style(obj, &theme->styles.icon_button, 0);
lv_obj_add_style(obj, &theme->styles.icon_button_selected, LV_STATE_CHECKED);
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);
@ -1853,6 +1858,11 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
return;
}
#endif
if(lv_obj_check_type(obj, &lvgl_badge_class)) {
/* badge */
lv_obj_add_style(obj, &theme->styles.badge, 0);
return;
}
if(lv_obj_check_type(obj, &lvgl_chip_class)) {
/* chip */
lv_obj_add_style(obj, &theme->styles.chip, 0);

View File

@ -15,6 +15,7 @@ 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;
extern const lv_obj_class_t lvgl_badge_class;
#define BLACK lv_color_black()
#define WHITE lv_color_white()
@ -86,6 +87,8 @@ extern const lv_obj_class_t lvgl_chip_class;
#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 BADGE_COLOR COLOR_ON_SURFACE
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
#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
@ -213,12 +216,12 @@ typedef struct {
lv_style_t screen;
lv_style_t container;
lv_style_t card;
lv_style_t badge;
lv_style_t chip;
#if LV_USE_BUTTON || LV_USE_MSGBOX
lv_style_t button;
#endif
lv_style_t icon_button;
lv_style_t icon_button_selected;
lv_style_t icon_button_filled;
lv_style_t icon_button_filled_pressed;
lv_style_t icon_button_tonal;
@ -546,6 +549,13 @@ 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.badge);
lv_style_set_width(&theme->styles.badge, BADGE_SIZE);
lv_style_set_height(&theme->styles.badge, BADGE_SIZE);
lv_style_set_radius(&theme->styles.badge, LV_RADIUS_CIRCLE);
lv_style_set_bg_color(&theme->styles.badge, BADGE_COLOR);
lv_style_set_bg_opa(&theme->styles.badge, LV_OPA_COVER);
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);
@ -576,10 +586,6 @@ static void style_init(my_theme_t * theme)
lv_style_set_radius(&theme->styles.icon_button, BUTTON_SHAPE);
lv_style_set_pad_all(&theme->styles.icon_button, BUTTON_PAD_VER);
style_init_reset(&theme->styles.icon_button_selected);
lv_style_set_border_color(&theme->styles.icon_button_selected, COLOR_OUTLINE);
lv_style_set_border_width(&theme->styles.icon_button_selected, BORDER);
style_init_reset(&theme->styles.icon_button_filled);
lv_style_set_bg_color(&theme->styles.icon_button_filled, ICON_BUTTON_FILLED_COLOR);
lv_style_set_bg_opa(&theme->styles.icon_button_filled, LV_OPA_COVER);
@ -1345,7 +1351,6 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
}
/* icon_button */
lv_obj_add_style(obj, &theme->styles.icon_button, 0);
lv_obj_add_style(obj, &theme->styles.icon_button_selected, LV_STATE_CHECKED);
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
lv_obj_add_style(obj, &theme->styles.focus_ring_inset, LV_STATE_FOCUS_KEY);
lv_obj_add_style(obj, &theme->styles.edit_ring_inset, LV_STATE_EDITED);
@ -1446,6 +1451,11 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
return;
}
#endif
if(lv_obj_check_type(obj, &lvgl_badge_class)) {
/* badge */
lv_obj_add_style(obj, &theme->styles.badge, 0);
return;
}
if(lv_obj_check_type(obj, &lvgl_chip_class)) {
/* chip */
lv_obj_add_style(obj, &theme->styles.chip, 0);

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration
#include <lvgl/widgets/badge.h>
static void badge_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
LV_UNUSED(class_p);
lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE);
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(obj, LV_OBJ_FLAG_IGNORE_LAYOUT);
}
// Not static: the theme styles badges by this class
extern "C" const lv_obj_class_t lvgl_badge_class = {
.base_class = &lv_obj_class,
.constructor_cb = badge_constructor,
.destructor_cb = nullptr,
.event_cb = nullptr,
.user_data = nullptr,
.name = "badge",
.width_def = 0,
.height_def = 0,
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_FALSE,
.instance_size = 0,
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
};
lv_obj_t* lvgl_badge_create(lv_obj_t* parent) {
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_badge_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}

View File

@ -28,6 +28,7 @@
@widget sliderbox: lvgl_sliderbox_class;
@widget card: lvgl_card_class;
@widget chip: lvgl_chip_class;
@widget badge: lvgl_badge_class;
// ============================================================
// 1. Base tokens
@ -198,6 +199,10 @@
@chip-pad-hor: if($isCompact, 3, @space-4);
@chip-pad-ver: if($isCompact, 2, 6dp);
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
@badge-color: @color-primary;
@badge-size: if($isCompact, 4, 6dp);
// Scrollbar
@scrollbar-color: @color-outline;
@scrollbar-opacity: 40%;
@ -228,7 +233,6 @@
// Icon button (dialog and window header buttons)
@icon-button-color: @color-on-surface-variant;
@icon-button-selected-color: @color-primary;
// Filled and tonal icon buttons: Material 3 large icon buttons with a container
@icon-button-container-shape: if($isCompact, @corner-compact, @corner-large);
@ -561,6 +565,14 @@ card {
padGap: @card-gap;
}
badge {
width: @badge-size;
height: @badge-size;
radius: circle;
bgColor: @badge-color;
bgOpa: cover;
}
chip {
bgOpa: transp;
textColor: @chip-label-color;
@ -629,11 +641,6 @@ icon_button {
padAll: @button-pad-ver;
}
// Material 3 selected (toggled) icon button
icon_button_selected {
textColor: @icon-button-selected-color;
}
icon_button_filled {
bgColor: @icon-button-filled-color;
bgOpa: cover;
@ -1295,7 +1302,6 @@ apply(toolbar) {
apply(icon_button) {
icon_button: default;
icon_button_selected: state.checked;
transition_exit: default;
state_pressed: state.pressed;
transition_enter: state.pressed;
@ -1380,6 +1386,10 @@ apply(checkbox) {
disabled: state.disabled;
}
apply(badge) {
badge: default;
}
apply(chip) {
chip: default;
transition_exit: default;

View File

@ -29,6 +29,7 @@
@widget sliderbox: lvgl_sliderbox_class;
@widget card: lvgl_card_class;
@widget chip: lvgl_chip_class;
@widget badge: lvgl_badge_class;
// ============================================================
// 1. Base tokens
@ -141,6 +142,10 @@
@chip-pad-hor: if($isCompact, 3, @space-4);
@chip-pad-ver: if($isCompact, 2, @space-2);
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
@badge-color: @color-on-surface;
@badge-size: if($isCompact, 4, 6dp);
// Scrollbar
@scrollbar-color: @color-on-surface;
@scrollbar-width: if($isCompact, 2, 3dp);
@ -387,6 +392,14 @@ card {
padGap: @card-gap;
}
badge {
width: @badge-size;
height: @badge-size;
radius: circle;
bgColor: @badge-color;
bgOpa: cover;
}
chip {
bgColor: @color-surface;
bgOpa: cover;
@ -418,12 +431,6 @@ icon_button {
padAll: @button-pad-ver;
}
// Selected (toggled) icon button: outlined, as there is no colour to show it with
icon_button_selected {
borderColor: @color-outline;
borderWidth: @border;
}
icon_button_filled {
bgColor: @icon-button-filled-color;
bgOpa: cover;
@ -971,7 +978,6 @@ apply(toolbar) {
apply(icon_button) {
icon_button: default;
icon_button_selected: state.checked;
inverted: state.pressed;
focus_ring_inset: state.focusKey;
edit_ring_inset: state.edited;
@ -1043,6 +1049,10 @@ apply(checkbox) {
disabled: part.indicator, state.disabled;
}
apply(badge) {
badge: default;
}
apply(chip) {
chip: default;
inverted: state.checked;

View File

@ -17,8 +17,7 @@ namespace tt::app::selectiondialog {
* @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the
* result as an APP_EVENT_RESULT in its own event loop: result is the selected item's index
* (>= 0), -1 if 0 items were provided (an error - the dialog auto-closes without showing
* anything), or a value not matching any item (currently always 1) if the dialog was
* dismissed without a selection. No result_bundle. If exactly 1 item is provided, the dialog
* anything), or -2 if the dialog was dismissed without a selection. No result_bundle. If exactly 1 item is provided, the dialog
* auto-closes with result index 0 without showing anything. The caller is responsible for
* calling app_manager_stop() on the returned instance id once it has handled the result.
* @return the new dialog's app instance id

View File

@ -2,6 +2,7 @@
#include <lvgl/devices/indev.h>
#include <lvgl/fonts.h>
#include <lvgl/widgets/badge.h>
#include <lvgl/widgets/icon_button.h>
#include <lvgl/widgets/toolbar.h>
@ -149,11 +150,8 @@ void AppGrid::populate() {
for (uint32_t i = first; i < last; i++) {
const auto& item = items[i];
// The theme styles the tile as an icon button: its focus, pressed and selected (favourite) states
// The theme styles the tile as an icon button, including its focus and pressed states
lv_obj_t* tile = lvgl_icon_button_create(grid);
if (item.highlighted) {
lv_obj_add_state(tile, LV_STATE_CHECKED);
}
lv_obj_set_size(tile, layout.tileWidth, layout.tileHeight);
lv_obj_set_style_pad_all(tile, layout.pad, LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(tile, layout.pad / 2, LV_STATE_DEFAULT);
@ -175,6 +173,12 @@ void AppGrid::populate() {
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_DOTS);
lv_label_set_text(label, item.manifest.name);
// A badge marks favourites, as it doesn't look like the focus indicator
if (item.highlighted) {
auto* badge = lvgl_badge_create(tile);
lv_obj_align(badge, LV_ALIGN_TOP_RIGHT, 0, 0);
}
lv_obj_add_event_cb(tile, onTileClicked, LV_EVENT_SHORT_CLICKED, this);
if (callbacks.onLongPressed != nullptr) {
lv_obj_add_event_cb(tile, onTileLongPressed, LV_EVENT_LONG_PRESSED, this);

View File

@ -1,4 +1,5 @@
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/app/selectiondialog/SelectionDialog.h>
#include <Tactility/file/File.h>
#include <Tactility/lvgl/Fonts.h>
#include <Tactility/lvgl/Lvgl.h>
@ -73,6 +74,14 @@ constexpr NamedColor PALETTE_COLORS[] = {
{ "Grey", LV_PALETTE_GREY },
};
constexpr size_t COLOR_COUNT = 3;
constexpr const char* COLOR_TITLES[COLOR_COUNT] = { "Primary color", "Secondary color", "Error color" };
struct ColorRowWidgets {
lv_obj_t* row = nullptr;
lv_obj_t* swatch = nullptr;
};
struct FontRowWidgets {
lv_obj_t* defaultButton = nullptr;
lv_obj_t* fileLabel = nullptr;
@ -84,6 +93,7 @@ struct Context {
TaskEventGroup* eventGroup = nullptr;
uint32_t selectRegularBit = 0;
uint32_t selectMonoBit = 0;
uint32_t selectColorBit = 0;
uint32_t applyBit = 0;
uint32_t cacheUpdatedBit = 0;
@ -101,6 +111,9 @@ struct Context {
std::atomic<bool> cacheUpdateSucceeded = false;
uint32_t selectLaunchId = 0;
/** The colour that the selection dialog is for */
size_t selectColorIndex = 0;
uint32_t colorSelectLaunchId = 0;
FontSlot selectSlot = FontSlot::Regular;
AppStream selectStream {};
uint8_t selectBuffer[256] {};
@ -115,7 +128,7 @@ struct Context {
lv_obj_t* darkChip = nullptr;
lv_obj_t* regularThemeChip = nullptr;
lv_obj_t* monoThemeChip = nullptr;
lv_obj_t* colorRows[3] = {};
ColorRowWidgets colorRows[COLOR_COUNT];
/** The display can only show the mono theme */
bool isMonoDisplay = false;
};
@ -183,6 +196,8 @@ void setChecked(lv_obj_t* object, bool checked) {
}
}
void updateColorRow(Context* ctx, size_t index);
void updateThemeWidgets(Context* ctx) {
const bool dark = isDark(ctx->pendingSettings);
setChecked(ctx->lightChip, !dark);
@ -192,9 +207,10 @@ void updateThemeWidgets(Context* ctx) {
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);
for (size_t i = 0; i < COLOR_COUNT; i++) {
if (ctx->colorRows[i].row != nullptr) {
setHidden(ctx->colorRows[i].row, ctx->pendingSettings.monoTheme);
updateColorRow(ctx, i);
}
}
}
@ -256,18 +272,35 @@ uint32_t getPaletteColor(lv_palette_t palette) {
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);
std::optional<uint32_t>& getColor(settings::appearance::AppearanceSettings& settings, size_t index) {
switch (index) {
case 0:
return settings.primaryColor;
case 1:
return settings.secondaryColor;
default:
return settings.errorColor;
}
// Any other option is the custom colour from the settings file, which stays unchanged
updateWidgets(ctx);
}
uint32_t getDefaultColor(size_t index) {
LvglThemeSettings defaults;
lvgl_theme_get_default_settings(&defaults);
const lv_color_t colors[COLOR_COUNT] = { defaults.color_primary, defaults.color_secondary, defaults.color_error };
return lv_color_to_u32(colors[index]) & 0xFFFFFF;
}
void updateColorRow(Context* ctx, size_t index) {
const auto& widgets = ctx->colorRows[index];
const auto& color = getColor(ctx->pendingSettings, index);
lv_obj_set_style_bg_color(widgets.swatch, lv_color_hex(color.value_or(getDefaultColor(index))), LV_STATE_DEFAULT);
}
void onColorButtonPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* button = static_cast<lv_obj_t*>(lv_event_get_target(event));
ctx->selectColorIndex = reinterpret_cast<uintptr_t>(lv_obj_get_user_data(button));
task_event_group_signal(ctx->eventGroup, ctx->selectColorBit);
}
void onBackPressed(lv_event_t* event) {
@ -373,29 +406,32 @@ lv_obj_t* createChip(lv_obj_t* parent, const char* text, lv_event_cb_t callback,
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);
ColorRowWidgets createColorRow(lv_obj_t* parent, size_t index, Context* ctx) {
ColorRowWidgets widgets;
// "Title [swatch] [Change]"
widgets.row = createLabeledRow(parent, COLOR_TITLES[index]);
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;
// The colour preview: its colour is the setting, its border uses the theme's text colour
widgets.swatch = lv_obj_create(widgets.row);
lv_obj_remove_style_all(widgets.swatch);
lv_obj_remove_flag(widgets.swatch, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(widgets.swatch, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_opa(widgets.swatch, LV_OPA_COVER, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(widgets.swatch, 1, LV_STATE_DEFAULT);
lv_obj_set_style_border_color(widgets.swatch, lv_obj_get_style_text_color(widgets.swatch, LV_PART_MAIN), LV_STATE_DEFAULT);
lv_obj_set_style_radius(widgets.swatch, LV_DPX(4), LV_STATE_DEFAULT);
auto* button = lv_button_create(widgets.row);
auto* button_label = lv_label_create(button);
lv_label_set_text(button_label, "Change");
lv_obj_set_user_data(button, reinterpret_cast<void*>(static_cast<uintptr_t>(index)));
lv_obj_add_event_cb(button, onColorButtonPressed, LV_EVENT_SHORT_CLICKED, ctx);
// As high as the button
lv_obj_update_layout(button);
const int32_t swatch_size = lv_obj_get_height(button);
lv_obj_set_size(widgets.swatch, swatch_size, swatch_size);
return widgets;
}
void createThemeCard(lv_obj_t* parent, Context* ctx) {
@ -418,9 +454,9 @@ void createThemeCard(lv_obj_t* parent, Context* ctx) {
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);
for (size_t i = 0; i < COLOR_COUNT; i++) {
ctx->colorRows[i] = createColorRow(card, i, ctx);
}
}
}
@ -521,8 +557,8 @@ void destroyWidgets(void* userData) {
ctx->darkChip = nullptr;
ctx->regularThemeChip = nullptr;
ctx->monoThemeChip = nullptr;
for (auto*& row : ctx->colorRows) {
row = nullptr;
for (auto& row : ctx->colorRows) {
row = {};
}
}
@ -631,6 +667,37 @@ void startFileSelection(Context* ctx, FontSlot slot) {
ctx->selectLaunchId = fileselection::startForExistingFile(ctx->appInstanceId, ctx->selectStream, ctx->selectBuffer, sizeof(ctx->selectBuffer), ctx->eventGroup);
}
void startColorSelection(Context* ctx) {
if (ctx->colorSelectLaunchId != 0 || ctx->applying) {
return;
}
std::vector<std::string> items;
items.reserve(PALETTE_COLOR_COUNT + 1);
items.emplace_back("Default");
for (const auto& named_color : PALETTE_COLORS) {
items.emplace_back(named_color.name);
}
ctx->colorSelectLaunchId = selectiondialog::start(ctx->appInstanceId, COLOR_TITLES[ctx->selectColorIndex], items);
}
void onColorSelectionResult(Context* ctx, const AppEvent& event) {
ctx->colorSelectLaunchId = 0;
// Index 0 is "Default", the others are the palette colours. Other results mean that the dialog was dismissed.
const int32_t selected = event.result.result;
if (selected >= 0 && selected <= static_cast<int32_t>(PALETTE_COLOR_COUNT)) {
lvgl_lock();
auto& color = getColor(ctx->pendingSettings, ctx->selectColorIndex);
if (selected == 0) {
color.reset();
} else {
color = getPaletteColor(PALETTE_COLORS[selected - 1].palette);
}
updateWidgets(ctx);
lvgl_unlock();
}
app_manager_stop(event.result.launch_id);
}
void onFileSelectionResult(Context* ctx, const AppEvent& event) {
ctx->selectLaunchId = 0;
if (event.result.result == 0 /* Ok */) {
@ -665,6 +732,7 @@ int32_t appMain(int argc, char* argv[]) {
ctx.eventGroup = &event_group;
check(task_event_group_claim_bit(&event_group, &ctx.selectRegularBit) == ERROR_NONE);
check(task_event_group_claim_bit(&event_group, &ctx.selectMonoBit) == ERROR_NONE);
check(task_event_group_claim_bit(&event_group, &ctx.selectColorBit) == ERROR_NONE);
check(task_event_group_claim_bit(&event_group, &ctx.applyBit) == ERROR_NONE);
check(task_event_group_claim_bit(&event_group, &ctx.cacheUpdatedBit) == ERROR_NONE);
@ -684,6 +752,9 @@ int32_t appMain(int argc, char* argv[]) {
if (flags & ctx.selectMonoBit) {
startFileSelection(&ctx, FontSlot::Mono);
}
if (flags & ctx.selectColorBit) {
startColorSelection(&ctx);
}
if (flags & ctx.applyBit) {
startApply(&ctx);
}
@ -697,6 +768,8 @@ int32_t appMain(int argc, char* argv[]) {
should_close = true;
} else if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.selectLaunchId) {
onFileSelectionResult(&ctx, event);
} else if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.colorSelectLaunchId) {
onColorSelectionResult(&ctx, event);
}
}
}

View File

@ -10,6 +10,7 @@
#include <lvgl/icons/launcher.h>
#include <lvgl/fonts.h>
#include <lvgl/lvgl.h>
#include <lvgl/theme.h>
#include <lvgl/widgets/icon_button.h>
#include <lvgl_window_manager/window_manager.h>
@ -151,9 +152,11 @@ void createWidgets(lv_obj_t* parent, void*) {
}
// The app list is the main action, so it stands out more than the others
auto* app_list_button = createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_APPS, "tactility.applist");
createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files");
createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings");
// Filled buttons would be solid black blocks with the monochrome theme
const auto variant = lvgl_theme_is_mono() ? LVGL_ICON_BUTTON_STANDARD : LVGL_ICON_BUTTON_FILLED;
auto* app_list_button = createAppButton(buttons_wrapper, variant, LVGL_ICON_LAUNCHER_APPS, "tactility.applist");
createAppButton(buttons_wrapper, variant, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files");
createAppButton(buttons_wrapper, variant, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings");
applyButtonMargins(buttons_wrapper, is_landscape_display);
// The launcher's container is several levels below the screen, and LVGL only sends

View File

@ -32,7 +32,7 @@ struct Context {
// The eventual appMain() return value - see AlertDialog.cpp's Context::result for why this
// is a plain (non-atomic) field safely shared between the LVGL thread (writer, before
// emitting APP_EVENT_CLOSE) and this dialog's own thread (reader, after waking from it).
int32_t result = 1; // Cancelled - safety-net default if closed without selecting an item
int32_t result = -2; // Cancelled - safety-net default if closed without selecting an item
};
struct ItemContext {

View File

@ -30,11 +30,25 @@ static std::optional<uint32_t> parseColor(const std::map<std::string, std::strin
return static_cast<uint32_t>(std::strtoul(entry->second.c_str(), nullptr, 16)) & 0xFFFFFF;
}
// Saving only updates the keys that are written, so an unset colour is written as an empty value
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;
} else {
map[key] = "";
}
}
static const char* toString(ThemeMode mode) {
switch (mode) {
case ThemeMode::Light:
return "light";
case ThemeMode::Dark:
return "dark";
default:
return "default";
}
}
@ -103,9 +117,7 @@ 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_THEME_MODE] = toString(settings.themeMode);
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);

View File

@ -292,20 +292,13 @@ def write_lvgl_variables(output_file, device_properties: dict):
has_statusbar_colors_inverted = get_boolean_property_or_false(device_properties, "lvgl.statusbarColorsInverted")
if has_statusbar_colors_inverted:
output_file.write("CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED=y\n")
# Tactility uses its own themes (Modules/lvgl-module), and lvgl.theme only selects the default light or dark mode
theme = get_property_or_default(device_properties, "lvgl.theme", "DefaultDark")
if theme == "DefaultDark":
output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n")
elif theme == "DefaultLight":
output_file.write("CONFIG_LV_THEME_DEFAULT_LIGHT=y\n")
elif theme == "Mono":
output_file.write("CONFIG_LV_USE_THEME_MONO=y\n")
# LVGL selects the theme with #if LV_USE_THEME_DEFAULT / #elif LV_USE_THEME_SIMPLE /
# #elif LV_USE_THEME_MONO, and the Kconfig defaults enable DEFAULT+SIMPLE for any
# non-1bpp color depth so MONO only takes effect once those are disabled.
output_file.write("CONFIG_LV_USE_THEME_DEFAULT=n\n")
output_file.write("CONFIG_LV_USE_THEME_SIMPLE=n\n")
else:
if theme not in ("DefaultDark", "DefaultLight", "Mono"):
exit_with_error(f"Unknown theme: {theme}")
output_file.write("CONFIG_LV_USE_THEME_DEFAULT=n\n")
output_file.write("CONFIG_LV_USE_THEME_SIMPLE=n\n")
output_file.write("CONFIG_LV_USE_THEME_MONO=n\n")
font_size_text = get_property_or_none(device_properties, "font.defaultSize")
if font_size_text is None:
font_size_text = get_property_or_default(device_properties, "lvgl.fontSize", "14")

View File

@ -557,22 +557,13 @@
* THEMES
*==================*/
/*Tactility uses its own themes (Modules/lvgl-module), so LVGL's themes are disabled*/
/*A simple, impressive and very complete theme*/
#define LV_USE_THEME_DEFAULT 1
#if LV_USE_THEME_DEFAULT
/*0: Light mode; 1: Dark mode*/
#define LV_THEME_DEFAULT_DARK 1
/*1: Enable grow on press*/
#define LV_THEME_DEFAULT_GROW 1
/*Default transition time in [ms]*/
#define LV_THEME_DEFAULT_TRANSITION_TIME 80
#endif /*LV_USE_THEME_DEFAULT*/
#define LV_USE_THEME_DEFAULT 0
/*A very simple theme that is a good starting point for a custom theme*/
#define LV_USE_THEME_SIMPLE 1
#define LV_USE_THEME_SIMPLE 0
/*A theme designed for monochrome displays*/
#define LV_USE_THEME_MONO 0