diff --git a/Modules/lvgl-module/include/lvgl/grid_navigation.h b/Modules/lvgl-module/include/lvgl/grid_navigation.h index 1319c4fab..ee97b2f2e 100644 --- a/Modules/lvgl-module/include/lvgl/grid_navigation.h +++ b/Modules/lvgl-module/include/lvgl/grid_navigation.h @@ -28,6 +28,13 @@ void lvgl_grid_navigation_remove(lv_obj_t* container); */ void lvgl_grid_navigation_step(lv_group_t* group, bool next); +/** + * @brief Hides the selection that keys made, e.g. when the touchscreen is used. The focus stays where it is, + * and the next key that moves the focus shows the selection again. + * @param[in] group the focus group + */ +void lvgl_focus_hide_key_selection(lv_group_t* group); + /** * @param[in] container a container that was passed to lvgl_grid_navigation_add() * @return the focused child, or NULL when no child is focused diff --git a/Modules/lvgl-module/include/lvgl/widgets/page_indicator.h b/Modules/lvgl-module/include/lvgl/widgets/page_indicator.h new file mode 100644 index 000000000..b97609f4f --- /dev/null +++ b/Modules/lvgl-module/include/lvgl/widgets/page_indicator.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Creates a page indicator: a row of dots with one dot per page, where the dot of the current page is filled. + * The theme determines the size and colour of the dots. + * When the dots are wider than the indicator's maximum width (lv_obj_set_style_max_width()), it shows the page number instead, e.g. "2/12". + * @param[in] parent the parent object + * @return the created page indicator + */ +lv_obj_t* lvgl_page_indicator_create(lv_obj_t* parent); + +/** + * @brief Sets the number of pages, which is the number of dots. + * @param[in] obj the page indicator + * @param[in] count the number of pages + */ +void lvgl_page_indicator_set_page_count(lv_obj_t* obj, uint32_t count); + +/** + * @brief Sets the current page, whose dot is filled. + * @param[in] obj the page indicator + * @param[in] page the zero-based index of the current page + */ +void lvgl_page_indicator_set_page(lv_obj_t* obj, uint32_t page); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/lvgl-module/source/devices/pointer.cpp b/Modules/lvgl-module/source/devices/pointer.cpp index 8963b621a..5b1ac8ad4 100644 --- a/Modules/lvgl-module/source/devices/pointer.cpp +++ b/Modules/lvgl-module/source/devices/pointer.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include #include +#include #include #include @@ -140,8 +141,8 @@ static void lvgl_pointer_pool_refresh(struct LvglPointerPool* pool, int32_t nati // the touch controller reports points in a different order (no touch-ID/tracking field exists // anywhere in this stack - see esp_lcd_touch_get_coordinates()/PointerApi.get_touched_points()). // Unmatched raw points (new touches) claim the nearest inactive slot. Slots with no matching -// point this round go inactive (RELEASED). -static void lvgl_pointer_pool_assign(struct LvglPointerPool* pool, int32_t native_x_max) { +// point this round go inactive (RELEASED). Returns true when a new touch started. +static bool lvgl_pointer_pool_assign(struct LvglPointerPool* pool, int32_t native_x_max) { // Touch drivers clamp raw coordinates to the panel's configured native resolution regardless // of calibration, so native_x_max is a valid scale reference for the distance cap even when calibration is disabled. // Falls back to a conservative fixed pixel value if the display/resolution isn't available for some reason. @@ -182,6 +183,7 @@ static void lvgl_pointer_pool_assign(struct LvglPointerPool* pool, int32_t nativ // Second pass: any unclaimed raw point is a new touch - hand it to the first inactive slot // that wasn't just released this round. + bool touch_started = false; for (uint8_t r = 0; r < pool->raw_count; r++) { if (raw_claimed[r]) continue; for (uint8_t s = 0; s < pool->slot_count; s++) { @@ -190,9 +192,11 @@ static void lvgl_pointer_pool_assign(struct LvglPointerPool* pool, int32_t nativ pool->slot_point[s].x = (lv_coord_t)pool->raw_x[r]; pool->slot_point[s].y = (lv_coord_t)pool->raw_y[r]; raw_claimed[r] = true; + touch_started = true; break; } } + return touch_started; } // The actual LVGL indev read callback, shared by every slot in the pool. Only the first slot to @@ -219,7 +223,11 @@ static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0; int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0; lvgl_pointer_pool_refresh(pool, native_x_max, native_y_max); - lvgl_pointer_pool_assign(pool, native_x_max); + // A touch hides the selection that keys made, so it doesn't look like two widgets are selected. + // This runs before LVGL handles the press, so a widget that the touch focuses isn't affected. + if (lvgl_pointer_pool_assign(pool, native_x_max) && lv_group_get_default() != NULL) { + lvgl_focus_hide_key_selection(lv_group_get_default()); + } } pool->round_pos = (uint8_t)((pool->round_pos + 1) % pool->slot_count); diff --git a/Modules/lvgl-module/source/devices/trackball.cpp b/Modules/lvgl-module/source/devices/trackball.cpp index 0d1d6d982..d46b5ab43 100644 --- a/Modules/lvgl-module/source/devices/trackball.cpp +++ b/Modules/lvgl-module/source/devices/trackball.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -25,6 +26,8 @@ struct LvglTrackballCtx { int32_t pending_count; // Keys mode: the LVGL key that is reported as pressed, or 0 uint32_t pressed_key; + // Pointer mode: whether the button was pressed during the previous read + bool pointer_pressed; }; static inline int32_t clamp(int32_t value, int32_t min_value, int32_t max_value) { @@ -153,6 +156,12 @@ static void lvgl_trackball_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { data->point.y = static_cast(ctx->cursor_y); data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + // Like a touch, a click hides the selection that keys made + if (pressed && !ctx->pointer_pressed && lv_group_get_default() != nullptr) { + lvgl_focus_hide_key_selection(lv_group_get_default()); + } + ctx->pointer_pressed = pressed; + if (pressed) { lv_display_trigger_activity(display); } diff --git a/Modules/lvgl-module/source/grid_navigation.cpp b/Modules/lvgl-module/source/grid_navigation.cpp index 069792a6c..e16ec1e1a 100644 --- a/Modules/lvgl-module/source/grid_navigation.cpp +++ b/Modules/lvgl-module/source/grid_navigation.cpp @@ -110,6 +110,22 @@ void lvgl_grid_navigation_step(lv_group_t* group, bool next) { } } +void lvgl_focus_hide_key_selection(lv_group_t* group) { + lv_obj_t* focused = lv_group_get_focused(group); + if (focused == nullptr) { + return; + } + lv_group_set_editing(group, false); + constexpr auto key_states = static_cast(LV_STATE_FOCUS_KEY | LV_STATE_EDITED); + lv_obj_remove_state(focused, key_states); + if (lvgl_grid_navigation_is_container(focused)) { + lv_obj_t* child = lvgl_grid_navigation_get_focused(focused); + if (child != nullptr) { + lv_obj_remove_state(child, key_states); + } + } +} + lv_obj_t* lvgl_grid_navigation_get_focused(lv_obj_t* container) { // Grid navigation marks the focused child with the focused state const uint32_t child_count = lv_obj_get_child_count(container); diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index acf26fb70..13352ec16 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -69,6 +70,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_step), DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_get_focused), DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_is_container), + DEFINE_MODULE_SYMBOL(lvgl_focus_hide_key_selection), // lvgl_insets DEFINE_MODULE_SYMBOL(lvgl_display_get_shape), DEFINE_MODULE_SYMBOL(lvgl_display_get_insets), @@ -81,6 +83,9 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lvgl_theme_is_mono), // lvgl_badge DEFINE_MODULE_SYMBOL(lvgl_badge_create), + DEFINE_MODULE_SYMBOL(lvgl_page_indicator_create), + DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page_count), + DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page), // lvgl_card DEFINE_MODULE_SYMBOL(lvgl_card_create), // lvgl_chip diff --git a/Modules/lvgl-module/source/themes/lv_theme_material.c b/Modules/lvgl-module/source/themes/lv_theme_material.c index 43c02b1dd..7b4ec65bf 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material.c @@ -16,6 +16,8 @@ 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; +extern const lv_obj_class_t lvgl_page_indicator_class; +extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define NEUTRAL_4 lv_color_hex(0x0E0E0E) #define NEUTRAL_6 lv_color_hex(0x131313) @@ -137,6 +139,9 @@ extern const lv_obj_class_t lvgl_badge_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 PAGE_INDICATOR_COLOR COLOR_ON_SURFACE_VARIANT +#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) +#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 3 : LV_DPX_CALC(theme->disp_dpi, 6)) #define BADGE_COLOR COLOR_SECONDARY #define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) #define SCROLLBAR_COLOR COLOR_OUTLINE @@ -334,6 +339,12 @@ typedef struct { lv_style_t screen; lv_style_t container; lv_style_t card; + lv_style_t page_indicator; +#if LV_USE_LABEL + lv_style_t page_indicator_label; +#endif + lv_style_t page_indicator_dot; + lv_style_t page_indicator_dot_current; lv_style_t badge; lv_style_t chip; lv_style_t chip_selected; @@ -758,6 +769,29 @@ 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.page_indicator); + lv_style_set_bg_opa(&theme->styles.page_indicator, LV_OPA_TRANSP); + lv_style_set_border_width(&theme->styles.page_indicator, 0); + lv_style_set_pad_all(&theme->styles.page_indicator, 0); + lv_style_set_pad_gap(&theme->styles.page_indicator, PAGE_INDICATOR_GAP); + +#if LV_USE_LABEL + style_init_reset(&theme->styles.page_indicator_label); + lv_style_set_text_color(&theme->styles.page_indicator_label, PAGE_INDICATOR_COLOR); +#endif + + style_init_reset(&theme->styles.page_indicator_dot); + lv_style_set_width(&theme->styles.page_indicator_dot, PAGE_INDICATOR_DOT_SIZE); + lv_style_set_height(&theme->styles.page_indicator_dot, PAGE_INDICATOR_DOT_SIZE); + lv_style_set_radius(&theme->styles.page_indicator_dot, LV_RADIUS_CIRCLE); + lv_style_set_bg_color(&theme->styles.page_indicator_dot, PAGE_INDICATOR_COLOR); + lv_style_set_bg_opa(&theme->styles.page_indicator_dot, LV_OPA_TRANSP); + lv_style_set_border_color(&theme->styles.page_indicator_dot, PAGE_INDICATOR_COLOR); + lv_style_set_border_width(&theme->styles.page_indicator_dot, 1); + + style_init_reset(&theme->styles.page_indicator_dot_current); + lv_style_set_bg_opa(&theme->styles.page_indicator_dot_current, LV_OPA_COVER); + style_init_reset(&theme->styles.badge); lv_style_set_width(&theme->styles.badge, BADGE_SIZE); lv_style_set_height(&theme->styles.badge, BADGE_SIZE); @@ -1802,6 +1836,11 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj) return; } #endif + /* page_indicator > label */ + if(lv_obj_check_type(parent, &lvgl_page_indicator_class)) { + lv_obj_add_style(obj, &theme->styles.page_indicator_label, 0); + return; + } return; } #endif @@ -1833,6 +1872,17 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj) return; } #endif + if(lv_obj_check_type(obj, &lvgl_page_indicator_class)) { + /* page_indicator */ + lv_obj_add_style(obj, &theme->styles.page_indicator, 0); + return; + } + if(lv_obj_check_type(obj, &lvgl_page_indicator_dot_class)) { + /* page_indicator_dot */ + lv_obj_add_style(obj, &theme->styles.page_indicator_dot, 0); + lv_obj_add_style(obj, &theme->styles.page_indicator_dot_current, LV_STATE_CHECKED); + return; + } if(lv_obj_check_type(obj, &lvgl_badge_class)) { /* badge */ lv_obj_add_style(obj, &theme->styles.badge, 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 cc35164b0..fcd53c2ce 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material_mono.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material_mono.c @@ -16,6 +16,8 @@ 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; +extern const lv_obj_class_t lvgl_page_indicator_class; +extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define BLACK lv_color_black() #define WHITE lv_color_white() @@ -87,6 +89,9 @@ extern const lv_obj_class_t lvgl_badge_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 PAGE_INDICATOR_COLOR COLOR_ON_SURFACE +#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) +#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 3 : LV_DPX_CALC(theme->disp_dpi, 6)) #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 @@ -216,6 +221,12 @@ typedef struct { lv_style_t screen; lv_style_t container; lv_style_t card; + lv_style_t page_indicator; +#if LV_USE_LABEL + lv_style_t page_indicator_label; +#endif + lv_style_t page_indicator_dot; + lv_style_t page_indicator_dot_current; lv_style_t badge; lv_style_t chip; #if LV_USE_BUTTON || LV_USE_MSGBOX @@ -548,6 +559,29 @@ 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.page_indicator); + lv_style_set_bg_opa(&theme->styles.page_indicator, LV_OPA_TRANSP); + lv_style_set_border_width(&theme->styles.page_indicator, 0); + lv_style_set_pad_all(&theme->styles.page_indicator, 0); + lv_style_set_pad_gap(&theme->styles.page_indicator, PAGE_INDICATOR_GAP); + +#if LV_USE_LABEL + style_init_reset(&theme->styles.page_indicator_label); + lv_style_set_text_color(&theme->styles.page_indicator_label, PAGE_INDICATOR_COLOR); +#endif + + style_init_reset(&theme->styles.page_indicator_dot); + lv_style_set_width(&theme->styles.page_indicator_dot, PAGE_INDICATOR_DOT_SIZE); + lv_style_set_height(&theme->styles.page_indicator_dot, PAGE_INDICATOR_DOT_SIZE); + lv_style_set_radius(&theme->styles.page_indicator_dot, LV_RADIUS_CIRCLE); + lv_style_set_bg_color(&theme->styles.page_indicator_dot, PAGE_INDICATOR_COLOR); + lv_style_set_bg_opa(&theme->styles.page_indicator_dot, LV_OPA_TRANSP); + lv_style_set_border_color(&theme->styles.page_indicator_dot, PAGE_INDICATOR_COLOR); + lv_style_set_border_width(&theme->styles.page_indicator_dot, 1); + + style_init_reset(&theme->styles.page_indicator_dot_current); + lv_style_set_bg_opa(&theme->styles.page_indicator_dot_current, LV_OPA_COVER); + style_init_reset(&theme->styles.badge); lv_style_set_width(&theme->styles.badge, BADGE_SIZE); lv_style_set_height(&theme->styles.badge, BADGE_SIZE); @@ -1401,6 +1435,11 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj) return; } #endif + /* page_indicator > label */ + if(lv_obj_check_type(parent, &lvgl_page_indicator_class)) { + lv_obj_add_style(obj, &theme->styles.page_indicator_label, 0); + return; + } return; } #endif @@ -1428,6 +1467,17 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj) return; } #endif + if(lv_obj_check_type(obj, &lvgl_page_indicator_class)) { + /* page_indicator */ + lv_obj_add_style(obj, &theme->styles.page_indicator, 0); + return; + } + if(lv_obj_check_type(obj, &lvgl_page_indicator_dot_class)) { + /* page_indicator_dot */ + lv_obj_add_style(obj, &theme->styles.page_indicator_dot, 0); + lv_obj_add_style(obj, &theme->styles.page_indicator_dot_current, LV_STATE_CHECKED); + return; + } if(lv_obj_check_type(obj, &lvgl_badge_class)) { /* badge */ lv_obj_add_style(obj, &theme->styles.badge, 0); diff --git a/Modules/lvgl-module/source/widgets/page_indicator.cpp b/Modules/lvgl-module/source/widgets/page_indicator.cpp new file mode 100644 index 000000000..b40afe447 --- /dev/null +++ b/Modules/lvgl-module/source/widgets/page_indicator.cpp @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration + +#include + +#include + +static void refresh(lv_obj_t* obj); + +static void on_style_changed(lv_event_t* event) { + refresh(lv_event_get_current_target_obj(event)); +} + +static void page_indicator_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_set_flex_flow(obj, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + // Shows the page numbers instead of the dots when they don't fit in the indicator's maximum width + lv_obj_t* label = lv_label_create(obj); + lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); + // A change of the maximum width can switch between dots and numbers + lv_obj_add_event_cb(obj, on_style_changed, LV_EVENT_STYLE_CHANGED, nullptr); +} + +static void page_indicator_dot_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); +} + +// Not static: the theme styles page indicators and their dots by these classes +extern "C" const lv_obj_class_t lvgl_page_indicator_class = { + .base_class = &lv_obj_class, + .constructor_cb = page_indicator_constructor, + .destructor_cb = nullptr, + .event_cb = nullptr, + .user_data = nullptr, + .name = "page_indicator", + .width_def = LV_SIZE_CONTENT, + .height_def = LV_SIZE_CONTENT, + .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 +}; + +extern "C" const lv_obj_class_t lvgl_page_indicator_dot_class = { + .base_class = &lv_obj_class, + .constructor_cb = page_indicator_dot_constructor, + .destructor_cb = nullptr, + .event_cb = nullptr, + .user_data = nullptr, + .name = "page_indicator_dot", + .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 +}; + +// The first child is the label, the dots follow +static lv_obj_t* get_label(lv_obj_t* obj) { + return lv_obj_get_child(obj, 0); +} + +static uint32_t get_dot_count(lv_obj_t* obj) { + return lv_obj_get_child_count(obj) - 1; +} + +static lv_obj_t* get_dot(lv_obj_t* obj, uint32_t index) { + return lv_obj_get_child(obj, static_cast(index + 1)); +} + +static void refresh(lv_obj_t* obj) { + const uint32_t count = get_dot_count(obj); + if (count == 0) { + return; + } + const int32_t dot_width = lv_obj_get_style_width(get_dot(obj, 0), LV_PART_MAIN); + const int32_t gap = lv_obj_get_style_pad_column(obj, LV_PART_MAIN); + const int32_t dots_width = static_cast(count) * dot_width + static_cast(count - 1) * gap; + const bool show_numbers = dots_width > lv_obj_get_style_max_width(obj, LV_PART_MAIN); + + lv_obj_t* label = get_label(obj); + lv_obj_set_flag(label, LV_OBJ_FLAG_HIDDEN, !show_numbers); + uint32_t page = 0; + for (uint32_t i = 0; i < count; i++) { + lv_obj_t* dot = get_dot(obj, i); + lv_obj_set_flag(dot, LV_OBJ_FLAG_HIDDEN, show_numbers); + if (lv_obj_has_state(dot, LV_STATE_CHECKED)) { + page = i; + } + } + if (show_numbers) { + char text[24]; + std::snprintf(text, sizeof(text), "%lu/%lu", static_cast(page + 1), static_cast(count)); + lv_label_set_text(label, text); + } +} + +extern "C" { + +lv_obj_t* lvgl_page_indicator_create(lv_obj_t* parent) { + lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_page_indicator_class, parent); + lv_obj_class_init_obj(obj); + return obj; +} + +void lvgl_page_indicator_set_page_count(lv_obj_t* obj, uint32_t count) { + uint32_t current = get_dot_count(obj); + while (current > count) { + lv_obj_delete(lv_obj_get_child(obj, -1)); + current--; + } + while (current < count) { + lv_obj_t* dot = lv_obj_class_create_obj(&lvgl_page_indicator_dot_class, obj); + lv_obj_class_init_obj(dot); + current++; + } + refresh(obj); +} + +void lvgl_page_indicator_set_page(lv_obj_t* obj, uint32_t page) { + const uint32_t count = get_dot_count(obj); + for (uint32_t i = 0; i < count; i++) { + lv_obj_set_state(get_dot(obj, i), LV_STATE_CHECKED, i == page); + } + refresh(obj); +} + +} diff --git a/Modules/lvgl-module/themes/material.lss b/Modules/lvgl-module/themes/material.lss index 2b06d98f6..6da95531b 100644 --- a/Modules/lvgl-module/themes/material.lss +++ b/Modules/lvgl-module/themes/material.lss @@ -29,6 +29,8 @@ @widget card: lvgl_card_class; @widget chip: lvgl_chip_class; @widget badge: lvgl_badge_class; +@widget page_indicator: lvgl_page_indicator_class; +@widget page_indicator_dot: lvgl_page_indicator_dot_class; // ============================================================ // 1. Base tokens @@ -199,6 +201,11 @@ @chip-pad-hor: if($isCompact, 3, @space-4); @chip-pad-ver: if($isCompact, 2, 6dp); +// Page indicator (lvgl_page_indicator_create): a dot per page, the current page's dot is filled +@page-indicator-color: @color-on-surface-variant; +@page-indicator-dot-size: if($isCompact, 4, 6dp); +@page-indicator-gap: if($isCompact, 3, 6dp); + // Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite @badge-color: @color-secondary; @badge-size: if($isCompact, 4, 6dp); @@ -565,6 +572,31 @@ card { padGap: @card-gap; } +page_indicator { + bgOpa: transp; + borderWidth: 0; + padAll: 0; + padGap: @page-indicator-gap; +} + +page_indicator_label { + textColor: @page-indicator-color; +} + +page_indicator_dot { + width: @page-indicator-dot-size; + height: @page-indicator-dot-size; + radius: circle; + bgColor: @page-indicator-color; + bgOpa: transp; + borderColor: @page-indicator-color; + borderWidth: 1; +} + +page_indicator_dot_current { + bgOpa: cover; +} + badge { width: @badge-size; height: @badge-size; @@ -1374,6 +1406,19 @@ apply(checkbox) { disabled: state.disabled; } +apply(page_indicator) { + page_indicator: default; +} + +apply(page_indicator > label) { + page_indicator_label: default; +} + +apply(page_indicator_dot) { + page_indicator_dot: default; + page_indicator_dot_current: state.checked; +} + apply(badge) { badge: default; } diff --git a/Modules/lvgl-module/themes/material_mono.lss b/Modules/lvgl-module/themes/material_mono.lss index 6ae20b1f8..8869e5972 100644 --- a/Modules/lvgl-module/themes/material_mono.lss +++ b/Modules/lvgl-module/themes/material_mono.lss @@ -30,6 +30,8 @@ @widget card: lvgl_card_class; @widget chip: lvgl_chip_class; @widget badge: lvgl_badge_class; +@widget page_indicator: lvgl_page_indicator_class; +@widget page_indicator_dot: lvgl_page_indicator_dot_class; // ============================================================ // 1. Base tokens @@ -142,6 +144,11 @@ @chip-pad-hor: if($isCompact, 3, @space-4); @chip-pad-ver: if($isCompact, 2, @space-2); +// Page indicator (lvgl_page_indicator_create): a dot per page, the current page's dot is filled +@page-indicator-color: @color-on-surface; +@page-indicator-dot-size: if($isCompact, 4, 6dp); +@page-indicator-gap: if($isCompact, 3, 6dp); + // 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); @@ -392,6 +399,31 @@ card { padGap: @card-gap; } +page_indicator { + bgOpa: transp; + borderWidth: 0; + padAll: 0; + padGap: @page-indicator-gap; +} + +page_indicator_label { + textColor: @page-indicator-color; +} + +page_indicator_dot { + width: @page-indicator-dot-size; + height: @page-indicator-dot-size; + radius: circle; + bgColor: @page-indicator-color; + bgOpa: transp; + borderColor: @page-indicator-color; + borderWidth: 1; +} + +page_indicator_dot_current { + bgOpa: cover; +} + badge { width: @badge-size; height: @badge-size; @@ -1037,6 +1069,19 @@ apply(checkbox) { disabled: part.indicator, state.disabled; } +apply(page_indicator) { + page_indicator: default; +} + +apply(page_indicator > label) { + page_indicator_label: default; +} + +apply(page_indicator_dot) { + page_indicator_dot: default; + page_indicator_dot_current: state.checked; +} + apply(badge) { badge: default; } diff --git a/Tactility/Private/Tactility/app/AppGrid.h b/Tactility/Private/Tactility/app/AppGrid.h index f1fb80d0a..36da50549 100644 --- a/Tactility/Private/Tactility/app/AppGrid.h +++ b/Tactility/Private/Tactility/app/AppGrid.h @@ -39,18 +39,36 @@ public: explicit AppGrid(const Callbacks& callbacks) : callbacks(callbacks) {} - /** Shows the tile icons in the theme's primary color. Call before creating the widgets. */ - void setPrimaryColorIcons(bool enabled) { primaryColorIcons = enabled; } + enum class IconColor { + /** The theme's icon colour */ + Default, + /** The theme's primary colour */ + Primary, + /** The theme's secondary colour */ + Secondary + }; + + /** Sets the colour of the tile icons. Call before creating the widgets. */ + void setIconColor(IconColor color) { iconColor = color; } + + /** Swiping left or right over the grid goes to the next or previous page. Call before creating the widgets. */ + void setSwipeNavigation(bool enabled) { swipeNavigation = enabled; } - /** Adds the paging buttons to the toolbar, creates the grid below it and populates it. */ - void createWidgets(lv_obj_t* parent, lv_obj_t* toolbar); /** - * Creates the grid and populates it. The page number and the page buttons go in pageBar, which is hidden when there's only one page. The page buttons wrap around. - * @param[in] parent the container for the grid - * @param[in] pageBar an empty container + * Creates the grid and populates it, with a bar below it: buttons on the left (see addBarButton()), + * a page indicator in the center and page buttons on the right. The page indicator and buttons are + * hidden when there's only one page, and the page buttons wrap around. + * @param[in] parent the container for the grid and the bar */ - void createWidgetsWithPageBar(lv_obj_t* parent, lv_obj_t* pageBar); + void createWidgetsWithBottomBar(lv_obj_t* parent); + + /** + * Adds an icon button to the left side of the bottom bar. Call right after createWidgetsWithBottomBar(). + * @param[in] icon a shared icon (see lvgl/icons/shared.h) + * @return the button + */ + lv_obj_t* addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData); /** * Rebuilds the grid asynchronously, so it is safe to call from a tile's own event. @@ -64,22 +82,29 @@ private: std::vector items; uint32_t page = 0; uint32_t pageCount = 1; - bool primaryColorIcons = false; + IconColor iconColor = IconColor::Default; + bool swipeNavigation = false; bool keepSelectionOnRepopulate = true; lv_obj_t* grid = nullptr; lv_obj_t* prevButton = nullptr; lv_obj_t* nextButton = nullptr; - lv_obj_t* pageBar = nullptr; - lv_obj_t* pageLabel = nullptr; + lv_obj_t* pageIndicator = nullptr; + lv_obj_t* pageButtons = nullptr; + lv_obj_t* barButtons = nullptr; void createGrid(lv_obj_t* parent); + void goToPreviousPage(); + void goToNextPage(); void populate(); static void onDeferredRepopulate(void* userData); + static void onDeferredNextPage(void* userData); + static void onDeferredPreviousPage(void* userData); static void onGridDeleted(lv_event_t* e); static void onGridSizeChanged(lv_event_t* e); static void onPrevPressed(lv_event_t* e); static void onNextPressed(lv_event_t* e); + static void onGridGesture(lv_event_t* e); static void onTileClicked(lv_event_t* e); static void onTileLongPressed(lv_event_t* e); static void onTileKey(lv_event_t* e); diff --git a/Tactility/Source/app/AppGrid.cpp b/Tactility/Source/app/AppGrid.cpp index 3813a9ab4..2d7a0fb77 100644 --- a/Tactility/Source/app/AppGrid.cpp +++ b/Tactility/Source/app/AppGrid.cpp @@ -3,9 +3,10 @@ #include #include #include +#include #include #include -#include +#include #include #include @@ -54,8 +55,67 @@ const AppGridItem* getTileItem(lv_event_t* e) { return static_cast(lv_obj_get_user_data(lv_event_get_target_obj(e))); } + +lv_obj_t* createBarSection(lv_obj_t* parent, lv_flex_align_t align) { + auto* section = lv_obj_create(parent); + lv_obj_set_size(section, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(section, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(section, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(section, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(section, align, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(section, LV_OBJ_FLAG_SCROLLABLE); + return section; +} + +// The width that a section's visible children need, including the gaps between them +int32_t getContentWidth(lv_obj_t* section) { + int32_t width = 0; + uint32_t visible = 0; + const uint32_t count = lv_obj_get_child_count(section); + for (uint32_t i = 0; i < count; i++) { + lv_obj_t* child = lv_obj_get_child(section, static_cast(i)); + if (!lv_obj_has_flag(child, LV_OBJ_FLAG_HIDDEN)) { + width += lv_obj_get_width(child); + visible++; + } + } + return visible > 1 ? width + static_cast(visible - 1) * lv_obj_get_style_pad_column(section, LV_PART_MAIN) : width; +} + +// The page indicator gets the space between the side sections, which grow equally to keep it centered +void updatePageIndicatorWidth(void* data) { + auto* bottom_bar = static_cast(data); + auto* left_section = lv_obj_get_child(bottom_bar, 0); + auto* indicator_section = lv_obj_get_child(bottom_bar, 1); + auto* page_buttons = lv_obj_get_child(bottom_bar, 2); + auto* indicator = lv_obj_get_child(indicator_section, 0); + if (indicator == nullptr) { + return; + } + const int32_t side_width = std::max(getContentWidth(left_section), getContentWidth(page_buttons)); + const int32_t gaps = 2 * lv_obj_get_style_pad_column(bottom_bar, LV_PART_MAIN); + const int32_t available = lv_obj_get_content_width(bottom_bar) - 2 * side_width - gaps; + lv_obj_set_style_max_width(indicator, std::max(0, available), LV_STATE_DEFAULT); +} + +void onBottomBarSizeChanged(lv_event_t* event) { + lv_async_call(updatePageIndicatorWidth, lv_event_get_target_obj(event)); +} + +void onBottomBarDeleted(lv_event_t* event) { + lv_async_call_cancel(updatePageIndicatorWidth, lv_event_get_target_obj(event)); +} + } // namespace +void AppGrid::onDeferredNextPage(void* userData) { + static_cast(userData)->goToNextPage(); +} + +void AppGrid::onDeferredPreviousPage(void* userData) { + static_cast(userData)->goToPreviousPage(); +} + void AppGrid::onDeferredRepopulate(void* userData) { static_cast(userData)->populate(); } @@ -63,6 +123,8 @@ void AppGrid::onDeferredRepopulate(void* userData) { void AppGrid::onGridDeleted(lv_event_t* e) { // Cancels a still-pending repopulate scheduled right before the grid itself was deleted (e.g. the app closing) lv_async_call_cancel(onDeferredRepopulate, lv_event_get_user_data(e)); + lv_async_call_cancel(onDeferredNextPage, lv_event_get_user_data(e)); + lv_async_call_cancel(onDeferredPreviousPage, lv_event_get_user_data(e)); } // Deferred: page size depends on the grid size, which is final only after the layout pass that triggered this. @@ -70,28 +132,42 @@ void AppGrid::onGridSizeChanged(lv_event_t* e) { lv_async_call(onDeferredRepopulate, lv_event_get_user_data(e)); } +// Changing the page doesn't keep the selected app selected: that would return to the selected app's page +void AppGrid::goToPreviousPage() { + page = page > 0 ? page - 1 : pageCount - 1; + keepSelectionOnRepopulate = false; + populate(); +} + +void AppGrid::goToNextPage() { + page = page + 1 < pageCount ? page + 1 : 0; + keepSelectionOnRepopulate = false; + populate(); +} + void AppGrid::onPrevPressed(lv_event_t* e) { - auto* self = static_cast(lv_event_get_user_data(e)); - if (self->page > 0) { - self->page--; - } else if (self->pageBar != nullptr) { - self->page = self->pageCount - 1; - } else { - return; - } - self->populate(); + static_cast(lv_event_get_user_data(e))->goToPreviousPage(); } void AppGrid::onNextPressed(lv_event_t* e) { + static_cast(lv_event_get_user_data(e))->goToNextPage(); +} + +// Swipes that start on a tile reach the grid too, as tiles pass their gestures on to the grid +void AppGrid::onGridGesture(lv_event_t* e) { auto* self = static_cast(lv_event_get_user_data(e)); - if (self->page + 1 < self->pageCount) { - self->page++; - } else if (self->pageBar != nullptr) { - self->page = 0; - } else { + lv_indev_t* indev = lv_indev_active(); + if (indev == nullptr || self->pageCount <= 1) { return; } - self->populate(); + const lv_dir_t direction = lv_indev_get_gesture_dir(indev); + if (direction != LV_DIR_LEFT && direction != LV_DIR_RIGHT) { + return; + } + // Keeps the tile where the swipe started from being clicked when the finger is lifted + lv_indev_wait_release(indev); + // Deferred: changing the page deletes the tiles, possibly including the one this event came from + lv_async_call(direction == LV_DIR_LEFT ? onDeferredNextPage : onDeferredPreviousPage, self); } void AppGrid::onTileClicked(lv_event_t* e) { @@ -119,8 +195,9 @@ void AppGrid::populate() { // replacement tiles doesn't restore the focus, so the app id is remembered here and // re-focused on its new tile once rebuilt. std::string focusedAppId; + const bool keepSelection = keepSelectionOnRepopulate; lv_group_t* group = lv_group_get_default(); - if (group != nullptr && keepSelectionOnRepopulate) { + if (group != nullptr && keepSelection) { // Grid navigation focuses the grid in the group, and a tile within the grid lv_obj_t* focused = lv_group_get_focused(group) == grid ? lvgl_grid_navigation_get_focused(grid) : nullptr; if (focused != nullptr && lv_obj_get_parent(focused) == grid) { @@ -152,15 +229,10 @@ void AppGrid::populate() { page = std::min(page, page_count - 1); pageCount = page_count; - if (pageBar != nullptr) { - lv_obj_set_flag(pageBar, LV_OBJ_FLAG_HIDDEN, page_count <= 1); - lv_label_set_text_fmt(pageLabel, "%lu/%lu", static_cast(page + 1), static_cast(page_count)); - } else { - lv_obj_set_state(prevButton, LV_STATE_DISABLED, page == 0); - lv_obj_set_state(nextButton, LV_STATE_DISABLED, page + 1 >= page_count); - lv_obj_set_flag(prevButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1); - lv_obj_set_flag(nextButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1); - } + lv_obj_set_flag(pageIndicator, LV_OBJ_FLAG_HIDDEN, page_count <= 1); + lv_obj_set_flag(pageButtons, LV_OBJ_FLAG_HIDDEN, page_count <= 1); + lvgl_page_indicator_set_page_count(pageIndicator, page_count); + lvgl_page_indicator_set_page(pageIndicator, page); lv_obj_t* focusedTile = nullptr; const uint32_t first = page * layout.pageSize; @@ -182,8 +254,10 @@ void AppGrid::populate() { lv_obj_set_style_text_align(icon, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT); lv_obj_set_size(icon, layout.iconSize, layout.iconSize); lv_label_set_text(icon, item.icon); - if (primaryColorIcons) { + if (iconColor == IconColor::Primary) { lv_obj_set_style_text_color(icon, lv_theme_get_color_primary(icon), LV_STATE_DEFAULT); + } else if (iconColor == IconColor::Secondary) { + lv_obj_set_style_text_color(icon, lv_theme_get_color_secondary(icon), LV_STATE_DEFAULT); } lv_obj_t* label = lv_label_create(tile); @@ -216,6 +290,10 @@ void AppGrid::populate() { if (focusedTile != nullptr) { lv_group_focus_obj(grid); lv_gridnav_set_focused(grid, focusedTile, LV_ANIM_OFF); + } else if (!keepSelection && group != nullptr && lv_group_get_focused(group) == grid) { + // Grid navigation selects the first new tile when the grid is focused, but a page change shows no selection. + // The next key that moves the focus selects a tile again. + lvgl_focus_hide_key_selection(group); } } @@ -232,42 +310,38 @@ void AppGrid::createGrid(lv_obj_t* parent) { // The arrow keys move between the tiles in rows and columns lvgl_grid_navigation_add(grid); lv_obj_add_event_cb(grid, onGridDeleted, LV_EVENT_DELETE, this); -} - -void AppGrid::createWidgets(lv_obj_t* parent, lv_obj_t* toolbar) { - prevButton = lvgl_toolbar_add_text_button_action(toolbar, "<", onPrevPressed, this); - nextButton = lvgl_toolbar_add_text_button_action(toolbar, ">", onNextPressed, this); - - createGrid(parent); - - // Resolves the grid's flex_grow height, which the page size is derived from. - lv_obj_update_layout(parent); - lv_obj_add_event_cb(grid, onGridSizeChanged, LV_EVENT_SIZE_CHANGED, this); - - populate(); - - const bool next_visible = !lv_obj_has_flag(nextButton, LV_OBJ_FLAG_HIDDEN); - if (next_visible && !lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) { - lv_group_focus_obj(nextButton); - lv_obj_add_state(nextButton, LV_STATE_FOCUS_KEY); + if (swipeNavigation) { + // LVGL sends a gesture to the first object, starting at the touched one, that doesn't pass it on to its parent + lv_obj_remove_flag(grid, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_add_event_cb(grid, onGridGesture, LV_EVENT_GESTURE, this); } } -void AppGrid::createWidgetsWithPageBar(lv_obj_t* parent, lv_obj_t* pageBar) { - createGrid(parent); +void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) { + // The side sections grow equally, so the page indicator stays centered + auto* bottom_bar = createBarSection(parent, LV_FLEX_ALIGN_START); + lv_obj_set_width(bottom_bar, LV_PCT(100)); + lvgl_obj_add_edge_padding(bottom_bar); + barButtons = createBarSection(bottom_bar, LV_FLEX_ALIGN_START); + lv_obj_set_flex_grow(barButtons, 1); + auto* indicator_section = createBarSection(bottom_bar, LV_FLEX_ALIGN_CENTER); + pageButtons = createBarSection(bottom_bar, LV_FLEX_ALIGN_END); + lv_obj_set_flex_grow(pageButtons, 1); + lv_obj_add_event_cb(bottom_bar, onBottomBarSizeChanged, LV_EVENT_SIZE_CHANGED, nullptr); + lv_obj_add_event_cb(bottom_bar, onBottomBarDeleted, LV_EVENT_DELETE, nullptr); - this->pageBar = pageBar; - lv_obj_set_flex_flow(pageBar, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(pageBar, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - pageLabel = lv_label_create(pageBar); - prevButton = lvgl_icon_button_create(pageBar); + createGrid(parent); + lv_obj_move_foreground(bottom_bar); + + pageIndicator = lvgl_page_indicator_create(indicator_section); + prevButton = lvgl_icon_button_create(pageButtons); lv_label_set_text(lv_label_create(prevButton), "<"); lv_obj_add_event_cb(prevButton, onPrevPressed, LV_EVENT_SHORT_CLICKED, this); - nextButton = lvgl_icon_button_create(pageBar); + nextButton = lvgl_icon_button_create(pageButtons); lv_label_set_text(lv_label_create(nextButton), ">"); lv_obj_add_event_cb(nextButton, onNextPressed, LV_EVENT_SHORT_CLICKED, this); // The arrow keys move between the page buttons - lvgl_grid_navigation_add(pageBar); + lvgl_grid_navigation_add(pageButtons); // Resolves the grid's flex_grow height, which the page size is derived from. lv_obj_update_layout(parent); @@ -281,4 +355,13 @@ void AppGrid::createWidgetsWithPageBar(lv_obj_t* parent, lv_obj_t* pageBar) { } } +lv_obj_t* AppGrid::addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData) { + auto* button = lvgl_icon_button_create(barButtons); + auto* label = lv_label_create(button); + lv_obj_set_style_text_font(label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT); + lv_label_set_text(label, icon); + lv_obj_add_event_cb(button, onClicked, LV_EVENT_SHORT_CLICKED, userData); + return button; +} + } diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index ee19d490b..636f3c442 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -11,11 +11,8 @@ #include #include -#include #include -#include #include -#include #include @@ -49,7 +46,6 @@ constexpr IconEntry ICONS[] = { {"tactility.i2cscanner", LVGL_ICON_SHARED_CABLE}, {"tactility.notes", LVGL_ICON_SHARED_EDIT_NOTE}, {"tactility.screenshot", LVGL_ICON_SHARED_IMAGE}, - {"tactility.settings", LVGL_ICON_SHARED_SETTINGS}, {"tactility.systeminfo", LVGL_ICON_SHARED_DEVICES}, {"tactility.terminal", LVGL_ICON_SHARED_TERMINAL}, {"tactility.webserversettings", LVGL_ICON_SHARED_CLOUD}, @@ -58,7 +54,6 @@ constexpr IconEntry ICONS[] = { // Hidden apps that are still shown in the launcher constexpr const char* SHOWN_HIDDEN_APP_IDS[] = { "tactility.files", - "tactility.settings", }; const char* appIcon(const ::AppManifest* manifest) { @@ -187,46 +182,21 @@ bool supportsPowerOff() { return supported; } -void addShortcut(lv_obj_t* parent, const char* icon, const char* appId) { - auto* button = lvgl_icon_button_create(parent); - auto* label = lv_label_create(button); - lv_obj_set_style_text_font(label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT); - lv_label_set_text(label, icon); - lv_obj_add_event_cb(button, onShortcutPressed, LV_EVENT_SHORT_CLICKED, const_cast(appId)); -} - -lv_obj_t* createBarSection(lv_obj_t* parent, lv_flex_align_t align) { - auto* section = lv_obj_create(parent); - lv_obj_set_size(section, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(section, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(section, 0, LV_STATE_DEFAULT); - lv_obj_set_flex_flow(section, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(section, align, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(section, LV_OBJ_FLAG_SCROLLABLE); - return section; -} - void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); - // Power off on the left, the page number and page buttons on the right - auto* bottom_bar = createBarSection(parent, LV_FLEX_ALIGN_START); - lv_obj_set_width(bottom_bar, LV_PCT(100)); - lvgl_obj_add_edge_padding(bottom_bar); - auto* left_section = createBarSection(bottom_bar, LV_FLEX_ALIGN_START); - lv_obj_set_flex_grow(left_section, 1); - auto* page_bar = createBarSection(bottom_bar, LV_FLEX_ALIGN_END); - // The monochrome theme's primary color is black, also on a black background - ctx->grid.setPrimaryColorIcons(!lvgl_theme_is_mono()); - ctx->grid.createWidgetsWithPageBar(parent, page_bar); - lv_obj_move_foreground(bottom_bar); - + if (!lvgl_theme_is_mono()) { + ctx->grid.setIconColor(AppGrid::IconColor::Primary); + } + ctx->grid.setSwipeNavigation(true); + ctx->grid.createWidgetsWithBottomBar(parent); + ctx->grid.addBarButton(LVGL_ICON_SHARED_SETTINGS, onShortcutPressed, const_cast("tactility.settings")); if (isAppRegistered("tactility.poweroff") && supportsPowerOff()) { - addShortcut(left_section, LVGL_ICON_SHARED_POWER_SETTINGS_NEW, "tactility.poweroff"); + ctx->grid.addBarButton(LVGL_ICON_SHARED_POWER_SETTINGS_NEW, onShortcutPressed, const_cast("tactility.poweroff")); } } diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp index d8f5e91f0..4e8883c7e 100644 --- a/Tactility/Source/app/settings/Settings.cpp +++ b/Tactility/Source/app/settings/Settings.cpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include @@ -109,9 +109,12 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); - auto* toolbar = lvgl_toolbar_create(parent, "Settings"); - lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); - ctx->grid.createWidgets(parent, toolbar); + if (!lvgl_theme_is_mono()) { + ctx->grid.setIconColor(AppGrid::IconColor::Secondary); + } + ctx->grid.setSwipeNavigation(true); + ctx->grid.createWidgetsWithBottomBar(parent); + ctx->grid.addBarButton(LVGL_ICON_SHARED_CLOSE, onBackPressed, ctx); } int32_t appMain(int argc, char* argv[]) {