// SPDX-License-Identifier: Apache-2.0 #define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include constexpr auto* TAG = "lvgl_statusbar"; constexpr auto STATUSBAR_ICON_LIMIT = LVGL_STATUSBAR_ICON_LIMIT; struct StatusbarIcon { char image[LVGL_STATUSBAR_IMAGE_MAX_LENGTH + 1] = {}; bool visible = false; bool claimed = false; // When the icon was added: the side strip drops the newest icons that don't fit uint32_t added_order = 0; }; struct Statusbar; struct StatusbarData { RecursiveMutex mutex {}; StatusbarIcon icons[STATUSBAR_ICON_LIMIT] = {}; Timer* time_update_timer = nullptr; uint8_t time_hours = 0; uint8_t time_minutes = 0; bool time_set = false; bool time_format_24h = true; uint32_t next_added_order = 0; void (*menu_callback)() = nullptr; // The created statusbars, which are updated when the icons or the time change Mutex instances_mutex {}; Statusbar* instances = nullptr; StatusbarData() { recursive_mutex_construct(&mutex); mutex_construct(&instances_mutex); } ~StatusbarData() { mutex_destruct(&instances_mutex); recursive_mutex_destruct(&mutex); } }; static StatusbarData statusbar_data; struct Statusbar { lv_obj_t obj; lv_obj_t* time; lv_obj_t* icons[STATUSBAR_ICON_LIMIT]; lv_obj_t* battery_icon; // The next created statusbar Statusbar* next; // The parent of the icons, which opens the system menu lv_obj_t* icon_row; // Pushes the action container to the bottom in the side layout. Null on circular displays. lv_obj_t* spacer; // Holds the action buttons of apps in the side layout. Null on circular displays. lv_obj_t* action_container; // The horizontal padding of the split layout int32_t split_pad_hor; bool side; }; static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj); static void statusbar_destructor(const lv_obj_class_t* class_p, lv_obj_t* obj); static void statusbar_event(const lv_obj_class_t* class_p, lv_event_t* event); static void update_time(Statusbar* statusbar); static void update_main(Statusbar* statusbar); static void update_icons(Statusbar* statusbar); static void onSideSizeChanged(lv_event_t* event); static TickType_t getNextUpdateTime() { time_t now = ::time(nullptr); tm* tm_struct = localtime(&now); uint32_t seconds_to_wait = 60U - tm_struct->tm_sec; LOG_D(TAG, "Update in %d s", (int)seconds_to_wait); return pdMS_TO_TICKS(seconds_to_wait * 1000U); } static void notify_instances() { mutex_lock(&statusbar_data.instances_mutex); for (auto* statusbar = statusbar_data.instances; statusbar != nullptr; statusbar = statusbar->next) { LOG_D(TAG, "Update event"); if (lvgl_try_lock(500 / portTICK_PERIOD_MS)) { update_main(statusbar); lv_obj_invalidate(&statusbar->obj); lvgl_unlock(); } else { LOG_W(TAG, "Mutex acquisition timeout (%s)", "Statusbar"); } } mutex_unlock(&statusbar_data.instances_mutex); } static void on_update_time(void* /*context*/) { time_t now = ::time(nullptr); tm* tm_struct = localtime(&now); if (recursive_mutex_try_lock(&statusbar_data.mutex, 100 / portTICK_PERIOD_MS)) { bool changed = false; if (tm_struct->tm_year >= (2025 - 1900)) { statusbar_data.time_hours = tm_struct->tm_hour; statusbar_data.time_minutes = tm_struct->tm_min; statusbar_data.time_set = true; changed = true; // Reschedule timer_reset_with_interval(statusbar_data.time_update_timer, getNextUpdateTime()); } else { timer_reset_with_interval(statusbar_data.time_update_timer, pdMS_TO_TICKS(60000U)); } recursive_mutex_unlock(&statusbar_data.mutex); if (changed) { notify_instances(); } } } static lv_obj_class_t statusbar_class = { .base_class = &lv_obj_class, .constructor_cb = &statusbar_constructor, .destructor_cb = &statusbar_destructor, .event_cb = &statusbar_event, .user_data = nullptr, .name = nullptr, .width_def = LV_PCT(100), .height_def = 20, .editable = false, .group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE, .instance_size = sizeof(Statusbar), .theme_inheritable = false }; static void onTimeChanged(struct SystemEvent* /*event*/, void* /*context*/) { recursive_mutex_lock(&statusbar_data.mutex); timer_reset_with_interval(statusbar_data.time_update_timer, 5); recursive_mutex_unlock(&statusbar_data.mutex); } static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) { LV_UNUSED(class_p); LV_TRACE_OBJ_CREATE("begin"); lv_obj_set_scrollable(obj, false); LV_TRACE_OBJ_CREATE("finished"); // Deliberately does NOT register the instance here - that happens at the end of // lvgl_statusbar_create(), once statusbar->icons[] is actually populated. Registering this early // would let a concurrent lvgl_statusbar_icon_add()/_remove()/_set_image()/_set_visibility() call // from another task run update_icon() on a still-null icons[] slot before this // instance's own creation loop below has had a chance to fill it in. recursive_mutex_lock(&statusbar_data.mutex); if (statusbar_data.time_update_timer == nullptr) { statusbar_data.time_update_timer = timer_alloc(TIMER_TYPE_ONCE, 200 / portTICK_PERIOD_MS, on_update_time, nullptr); check(statusbar_data.time_update_timer != nullptr); timer_start(statusbar_data.time_update_timer); system_event_callback_add(KERNEL_EVENT_TIME_CHANGED, onTimeChanged, nullptr); } recursive_mutex_unlock(&statusbar_data.mutex); } static void statusbar_destructor(const lv_obj_class_t* class_p, lv_obj_t* obj) { auto* statusbar = (Statusbar*)obj; mutex_lock(&statusbar_data.instances_mutex); for (auto** link = &statusbar_data.instances; *link != nullptr; link = &(*link)->next) { if (*link == statusbar) { *link = statusbar->next; break; } } mutex_unlock(&statusbar_data.instances_mutex); } static void update_icon(lv_obj_t* image, const StatusbarIcon* icon) { if (icon->image[0] != '\0' && icon->visible && icon->claimed) { lv_image_set_src(image, icon->image); lv_obj_set_hidden(image, false); } else { lv_obj_set_hidden(image, true); } } static void show_menu() { recursive_mutex_lock(&statusbar_data.mutex); auto* callback = statusbar_data.menu_callback; recursive_mutex_unlock(&statusbar_data.mutex); if (callback != nullptr) { callback(); } } static void onIconRowClicked(lv_event_t* /*event*/) { show_menu(); } static bool is_inside(lv_obj_t* obj, lv_obj_t* ancestor) { for (lv_obj_t* current = obj; current != nullptr; current = lv_obj_get_parent(current)) { if (current == ancestor) { return true; } } return false; } // In the side strip on the left, the right key moves the focus to the first widget outside of the strip static void onIconRowKey(lv_event_t* event) { auto* statusbar = static_cast(lv_event_get_user_data(event)); if (!statusbar->side) { return; } if (lv_event_get_key(event) != LV_KEY_RIGHT) { return; } lv_group_t* group = lv_obj_get_group(statusbar->icon_row); if (group == nullptr) { return; } const uint32_t count = lv_group_get_obj_count(group); for (uint32_t i = 0; i < count; i++) { lv_group_focus_next(group); if (!is_inside(lv_group_get_focused(group), &statusbar->obj)) { return; } } } static void onStatusbarGesture(lv_event_t* /*event*/) { lv_indev_t* indev = lv_indev_active(); if (indev != nullptr && lv_indev_get_gesture_dir(indev) == LV_DIR_BOTTOM) { // The release must not click the pressed widget lv_indev_wait_release(indev); show_menu(); } } extern "C" { void lvgl_statusbar_set_menu_callback(void (*callback)()) { recursive_mutex_lock(&statusbar_data.mutex); statusbar_data.menu_callback = callback; recursive_mutex_unlock(&statusbar_data.mutex); } void lvgl_statusbar_set_time_format_24h(bool format_24h) { recursive_mutex_lock(&statusbar_data.mutex); statusbar_data.time_format_24h = format_24h; recursive_mutex_unlock(&statusbar_data.mutex); notify_instances(); } lv_obj_t* lvgl_statusbar_create(lv_obj_t* parent) { statusbar_class.height_def = lvgl_statusbar_get_height(); lv_obj_t* obj = lv_obj_class_create_obj(&statusbar_class, parent); lv_obj_class_init_obj(obj); auto* statusbar = reinterpret_cast(obj); lv_obj_set_width(obj, LV_PCT(100)); lv_obj_set_style_pad_ver(obj, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT); lv_obj_center(obj); auto icon_size = lvgl_get_statusbar_icon_font_height(); const bool is_compact = lvgl_theme_is_compact(); auto icon_padding = !is_compact ? static_cast(icon_size * 0.2f) : 2; lv_display_t* display = lv_obj_get_display(obj); DisplayShape shape; lvgl_display_get_shape(display, &shape); // The parent of the icons, which opens the system menu lv_obj_t* icon_row; if (shape.shape == DISPLAY_SHAPE_CIRCLE) { // Two centered rows, as the top of a circle is narrow: the time, and the icons below it const int32_t diameter = std::min(lv_display_get_horizontal_resolution(display), lv_display_get_vertical_resolution(display)); lv_obj_set_height(obj, LV_SIZE_CONTENT); lv_obj_set_style_pad_top(obj, diameter / 24, LV_STATE_DEFAULT); lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); statusbar->time = lv_label_create(obj); update_time(statusbar); icon_row = lvgl_focus_container_create(obj); lv_obj_set_style_pad_ver(icon_row, static_cast(icon_size / 10), LV_STATE_DEFAULT); lv_obj_set_style_pad_column(icon_row, icon_padding, LV_STATE_DEFAULT); lv_obj_set_flex_flow(icon_row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(icon_row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); } else { 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); lv_obj_set_style_pad_column(obj, icon_padding, LV_STATE_DEFAULT); if (shape.shape == DISPLAY_SHAPE_ROUNDED) { // Moves the content inward from the rounded corners, measured at the top of the icons const int32_t content_top = (lvgl_statusbar_get_height() - static_cast(icon_size)) / 2; lv_obj_set_style_pad_hor(obj, 2 + lvgl_display_get_row_inset(display, content_top), LV_STATE_DEFAULT); } statusbar->time = lv_label_create(obj); lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT); update_time(statusbar); auto* left_spacer = lv_obj_create(obj); lv_obj_set_scrollable(left_spacer, false); lv_obj_set_clickable(left_spacer, false); lv_obj_set_size(left_spacer, 1, 1); lv_obj_set_flex_grow(left_spacer, 1); lv_obj_set_style_bg_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT); lv_obj_set_style_border_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT); statusbar->spacer = left_spacer; icon_row = lvgl_focus_container_create(obj); lv_obj_set_style_pad_column(icon_row, icon_padding, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(icon_row, icon_padding, LV_STATE_DEFAULT); lv_obj_set_style_margin_right(icon_row, 2, LV_STATE_DEFAULT); lv_obj_set_flex_flow(icon_row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(icon_row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); auto* action_container = lv_obj_create(obj); lv_obj_remove_style_all(action_container); lv_obj_set_size(action_container, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_set_flex_flow(action_container, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(action_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_scrollable(action_container, false); lv_obj_set_hidden(action_container, true); statusbar->action_container = action_container; statusbar->split_pad_hor = lv_obj_get_style_pad_left(obj, LV_PART_MAIN); } statusbar->icon_row = icon_row; lv_obj_add_flag(icon_row, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(icon_row, onIconRowClicked, LV_EVENT_SHORT_CLICKED, nullptr); lv_obj_add_event_cb(icon_row, onIconRowKey, LV_EVENT_KEY, statusbar); // Swiping down from anywhere on the statusbar opens the system menu lv_obj_set_gesture_bubble(obj, false); lv_obj_add_event_cb(obj, onStatusbarGesture, LV_EVENT_GESTURE, nullptr); if (statusbar->action_container != nullptr) { lv_obj_add_event_cb(obj, onSideSizeChanged, LV_EVENT_SIZE_CHANGED, statusbar); lv_obj_add_event_cb(statusbar->action_container, onSideSizeChanged, LV_EVENT_SIZE_CHANGED, statusbar); } recursive_mutex_lock(&statusbar_data.mutex); for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { auto* image = lv_image_create(icon_row); lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT); lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT); statusbar->icons[i] = image; update_icon(image, &(statusbar_data.icons[i])); } // Only now is statusbar->icons[] fully populated - see statusbar_constructor()'s comment for // why the instance can't be registered any earlier. Registering before unlocking (rather // than after) closes a narrow window where a concurrent icon change could be missed by // this instance after the icons are populated but before it's registered. mutex_lock(&statusbar_data.instances_mutex); statusbar->next = statusbar_data.instances; statusbar_data.instances = statusbar; mutex_unlock(&statusbar_data.instances_mutex); recursive_mutex_unlock(&statusbar_data.mutex); // Only the icon row is focused, it joins the group by itself if (lv_obj_get_group(obj) != nullptr) { lv_group_remove_obj(obj); } return obj; } } static void update_time(Statusbar* statusbar) { if (statusbar_data.time_set) { bool format24 = statusbar_data.time_format_24h; int hours = format24 ? statusbar_data.time_hours : statusbar_data.time_hours % 12; lv_label_set_text_fmt(statusbar->time, "%d:%02d", hours, statusbar_data.time_minutes); } else { lv_label_set_text(statusbar->time, ""); } } // In the side strip, the time and the action buttons always show. The icons get the height that's left, and the newest ones are hidden when they don't fit. // Call while holding statusbar_data.mutex, after update_icon() set the icons' visibility. static void apply_side_icon_limit(Statusbar* statusbar) { if (!statusbar->side) { return; } lv_obj_t* obj = &statusbar->obj; const int32_t gap = lv_obj_get_style_pad_row(obj, LV_PART_MAIN); // The time, the icons, the spacer and the actions are separated by 3 gaps, and the spacer is at least 1 pixel high const int32_t taken = lv_obj_get_height(statusbar->time) + lv_obj_get_style_margin_bottom(statusbar->time, LV_PART_MAIN) + lv_obj_get_height(statusbar->action_container) + 3 * gap + 1; const int32_t available = lv_obj_get_content_height(obj) - taken; const int32_t icon_size = static_cast(lvgl_get_statusbar_icon_font_height()); const int32_t icon_gap = lv_obj_get_style_pad_row(statusbar->icon_row, LV_PART_MAIN); const int32_t fitting = available >= icon_size ? (available + icon_gap) / (icon_size + icon_gap) : 0; // The shown icons, oldest first int shown[STATUSBAR_ICON_LIMIT]; int shown_count = 0; for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { if (!lv_obj_is_hidden(statusbar->icons[i])) { shown[shown_count++] = i; } } std::sort(shown, shown + shown_count, [](int a, int b) { return statusbar_data.icons[a].added_order < statusbar_data.icons[b].added_order; }); for (int i = static_cast(fitting); i < shown_count; ++i) { lv_obj_set_hidden(statusbar->icons[shown[i]], true); } } static void update_icons(Statusbar* statusbar) { if (recursive_mutex_try_lock(&statusbar_data.mutex, 200 / portTICK_PERIOD_MS)) { for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { update_icon(statusbar->icons[i], &(statusbar_data.icons[i])); } apply_side_icon_limit(statusbar); recursive_mutex_unlock(&statusbar_data.mutex); } } // The room for the icons changes with the strip's height and the actions that apps add static void onSideSizeChanged(lv_event_t* event) { auto* statusbar = static_cast(lv_event_get_user_data(event)); if (statusbar->side) { update_icons(statusbar); } } static void update_main(Statusbar* statusbar) { update_time(statusbar); update_icons(statusbar); } static void statusbar_event(const lv_obj_class_t* class_p, lv_event_t* event) { // Call the ancestor's event handler lv_result_t result = lv_obj_event_base(&statusbar_class, event); if (result != LV_RES_OK) { return; } lv_event_code_t code = lv_event_get_code(event); auto* obj = static_cast(lv_event_get_target(event)); if (code == LV_EVENT_VALUE_CHANGED) { lv_obj_invalidate(obj); } } static void copy_image(char* target, const char* image) { if (image == nullptr) { target[0] = '\0'; return; } if (strlen(image) > LVGL_STATUSBAR_IMAGE_MAX_LENGTH) { LOG_W(TAG, "Image too long: %s", image); } strncpy(target, image, LVGL_STATUSBAR_IMAGE_MAX_LENGTH); target[LVGL_STATUSBAR_IMAGE_MAX_LENGTH] = '\0'; } extern "C" { int8_t lvgl_statusbar_icon_add(const char* image, bool visible) { recursive_mutex_lock(&statusbar_data.mutex); int8_t result = -1; for (int8_t i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { if (!statusbar_data.icons[i].claimed) { statusbar_data.icons[i].claimed = true; statusbar_data.icons[i].visible = visible; copy_image(statusbar_data.icons[i].image, image); statusbar_data.icons[i].added_order = statusbar_data.next_added_order++; result = i; LOG_D(TAG, "id %d: added", (int)i); break; } } recursive_mutex_unlock(&statusbar_data.mutex); if (result < 0) { LOG_W(TAG, "No icon slot left, the icon isn't shown"); } notify_instances(); return result; } void lvgl_statusbar_icon_remove(int8_t id) { LOG_D(TAG, "id %d: remove", (int)id); if (id == -1) { return; } check(id >= 0 && id < STATUSBAR_ICON_LIMIT); recursive_mutex_lock(&statusbar_data.mutex); StatusbarIcon* icon = &statusbar_data.icons[id]; icon->claimed = false; icon->visible = false; icon->image[0] = '\0'; recursive_mutex_unlock(&statusbar_data.mutex); notify_instances(); } void lvgl_statusbar_icon_set_image(int8_t id, const char* image) { LOG_D(TAG, "id %d: set image %s", (int)id, image != nullptr ? image : "(none)"); if (id == -1) { return; } check(id >= 0 && id < STATUSBAR_ICON_LIMIT); recursive_mutex_lock(&statusbar_data.mutex); StatusbarIcon* icon = &statusbar_data.icons[id]; check(icon->claimed); copy_image(icon->image, image); recursive_mutex_unlock(&statusbar_data.mutex); notify_instances(); } void lvgl_statusbar_icon_set_visibility(int8_t id, bool visible) { LOG_D(TAG, "id %d: set visibility %d", (int)id, (int)visible); if (id == -1) { return; } check(id >= 0 && id < STATUSBAR_ICON_LIMIT); recursive_mutex_lock(&statusbar_data.mutex); StatusbarIcon* icon = &statusbar_data.icons[id]; check(icon->claimed); icon->visible = visible; recursive_mutex_unlock(&statusbar_data.mutex); notify_instances(); } } // The colour that is visible behind the object: its own background or the first visible one of its parents static lv_color_t get_visible_bg_color(lv_obj_t* obj) { for (lv_obj_t* current = obj; current != nullptr; current = lv_obj_get_parent(current)) { if (lv_obj_get_style_bg_opa(current, LV_PART_MAIN) >= LV_OPA_50) { return lv_obj_get_style_bg_color(current, LV_PART_MAIN); } } return lv_obj_get_style_bg_color(obj, LV_PART_MAIN); } // The width of an app's action button in the side strip, measured with a temporary button like the ones apps add static int32_t get_action_button_width(lv_obj_t* actionContainer) { auto* button = lvgl_icon_button_create(actionContainer); // It must not take the focus if (lv_obj_get_group(button) != nullptr) { lv_group_remove_obj(button); } 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, LVGL_ICON_SHARED_SETTINGS); lv_obj_update_layout(button); const int32_t width = lv_obj_get_width(button); lv_obj_delete(button); return width; } // The width of the widest time that the label can show, as the font's digits can differ in width static int32_t get_widest_time_width(const lv_font_t* font, int32_t letterSpace) { char widest_digit = '0'; int32_t widest_digit_width = 0; for (char digit = '0'; digit <= '9'; digit++) { const char text[2] = { digit, '\0' }; lv_point_t size; lv_text_get_size(&size, text, font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE); if (size.x > widest_digit_width) { widest_digit_width = size.x; widest_digit = digit; } } const char text[6] = { widest_digit, widest_digit, ':', widest_digit, widest_digit, '\0' }; lv_point_t size; lv_text_get_size(&size, text, font, letterSpace, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE); return size.x; } extern "C" { void lvgl_statusbar_set_side(lv_obj_t* obj, bool side) { auto* statusbar = reinterpret_cast(obj); // Circular displays only have room for the statusbar at the top if (statusbar->action_container == nullptr || statusbar->side == side) { return; } statusbar->side = side; lv_obj_remove_local_style_prop(obj, LV_STYLE_BG_COLOR, LV_PART_MAIN); lv_obj_remove_local_style_prop(obj, LV_STYLE_BG_OPA, LV_PART_MAIN); if (side) { lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT); // A fixed width, so the strip doesn't change when an app adds or removes action buttons lv_obj_set_hidden(statusbar->action_container, false); const int32_t time_width = get_widest_time_width(lvgl_get_text_font(FONT_SIZE_SMALL), lv_obj_get_style_text_letter_space(statusbar->time, LV_PART_MAIN)); const int32_t content_width = std::max(get_action_button_width(statusbar->action_container), time_width); lv_obj_set_size(obj, content_width + 2 * 2, LV_PCT(100)); // Keeps the action buttons away from the screen's bottom edge. The compact layout has no room to spare. const bool compact = lvgl_theme_is_compact(); lv_obj_set_style_pad_top(obj, compact ? 0 : 4, LV_STATE_DEFAULT); lv_obj_set_style_pad_bottom(obj, compact ? 0 : 8, LV_STATE_DEFAULT); // Slightly darker than the colour behind it, so the strip stands out from the app next to it const lv_color_t background = get_visible_bg_color(obj); lv_obj_set_style_bg_color(obj, lv_color_darken(background, LV_OPA_20), LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_STATE_DEFAULT); lv_obj_set_style_margin_left(statusbar->time, 0, LV_STATE_DEFAULT); // The small font fits the time in the narrow strip lv_obj_set_style_text_font(statusbar->time, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT); lv_obj_set_style_margin_bottom(statusbar->time, 10, LV_STATE_DEFAULT); // As wide as the strip, so the column of items is centered on the strip's full width lv_obj_set_width(statusbar->time, LV_PCT(100)); lv_obj_set_style_text_align(statusbar->time, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT); // The time stays on one line, also when it's wider than expected lv_label_set_long_mode(statusbar->time, LV_LABEL_LONG_MODE_CLIP); lv_obj_set_style_margin_right(statusbar->icon_row, 0, LV_STATE_DEFAULT); // The icon that's last in the top bar is closest to the screen's corner there, so it goes on top lv_obj_set_flex_flow(statusbar->icon_row, LV_FLEX_FLOW_COLUMN_REVERSE); // The time, the icons, the spacer and then the actions lv_obj_move_to_index(statusbar->icon_row, 1); lv_obj_set_hidden(statusbar->action_container, false); } else { lv_obj_set_size(obj, LV_PCT(100), lvgl_statusbar_get_height()); lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW); lv_obj_set_style_pad_hor(obj, statusbar->split_pad_hor, LV_STATE_DEFAULT); lv_obj_set_style_pad_ver(obj, 0, LV_STATE_DEFAULT); lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT); lv_obj_remove_local_style_prop(statusbar->time, LV_STYLE_TEXT_FONT, LV_PART_MAIN); lv_obj_remove_local_style_prop(statusbar->time, LV_STYLE_MARGIN_BOTTOM, LV_PART_MAIN); lv_obj_set_width(statusbar->time, LV_SIZE_CONTENT); lv_obj_remove_local_style_prop(statusbar->time, LV_STYLE_TEXT_ALIGN, LV_PART_MAIN); lv_label_set_long_mode(statusbar->time, LV_LABEL_LONG_MODE_WRAP); lv_obj_set_style_margin_right(statusbar->icon_row, 2, LV_STATE_DEFAULT); lv_obj_set_flex_flow(statusbar->icon_row, LV_FLEX_FLOW_ROW); // The time, the spacer and then the icons lv_obj_move_to_index(statusbar->icon_row, 2); lv_obj_set_hidden(statusbar->action_container, true); } // The split layout shows all icons, the side strip only the ones that fit update_icons(statusbar); } lv_obj_t* lvgl_statusbar_get_action_container(lv_obj_t* obj) { return reinterpret_cast(obj)->action_container; } int32_t lvgl_statusbar_get_height() { const auto icon_size = lvgl_get_statusbar_icon_font_height(); const auto vertical_padding = static_cast((static_cast(icon_size) * 0.1f)); return static_cast(icon_size + (2 * vertical_padding) + 4); } }