diff --git a/Devices/lilygo-t5-epd47-s3/device.properties b/Devices/lilygo-t5-epd47-s3/device.properties index f518283d2..9e19bab83 100644 --- a/Devices/lilygo-t5-epd47-s3/device.properties +++ b/Devices/lilygo-t5-epd47-s3/device.properties @@ -22,5 +22,3 @@ lvgl.colorDepth=8 lvgl.fontSize=24 lvgl.theme=Mono lvgl.statusbarColorsInverted=true - -sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0 diff --git a/Devices/lilygo-tdeck-max/device.properties b/Devices/lilygo-tdeck-max/device.properties index 20462171b..5e2b146a5 100644 --- a/Devices/lilygo-tdeck-max/device.properties +++ b/Devices/lilygo-tdeck-max/device.properties @@ -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 diff --git a/Devices/lilygo-tdeck-pro/device.properties b/Devices/lilygo-tdeck-pro/device.properties index 7ac27e12a..eef070099 100644 --- a/Devices/lilygo-tdeck-pro/device.properties +++ b/Devices/lilygo-tdeck-pro/device.properties @@ -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 diff --git a/Devices/m5stack-papers3/device.properties b/Devices/m5stack-papers3/device.properties index 69b644d3f..57373e6ad 100644 --- a/Devices/m5stack-papers3/device.properties +++ b/Devices/m5stack-papers3/device.properties @@ -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 \ No newline at end of file diff --git a/Modules/lvgl-module/CMakeLists.txt b/Modules/lvgl-module/CMakeLists.txt index 6c1f79af0..de7d34e0e 100644 --- a/Modules/lvgl-module/CMakeLists.txt +++ b/Modules/lvgl-module/CMakeLists.txt @@ -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}" ) diff --git a/Modules/lvgl-module/include/lvgl/theme.h b/Modules/lvgl-module/include/lvgl/theme.h index c5d527049..d22620945 100644 --- a/Modules/lvgl-module/include/lvgl/theme.h +++ b/Modules/lvgl-module/include/lvgl/theme.h @@ -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 diff --git a/Modules/lvgl-module/include/lvgl/widgets/badge.h b/Modules/lvgl-module/include/lvgl/widgets/badge.h new file mode 100644 index 000000000..84d326f97 --- /dev/null +++ b/Modules/lvgl-module/include/lvgl/widgets/badge.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#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 diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index dee5ff830..e300ae9c0 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -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 diff --git a/Modules/lvgl-module/source/theme.cpp b/Modules/lvgl-module/source/theme.cpp index 6918cd13d..80090ffad 100644 --- a/Modules/lvgl-module/source/theme.cpp +++ b/Modules/lvgl-module/source/theme.cpp @@ -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: diff --git a/Modules/lvgl-module/source/themes/lv_theme_material.c b/Modules/lvgl-module/source/themes/lv_theme_material.c index d71fcd4c3..162c7f6c8 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material.c @@ -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); 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 acc2264ed..75ed66609 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material_mono.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material_mono.c @@ -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); diff --git a/Modules/lvgl-module/source/widgets/badge.cpp b/Modules/lvgl-module/source/widgets/badge.cpp new file mode 100644 index 000000000..d91defb03 --- /dev/null +++ b/Modules/lvgl-module/source/widgets/badge.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration + +#include + +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; +} diff --git a/Modules/lvgl-module/themes/material.lss b/Modules/lvgl-module/themes/material.lss index 4df1ff37a..880d3a70e 100644 --- a/Modules/lvgl-module/themes/material.lss +++ b/Modules/lvgl-module/themes/material.lss @@ -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; diff --git a/Modules/lvgl-module/themes/material_mono.lss b/Modules/lvgl-module/themes/material_mono.lss index eca349205..00f640cf0 100644 --- a/Modules/lvgl-module/themes/material_mono.lss +++ b/Modules/lvgl-module/themes/material_mono.lss @@ -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; diff --git a/Tactility/Include/Tactility/app/selectiondialog/SelectionDialog.h b/Tactility/Include/Tactility/app/selectiondialog/SelectionDialog.h index 095220b0c..6e8ddf793 100644 --- a/Tactility/Include/Tactility/app/selectiondialog/SelectionDialog.h +++ b/Tactility/Include/Tactility/app/selectiondialog/SelectionDialog.h @@ -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 diff --git a/Tactility/Source/app/AppGrid.cpp b/Tactility/Source/app/AppGrid.cpp index 33c0717b5..935302b95 100644 --- a/Tactility/Source/app/AppGrid.cpp +++ b/Tactility/Source/app/AppGrid.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -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); diff --git a/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp b/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp index c73238725..f5565f11a 100644 --- a/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp +++ b/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -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 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(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); +std::optional& 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(lv_event_get_user_data(event)); + auto* button = static_cast(lv_event_get_target(event)); + ctx->selectColorIndex = reinterpret_cast(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* 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(static_cast(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 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(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); } } } diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index e04317169..642008025 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -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 diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp index 588406813..564e23280 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp @@ -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 { diff --git a/Tactility/Source/settings/AppearanceSettings.cpp b/Tactility/Source/settings/AppearanceSettings.cpp index e3a9ab2f8..a35d69f9e 100644 --- a/Tactility/Source/settings/AppearanceSettings.cpp +++ b/Tactility/Source/settings/AppearanceSettings.cpp @@ -30,11 +30,25 @@ static std::optional parseColor(const std::map(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& 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; + } 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); diff --git a/device.py b/device.py index 75773ee85..78f163d27 100755 --- a/device.py +++ b/device.py @@ -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") diff --git a/lv_conf.h b/lv_conf.h index a9f933c9d..5a84bc371 100644 --- a/lv_conf.h +++ b/lv_conf.h @@ -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