mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
683 lines
27 KiB
C++
683 lines
27 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#define LV_USE_PRIVATE_API 1 // For actual lv_obj_t declaration
|
|
|
|
#include <lvgl/widgets/statusbar.h>
|
|
|
|
#include <tactility/check.h>
|
|
#include <tactility/concurrent/mutex.h>
|
|
#include <tactility/concurrent/recursive_mutex.h>
|
|
#include <tactility/concurrent/timer.h>
|
|
#include <tactility/log.h>
|
|
#include <tactility/system_event.h>
|
|
#include <tactility/time.h>
|
|
|
|
#include <lvgl/fonts.h>
|
|
#include <lvgl/insets.h>
|
|
#include <lvgl/lvgl.h>
|
|
#include <lvgl/theme.h>
|
|
#include <lvgl/icons/shared.h>
|
|
#include <lvgl/widgets/focus_container.h>
|
|
#include <lvgl/widgets/icon_button.h>
|
|
|
|
#include <TactilityCpp/Memory.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
// Allocated once in external memory when available
|
|
static StatusbarData& statusbar_data() {
|
|
static StatusbarData* instance = tt::newObject<StatusbarData>();
|
|
check(instance != nullptr);
|
|
return *instance;
|
|
}
|
|
|
|
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<Statusbar*>(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<Statusbar*>(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<uint32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<int>(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<Statusbar*>(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_obj_t*>(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<Statusbar*>(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<Statusbar*>(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<uint32_t>((static_cast<float>(icon_size) * 0.1f));
|
|
return static_cast<int32_t>(icon_size + (2 * vertical_padding) + 4);
|
|
}
|
|
|
|
}
|