diff --git a/README.md b/README.md index 3ca75aec..d838c92d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ Creating a touch-capable UI is [easy](https://docs.lvgl.io/8.3/get-started/quick ```c static void app_show(TT_UNUSED App app, lv_obj_t* parent) { + // Default toolbar with app name and close button + lv_obj_t* toolbar = tt_toolbar_create_for_app(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + // Label widget lv_obj_t* label = lv_label_create(parent); lv_label_set_text(label, "Hello, world!"); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); diff --git a/app-esp/src/hello_world/hello_world.c b/app-esp/src/hello_world/hello_world.c index 3d80faa0..aacc900a 100644 --- a/app-esp/src/hello_world/hello_world.c +++ b/app-esp/src/hello_world/hello_world.c @@ -1,7 +1,11 @@ #include "hello_world.h" #include "lvgl.h" +#include "ui/toolbar.h" static void app_show(TT_UNUSED App app, lv_obj_t* parent) { + lv_obj_t* toolbar = tt_toolbar_create_for_app(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_t* label = lv_label_create(parent); lv_label_set_text(label, "Hello, world!"); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); diff --git a/app-sim/src/hello_world/hello_world.c b/app-sim/src/hello_world/hello_world.c index bfdb03a9..70c28e61 100644 --- a/app-sim/src/hello_world/hello_world.c +++ b/app-sim/src/hello_world/hello_world.c @@ -1,8 +1,11 @@ #include "hello_world.h" -#include "services/gui/gui.h" #include "services/loader/loader.h" +#include "ui/toolbar.h" static void app_show(TT_UNUSED App app, lv_obj_t* parent) { + lv_obj_t* toolbar = tt_toolbar_create_for_app(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_t* label = lv_label_create(parent); lv_label_set_text(label, "Hello, world!"); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); diff --git a/app-sim/src/lv_conf.h b/app-sim/src/lv_conf.h index 8ab747de..a39a46f0 100644 --- a/app-sim/src/lv_conf.h +++ b/app-sim/src/lv_conf.h @@ -243,14 +243,14 @@ *-----------*/ /*1: Show CPU usage and FPS count*/ -#define LV_USE_PERF_MONITOR 1 +#define LV_USE_PERF_MONITOR 0 #if LV_USE_PERF_MONITOR #define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT #endif /*1: Show the used memory and the memory fragmentation * Requires LV_MEM_CUSTOM = 0*/ -#define LV_USE_MEM_MONITOR 1 +#define LV_USE_MEM_MONITOR 0 #if LV_USE_MEM_MONITOR #define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT #endif diff --git a/docs/ideas.md b/docs/ideas.md index f40c5de1..c8e87a08 100644 --- a/docs/ideas.md +++ b/docs/ideas.md @@ -12,6 +12,10 @@ - Support for displays with different DPI. Consider the layer-based system like on Android. - Display orientation support for Display app - If present, use LED to show boot status +- 2 wire speaker support +- tt_app_start() and similar functions as proxies for Loader app start/stop/etc. +- tt_app_set_result() for apps that need to return data to other apps (e.g. file selection) +- Make a statusbar service that apps can register icons to. Gui can observe its status changes? # App Improvement Ideas - Sort desktop apps by name. diff --git a/docs/pics/hello-world.png b/docs/pics/hello-world.png index 21f6ab00..f4651fb3 100644 Binary files a/docs/pics/hello-world.png and b/docs/pics/hello-world.png differ diff --git a/tactility-core/src/string_utils.c b/tactility-core/src/string_utils.c new file mode 100644 index 00000000..554452af --- /dev/null +++ b/tactility-core/src/string_utils.c @@ -0,0 +1,26 @@ +#include "string_utils.h" +#include + +int tt_string_find_last_index(const char* text, size_t from_index, char find) { + for (size_t i = from_index; i >= 0; i--) { + if (text[i] == find) { + return (int)i; + } + } + return -1; +} + +bool tt_string_get_path_parent(const char* path, char* output) { + int index = tt_string_find_last_index(path, strlen(path) - 1, '/'); + if (index == -1) { + return false; + } else if (index == 0) { + output[0] = '/'; + output[1] = 0x00; + return true; + } else { + memcpy(output, path, index); + output[index] = 0x00; + return true; + } +} diff --git a/tactility-core/src/string_utils.h b/tactility-core/src/string_utils.h new file mode 100644 index 00000000..b3d32fd5 --- /dev/null +++ b/tactility-core/src/string_utils.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Find the last occurrence of a character. + * @param[in] text the text to search in + * @param[in] from_index the index to search from (searching from right to left) + * @param[in] find the character to search for + * @return the index of the found character, or -1 if none found + */ +int tt_string_find_last_index(const char* text, size_t from_index, char find); + +/** + * Given a filesystem path as input, try and get the parent path. + * @param[in] path input path + * @param[out] output an output buffer that is allocated to at least the size of "current" + * @return true when successful + */ +bool tt_string_get_path_parent(const char* path, char* output); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.c b/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.c index c80fec31..fe1e57c4 100644 --- a/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.c +++ b/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.c @@ -6,6 +6,7 @@ #include "services/wifi/wifi_credentials.h" #include "ui/spacer.h" #include "ui/style.h" +#include "ui/toolbar.h" #include "wifi_connect.h" #include "wifi_connect_bundle.h" #include "wifi_connect_state.h" @@ -69,26 +70,29 @@ void wifi_connect_view_create(App app, void* wifi, lv_obj_t* parent) { WifiConnectView* view = &wifi_connect->view; lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - tt_lv_obj_set_style_auto_padding(parent); + tt_toolbar_create_for_app(parent, app); - view->root = parent; + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_t* ssid_label = lv_label_create(parent); + lv_obj_t* ssid_label = lv_label_create(wrapper); lv_label_set_text(ssid_label, "Network:"); - view->ssid_textarea = lv_textarea_create(parent); + view->ssid_textarea = lv_textarea_create(wrapper); lv_textarea_set_one_line(view->ssid_textarea, true); - tt_lv_spacer_create(parent, 1, 8); + tt_lv_spacer_create(wrapper, 1, 8); - lv_obj_t* password_label = lv_label_create(parent); + lv_obj_t* password_label = lv_label_create(wrapper); lv_label_set_text(password_label, "Password:"); - view->password_textarea = lv_textarea_create(parent); + view->password_textarea = lv_textarea_create(wrapper); lv_textarea_set_one_line(view->password_textarea, true); lv_textarea_set_password_mode(view->password_textarea, true); - tt_lv_spacer_create(parent, 1, 8); + tt_lv_spacer_create(wrapper, 1, 8); - wifi_connect_view_create_bottom_buttons(wifi, parent); + wifi_connect_view_create_bottom_buttons(wifi, wrapper); gui_keyboard_add_textarea(view->ssid_textarea); gui_keyboard_add_textarea(view->password_textarea); diff --git a/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.h b/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.h index 432e9d5a..76dea634 100644 --- a/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.h +++ b/tactility-esp/src/apps/system/wifi_connect/wifi_connect_view.h @@ -9,7 +9,6 @@ extern "C" { #endif typedef struct { - lv_obj_t* root; lv_obj_t* ssid_textarea; lv_obj_t* password_textarea; lv_obj_t* connect_button; diff --git a/tactility-esp/src/apps/system/wifi_manage/wifi_manage.c b/tactility-esp/src/apps/system/wifi_manage/wifi_manage.c index 7b3d037a..643406cd 100644 --- a/tactility-esp/src/apps/system/wifi_manage/wifi_manage.c +++ b/tactility-esp/src/apps/system/wifi_manage/wifi_manage.c @@ -125,7 +125,7 @@ static void app_show(App app, lv_obj_t* parent) { wifi_manage_lock(wifi); wifi->view_enabled = true; strcpy((char*)wifi->state.connect_ssid, "Connected"); // TODO update with proper SSID - wifi_manage_view_create(&wifi->view, &wifi->bindings, parent); + wifi_manage_view_create(app, &wifi->view, &wifi->bindings, parent); wifi_manage_view_update(&wifi->view, &wifi->bindings, &wifi->state); wifi_manage_unlock(wifi); diff --git a/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.c b/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.c index 32d3eabb..1d5270e4 100644 --- a/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.c +++ b/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.c @@ -3,6 +3,7 @@ #include "log.h" #include "services/wifi/wifi.h" #include "ui/style.h" +#include "ui/toolbar.h" #include "wifi_manage_state.h" #define TAG "wifi_main_view" @@ -147,14 +148,19 @@ static void update_connected_ap(WifiManageView* view, WifiManageState* state, Wi // region Main -void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent) { +void wifi_manage_view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent) { view->root = parent; lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - tt_lv_obj_set_style_auto_padding(parent); + tt_toolbar_create_for_app(parent, app); + + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); // Top row: enable/disable - lv_obj_t* switch_container = lv_obj_create(parent); + lv_obj_t* switch_container = lv_obj_create(wrapper); lv_obj_set_width(switch_container, LV_PCT(100)); lv_obj_set_height(switch_container, LV_SIZE_CONTENT); tt_lv_obj_set_style_no_padding(switch_container); @@ -168,7 +174,7 @@ void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_add_event_cb(view->enable_switch, on_enable_switch_changed, LV_EVENT_ALL, bindings); lv_obj_set_align(view->enable_switch, LV_ALIGN_RIGHT_MID); - view->connected_ap_container = lv_obj_create(parent); + view->connected_ap_container = lv_obj_create(wrapper); lv_obj_set_size(view->connected_ap_container, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_min_height(view->connected_ap_container, SPINNER_HEIGHT, 0); tt_lv_obj_set_style_no_padding(view->connected_ap_container); @@ -185,7 +191,7 @@ void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, // Networks - lv_obj_t* networks_header = lv_obj_create(parent); + lv_obj_t* networks_header = lv_obj_create(wrapper); lv_obj_set_size(networks_header, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_min_height(networks_header, SPINNER_HEIGHT, 0); tt_lv_obj_set_style_no_padding(networks_header); @@ -201,7 +207,7 @@ void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_set_style_pad_bottom(view->scanning_spinner, 4, 0); lv_obj_align_to(view->scanning_spinner, view->networks_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0); - view->networks_list = lv_obj_create(parent); + view->networks_list = lv_obj_create(wrapper); lv_obj_set_flex_flow(view->networks_list, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(view->networks_list, LV_PCT(100)); lv_obj_set_height(view->networks_list, LV_SIZE_CONTENT); diff --git a/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.h b/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.h index 76b9cd88..2d371ab3 100644 --- a/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.h +++ b/tactility-esp/src/apps/system/wifi_manage/wifi_manage_view.h @@ -1,5 +1,6 @@ #pragma once +#include "app.h" #include "lvgl.h" #include "wifi_manage_bindings.h" #include "wifi_manage_state.h" @@ -18,7 +19,7 @@ typedef struct { lv_obj_t* connected_ap_label; } WifiManageView; -void wifi_manage_view_create(WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent); +void wifi_manage_view_create(App app, WifiManageView* view, WifiManageBindings* bindings, lv_obj_t* parent); void wifi_manage_view_update(WifiManageView* view, WifiManageBindings* bindings, WifiManageState* state); #ifdef __cplusplus diff --git a/tactility/CMakeLists.txt b/tactility/CMakeLists.txt index 8f0d906f..cebe4538 100644 --- a/tactility/CMakeLists.txt +++ b/tactility/CMakeLists.txt @@ -24,6 +24,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) PUBLIC idf::driver PUBLIC idf::spiffs PUBLIC idf::nvs_flash + PUBLIC idf::newlib # for scandir() and related ) else() add_definitions(-D_Nullable=) diff --git a/tactility/src/app.c b/tactility/src/app.c index 7b675351..25a3858b 100644 --- a/tactility/src/app.c +++ b/tactility/src/app.c @@ -4,6 +4,10 @@ static AppFlags tt_app_get_flags_default(AppType type); +static const AppFlags DEFAULT_FLAGS = { + .show_statusbar = true +}; + // region Alloc/free App tt_app_alloc(const AppManifest* manifest, Bundle* _Nullable parameters) { @@ -41,19 +45,7 @@ static void tt_app_unlock(AppData* data) { } static AppFlags tt_app_get_flags_default(AppType type) { - static const AppFlags DEFAULT_DESKTOP_FLAGS = { - .show_toolbar = false, - .show_statusbar = true - }; - - static const AppFlags DEFAULT_APP_FLAGS = { - .show_toolbar = true, - .show_statusbar = true - }; - - return type == AppTypeDesktop - ? DEFAULT_DESKTOP_FLAGS - : DEFAULT_APP_FLAGS; + return DEFAULT_FLAGS; } // endregion Internal diff --git a/tactility/src/app.h b/tactility/src/app.h index d93d04a8..c5e0ac66 100644 --- a/tactility/src/app.h +++ b/tactility/src/app.h @@ -18,7 +18,6 @@ typedef enum { typedef union { struct { bool show_statusbar : 1; - bool show_toolbar : 1; }; unsigned char flags; } AppFlags; diff --git a/tactility/src/apps/desktop/desktop.c b/tactility/src/apps/desktop/desktop.c index 879b55c4..7f4699af 100644 --- a/tactility/src/apps/desktop/desktop.c +++ b/tactility/src/apps/desktop/desktop.c @@ -14,7 +14,8 @@ static void on_app_pressed(lv_event_t* e) { static void create_app_widget(const AppManifest* manifest, void* parent) { tt_check(parent); lv_obj_t* list = (lv_obj_t*)parent; - lv_obj_t* btn = lv_list_add_btn(list, LV_SYMBOL_FILE, manifest->name); + const char* icon = manifest->icon ?: LV_SYMBOL_FILE; + lv_obj_t* btn = lv_list_add_btn(list, icon, manifest->name); lv_obj_add_event_cb(btn, &on_app_pressed, LV_EVENT_CLICKED, (void*)manifest); } diff --git a/tactility/src/apps/settings/display/display.c b/tactility/src/apps/settings/display/display.c index f0127648..f0981e6c 100644 --- a/tactility/src/apps/settings/display/display.c +++ b/tactility/src/apps/settings/display/display.c @@ -3,7 +3,7 @@ #include "preferences.h" #include "tactility.h" #include "ui/spacer.h" -#include "ui/style.h" +#include "ui/toolbar.h" static bool backlight_duty_set = false; static uint8_t backlight_duty = 255; @@ -25,19 +25,21 @@ static void slider_event_cb(lv_event_t* e) { static void app_show(TT_UNUSED App app, lv_obj_t* parent) { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - tt_lv_obj_set_style_auto_padding(parent); - lv_obj_t* label = lv_label_create(parent); + tt_toolbar_create_for_app(parent, app); + + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + + lv_obj_t* label = lv_label_create(wrapper); lv_label_set_text(label, "Brightness"); - tt_lv_spacer_create(parent, 1, 2); + tt_lv_spacer_create(wrapper, 1, 2); - lv_obj_t* slider_container = lv_obj_create(parent); - lv_obj_set_size(slider_container, LV_PCT(100), LV_SIZE_CONTENT); - - lv_obj_t* slider = lv_slider_create(slider_container); - lv_obj_set_width(slider, LV_PCT(90)); - lv_obj_center(slider); + lv_obj_t* slider = lv_slider_create(wrapper); + lv_obj_set_width(slider, LV_PCT(100)); lv_slider_set_range(slider, 0, 255); lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL); diff --git a/tactility/src/apps/settings/settings.c b/tactility/src/apps/settings/settings.c index 429954f9..1ec29d16 100644 --- a/tactility/src/apps/settings/settings.c +++ b/tactility/src/apps/settings/settings.c @@ -2,6 +2,7 @@ #include "check.h" #include "lvgl.h" #include "services/loader/loader.h" +#include "ui/toolbar.h" static void on_app_pressed(lv_event_t* e) { lv_event_code_t code = lv_event_get_code(e); @@ -19,9 +20,13 @@ static void create_app_widget(const AppManifest* manifest, void* parent) { } static void on_show(TT_UNUSED App app, lv_obj_t* parent) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + tt_toolbar_create_for_app(parent, app); + lv_obj_t* list = lv_list_create(parent); - lv_obj_set_size(list, LV_PCT(100), LV_PCT(100)); - lv_obj_center(list); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); tt_app_manifest_registry_for_each_of_type(AppTypeSettings, list, create_app_widget); } @@ -29,7 +34,7 @@ static void on_show(TT_UNUSED App app, lv_obj_t* parent) { const AppManifest settings_app = { .id = "settings", .name = "Settings", - .icon = NULL, + .icon = LV_SYMBOL_SETTINGS, .type = AppTypeSystem, .on_start = NULL, .on_stop = NULL, diff --git a/tactility/src/apps/system/files/file_utils.c b/tactility/src/apps/system/files/file_utils.c new file mode 100644 index 00000000..a92eb161 --- /dev/null +++ b/tactility/src/apps/system/files/file_utils.c @@ -0,0 +1,67 @@ +#include "file_utils.h" +#include "tactility_core.h" + +#define TAG "file_utils" + +#define SCANDIR_LIMIT 128 + +int tt_dirent_filter_dot_entries(const struct dirent* entry) { + return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; +} + +int tt_dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right) { + bool left_is_dir = (*left)->d_type == TT_DT_DIR; + bool right_is_dir = (*right)->d_type == TT_DT_DIR; + if (left_is_dir == right_is_dir) { + return strcmp((*left)->d_name, (*right)->d_name); + } else { + return (left_is_dir < right_is_dir) ? 1 : -1; + } +} + +int tt_dirent_sort_alpha(const struct dirent** left, const struct dirent** right) { + return strcmp((*left)->d_name, (*right)->d_name); +} + +int tt_scandir( + const char* path, + struct dirent*** output, + ScandirFilter _Nullable filter, + ScandirSort _Nullable sort +) { + DIR* dir = opendir(path); + if (dir == NULL) { + return -1; + } + + *output = malloc(sizeof(void*) * SCANDIR_LIMIT); + struct dirent** dirent_array = *output; + int dirent_buffer_index = 0; + + struct dirent* current_entry; + while ((current_entry = readdir(dir)) != NULL) { + TT_LOG_D(TAG, "debug: %s %d", current_entry->d_name, current_entry->d_type); + if (filter(current_entry) == 0) { + dirent_array[dirent_buffer_index] = malloc(sizeof(struct dirent)); + memcpy(dirent_array[dirent_buffer_index], current_entry, sizeof(struct dirent)); + + dirent_buffer_index++; + if (dirent_buffer_index >= SCANDIR_LIMIT) { + TT_LOG_E(TAG, "directory has more than %d files", SCANDIR_LIMIT); + break; + } + } + } + + if (dirent_buffer_index == 0) { + free(*output); + *output = NULL; + } else { + if (sort) { + qsort(dirent_array, dirent_buffer_index, sizeof(struct dirent*), (__compar_fn_t)sort); + } + } + + closedir(dir); + return dirent_buffer_index; +}; diff --git a/tactility/src/apps/system/files/file_utils.h b/tactility/src/apps/system/files/file_utils.h new file mode 100644 index 00000000..f724a719 --- /dev/null +++ b/tactility/src/apps/system/files/file_utils.h @@ -0,0 +1,68 @@ +#pragma once + +#include + +/** File types for `dirent`'s `d_type`. */ +enum { + TT_DT_UNKNOWN = 0, +#define TT_DT_UNKNOWN TT_DT_UNKNOWN + TT_DT_FIFO = 1, +#define TT_DT_FIFO TT_DT_FIFO + TT_DT_CHR = 2, +#define TT_DT_CHR TT_DT_CHR + TT_DT_DIR = 4, +#define TT_DT_DIR TT_DT_DIR + TT_DT_BLK = 6, +#define TT_DT_BLK TT_DT_BLK + TT_DT_REG = 8, +#define TT_DT_REG TT_DT_REG + TT_DT_LNK = 10, +#define TT_DT_LNK TT_DT_LNK + TT_DT_SOCK = 12, +#define TT_DT_SOCK TT_DT_SOCK + TT_DT_WHT = 14 +#define TT_DT_WHT TT_DT_WHT +}; + +typedef int (*ScandirFilter)(const struct dirent*); + +typedef int (*ScandirSort)(const struct dirent**, const struct dirent**); + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Alphabetic sorting function for tt_scandir() + * @param left left-hand side part for comparison + * @param right right-hand side part for comparison + * @return 0, -1 or 1 + */ +int tt_dirent_sort_alpha(const struct dirent** left, const struct dirent** right); + +int tt_dirent_sort_alpha_and_type(const struct dirent** left, const struct dirent** right); + +int tt_dirent_filter_dot_entries(const struct dirent* entry); + +/** + * A scandir()-like implementation that works on ESP32. + * It does not return "." and ".." items but otherwise functions the same. + * It returns an allocated output array with allocated dirent instances. + * The caller is responsible for free-ing the memory of these. + * + * @param[in] path path the scan for files and directories + * @param[out] output a pointer to an array of dirent* + * @param[in] filter an optional filter to filter out specific items + * @param[in] sort an optional sorting function + * @return the amount of items that were stored in "output" or -1 when an error occurred + */ +int tt_scandir( + const char* path, + struct dirent*** output, + ScandirFilter _Nullable filter, + ScandirSort _Nullable sort +); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/tactility/src/apps/system/files/files.c b/tactility/src/apps/system/files/files.c new file mode 100644 index 00000000..f15f1a52 --- /dev/null +++ b/tactility/src/apps/system/files/files.c @@ -0,0 +1,152 @@ +#include "files_data.h" + +#include "app.h" +#include "check.h" +#include "file_utils.h" +#include "lvgl.h" +#include "services/loader/loader.h" +#include "ui/toolbar.h" +#include + +#define TAG "files_app" + +bool tt_string_ends_with(const char* base, const char* postfix) { + size_t postfix_len = strlen(postfix); + size_t base_len = strlen(base); + if (base_len < postfix_len) { + return false; + } + + for (int i = (int)postfix_len - 1; i >= 0; i--) { + if (tolower(base[base_len - postfix_len + i]) != postfix[i]) { + return false; + } + } + + return true; +} + +static bool is_image_file(const char* filename) { + return tt_string_ends_with(filename, ".jpg") || + tt_string_ends_with(filename, ".png") || + tt_string_ends_with(filename, ".jpeg") || + tt_string_ends_with(filename, ".svg") || + tt_string_ends_with(filename, ".bmp"); +} + +// region Views + +static void update_views(FilesData* data); + +static void on_navigate_up_pressed(TT_UNUSED lv_event_t* event) { + FilesData* files_data = (FilesData*)event->user_data; + if (strcmp(files_data->current_path, "/") != 0) { + files_data_set_entries_navigate_up(files_data); + } + update_views(files_data); +} + +static void on_exit_app_pressed(TT_UNUSED lv_event_t* event) { + loader_stop_app(); +} + +static void on_file_pressed(lv_event_t* e) { + lv_event_code_t code = lv_event_get_code(e); + if (code == LV_EVENT_CLICKED) { + lv_obj_t* button = e->current_target; + FilesData* files_data = lv_obj_get_user_data(button); + + struct dirent* dir_entry = e->user_data; + TT_LOG_I(TAG, "clicked %s %d", dir_entry->d_name, dir_entry->d_type); + + switch (dir_entry->d_type) { + case TT_DT_DIR: + files_data_set_entries_for_path(files_data, dir_entry->d_name); + update_views(files_data); + break; + case TT_DT_LNK: + TT_LOG_W(TAG, "opening links is not supported"); + break; + case TT_DT_REG: + TT_LOG_W(TAG, "opening files is not supported"); + break; + default: + TT_LOG_W(TAG, "file type %d is not supported", dir_entry->d_type); + break; + } + } +} + +static void create_file_widget(FilesData* files_data, lv_obj_t* parent, struct dirent* dir_entry) { + tt_check(parent); + lv_obj_t* list = (lv_obj_t*)parent; + const char* symbol; + switch (dir_entry->d_type) { + case TT_DT_DIR: + symbol = LV_SYMBOL_DIRECTORY; + break; + case TT_DT_REG: + symbol = is_image_file(dir_entry->d_name) ? LV_SYMBOL_IMAGE : LV_SYMBOL_FILE; + break; + case TT_DT_LNK: + symbol = LV_SYMBOL_LOOP; + break; + default: + symbol = LV_SYMBOL_SETTINGS; + break; + } + lv_obj_t* button = lv_list_add_btn(list, symbol, dir_entry->d_name); + lv_obj_set_user_data(button, files_data); + lv_obj_add_event_cb(button, &on_file_pressed, LV_EVENT_CLICKED, (void*)dir_entry); +} + +static void update_views(FilesData* data) { + lv_obj_clean(data->list); + for (int i = 0; i < data->dir_entries_count; ++i) { + create_file_widget(data, data->list, data->dir_entries[i]); + } +} + +// endregion Views + +// region Lifecycle + +static void on_show(App app, lv_obj_t* parent) { + FilesData* data = tt_app_get_data(app); + + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + lv_obj_t* toolbar = tt_toolbar_create(parent, "Files"); + tt_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, &on_exit_app_pressed, NULL); + tt_toolbar_add_action(toolbar, LV_SYMBOL_UP, "Navigate up", &on_navigate_up_pressed, data); + + data->list = lv_list_create(parent); + lv_obj_set_width(data->list, LV_PCT(100)); + lv_obj_set_flex_grow(data->list, 1); + + update_views(data); +} + +static void on_start(App app) { + FilesData* data = files_data_alloc(); + files_data_set_entries_root(data); + tt_app_set_data(app, data); +} + +static void on_stop(App app) { + FilesData* data = tt_app_get_data(app); + files_data_free(data); +} + +// endregion Lifecycle + +const AppManifest files_app = { + .id = "files", + .name = "Files", + .icon = NULL, + .type = AppTypeSystem, + .on_start = &on_start, + .on_stop = &on_stop, + .on_show = &on_show, + .on_hide = NULL +}; diff --git a/tactility/src/apps/system/files/files_data.c b/tactility/src/apps/system/files/files_data.c new file mode 100644 index 00000000..6a1f336f --- /dev/null +++ b/tactility/src/apps/system/files/files_data.c @@ -0,0 +1,118 @@ +#include "files_data.h" +#include "file_utils.h" +#include "tactility_core.h" +#include + +#define TAG "files_app" + +FilesData* files_data_alloc() { + FilesData* data = malloc(sizeof(FilesData)); + *data = (FilesData) { + .current_path = {'/', 0x00}, + .dir_entries = NULL, + .dir_entries_count = 0 + }; + return data; +} + +void files_data_free(FilesData* data) { + files_data_free_entries(data); + free(data); +} + +void files_data_free_entries(FilesData* data) { + for (int i = 0; i < data->dir_entries_count; ++i) { + free(data->dir_entries[i]); + } + free(data->dir_entries); + data->dir_entries = NULL; + data->dir_entries_count = 0; +} + +void files_data_set_entries(FilesData* data, struct dirent** entries, int count) { + if (data->dir_entries != NULL) { + files_data_free_entries(data); + } + + data->dir_entries = entries; + data->dir_entries_count = count; +} + + +void files_data_set_entries_navigate_up(FilesData* data) { + TT_LOG_I(TAG, "navigating upwards"); + char new_absolute_path[MAX_PATH_LENGTH]; + if (tt_string_get_path_parent(data->current_path, new_absolute_path)) { + if (strcmp(new_absolute_path, "/") == 0) { + files_data_set_entries_root(data); + } else { + strcpy(data->current_path, new_absolute_path); + data->dir_entries_count = tt_scandir(new_absolute_path, &(data->dir_entries), &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type); + TT_LOG_I(TAG, "%s has %u entries", new_absolute_path, data->dir_entries_count); + } + } +} + +void files_data_set_entries_for_path(FilesData* data, const char* path) { + size_t current_path_length = strlen(data->current_path); + size_t added_path_length = strlen(path); + size_t total_path_length = current_path_length + added_path_length + 1; // two paths with `/` + + if (total_path_length >= MAX_PATH_LENGTH) { + TT_LOG_E(TAG, "Path limit reached (%d chars)", MAX_PATH_LENGTH); + return; + } + + char new_absolute_path[MAX_PATH_LENGTH]; + memcpy(new_absolute_path, data->current_path, current_path_length); + // Postfix with "/" when the current path isn't "/" + if (current_path_length != 1) { + new_absolute_path[current_path_length] = '/'; + strcpy(&new_absolute_path[current_path_length + 1], path); + } else { + strcpy(&new_absolute_path[current_path_length], path); + } + TT_LOG_I(TAG, "Navigating from %s to %s", data->current_path, new_absolute_path); + + struct dirent** entries = NULL; + int count = tt_scandir(new_absolute_path, &entries, &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type); + if (count >= 0) { + TT_LOG_I(TAG, "%s has %u entries", new_absolute_path, count); + files_data_set_entries(data, entries, count); + strcpy(data->current_path, new_absolute_path); + } else { + TT_LOG_E(TAG, "Failed to fetch entries for %s", new_absolute_path); + } +} + +void files_data_set_entries_root(FilesData* data) { + data->current_path[0] = '/'; + data->current_path[1] = 0x00; +#ifdef ESP_PLATFORM + int dir_entries_count = 3; + struct dirent** dir_entries = malloc(sizeof(struct dirent*) * 3); + + dir_entries[0] = malloc(sizeof(struct dirent)); + dir_entries[0]->d_type = 4; + strcpy(dir_entries[0]->d_name, "assets"); + + dir_entries[1] = malloc(sizeof(struct dirent)); + dir_entries[1]->d_type = 4; + strcpy(dir_entries[1]->d_name, "config"); + + dir_entries[2] = malloc(sizeof(struct dirent)); + dir_entries[2]->d_type = 4; + strcpy(dir_entries[2]->d_name, "sdcard"); + + files_data_set_entries(data, dir_entries, dir_entries_count); + TT_LOG_I(TAG, "test: %s", dir_entries[0]->d_name); +#else + struct dirent** dir_entries = NULL; + int count = tt_scandir(data->current_path, &dir_entries, &tt_dirent_filter_dot_entries, &tt_dirent_sort_alpha_and_type); + if (count >= 0) { + files_data_set_entries(data, dir_entries, count); + } else { + TT_LOG_E(TAG, "Failed to fetch root dir items"); + } +#endif +} diff --git a/tactility/src/apps/system/files/files_data.h b/tactility/src/apps/system/files/files_data.h new file mode 100644 index 00000000..47000df9 --- /dev/null +++ b/tactility/src/apps/system/files/files_data.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "lvgl.h" + +#define MAX_PATH_LENGTH 256 + +typedef struct { + char current_path[MAX_PATH_LENGTH]; + struct dirent** dir_entries; + int dir_entries_count; + lv_obj_t* list; +} FilesData; + +#ifdef __cplusplus +extern "C" { +#endif + +FilesData* files_data_alloc(); +void files_data_free(FilesData* data); +void files_data_free_entries(FilesData* data); +void files_data_set_entries(FilesData* data, struct dirent** entries, int count); +void files_data_set_entries_for_path(FilesData* data, const char* path); +void files_data_set_entries_navigate_up(FilesData* data); +void files_data_set_entries_root(FilesData* data); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/tactility/src/apps/system/system_info/system_info.c b/tactility/src/apps/system/system_info/system_info.c index 6e68a8a2..a857b400 100644 --- a/tactility/src/apps/system/system_info/system_info.c +++ b/tactility/src/apps/system/system_info/system_info.c @@ -1,8 +1,17 @@ #include "app.h" #include "lvgl.h" +#include "ui/toolbar.h" -static void app_show(TT_UNUSED App app, lv_obj_t* parent) { - lv_obj_t* heap_info = lv_label_create(parent); +static void app_show(App app, lv_obj_t* parent) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + tt_toolbar_create_for_app(parent, app); + + lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(wrapper, 1); + + lv_obj_t* heap_info = lv_label_create(wrapper); lv_label_set_recolor(heap_info, true); lv_obj_set_width(heap_info, 200); lv_obj_set_style_text_align(heap_info, LV_TEXT_ALIGN_CENTER, 0); @@ -18,7 +27,7 @@ static void app_show(TT_UNUSED App app, lv_obj_t* parent) { #endif lv_obj_align(heap_info, LV_ALIGN_CENTER, 0, -20); - lv_obj_t* spi_info = lv_label_create(parent); + lv_obj_t* spi_info = lv_label_create(wrapper); lv_label_set_recolor(spi_info, true); lv_obj_set_width(spi_info, 200); lv_obj_set_style_text_align(spi_info, LV_TEXT_ALIGN_CENTER, 0); diff --git a/tactility/src/services/gui/gui_draw.c b/tactility/src/services/gui/gui_draw.c index e88771ce..66ddb51d 100644 --- a/tactility/src/services/gui/gui_draw.c +++ b/tactility/src/services/gui/gui_draw.c @@ -3,8 +3,6 @@ #include "gui_i.h" #include "log.h" #include "services/gui/widgets/statusbar.h" -#include "services/loader/loader.h" -#include "ui/spacer.h" #include "ui/style.h" #include "ui/toolbar.h" @@ -25,26 +23,6 @@ static lv_obj_t* create_app_views(Gui* gui, lv_obj_t* parent, App app) { tt_lv_statusbar_create(vertical_container); } - gui->toolbar = NULL; - if (flags.show_toolbar) { - const AppManifest* manifest = tt_app_get_manifest(app); - if (manifest != NULL) { - // TODO: Keep toolbar on app level so app can update it (app_set_toolbar() etc?) - Toolbar toolbar = { - .nav_action = &loader_stop_app, - .nav_icon = LV_SYMBOL_CLOSE, - .title = manifest->name - }; - lv_obj_t* toolbar_widget = tt_lv_toolbar_create(vertical_container, &toolbar); - lv_obj_set_pos(toolbar_widget, 0, STATUSBAR_HEIGHT); - - // Black area between toolbar and content below - lv_obj_t* spacer = tt_lv_spacer_create(vertical_container, 1, 2); - tt_lv_obj_set_style_bg_blacken(spacer); - gui->toolbar = toolbar_widget; - } - } - lv_obj_t* child_container = lv_obj_create(vertical_container); lv_obj_set_width(child_container, LV_PCT(100)); lv_obj_set_flex_grow(child_container, 1); diff --git a/tactility/src/services/gui/gui_i.h b/tactility/src/services/gui/gui_i.h index e0ddc0ef..84f78b49 100644 --- a/tactility/src/services/gui/gui_i.h +++ b/tactility/src/services/gui/gui_i.h @@ -25,7 +25,6 @@ struct Gui { // App-specific ViewPort* app_view_port; - lv_obj_t* _Nullable toolbar; lv_obj_t* _Nullable keyboard; lv_group_t* keyboard_group; }; diff --git a/tactility/src/services/gui/gui_keyboard.c b/tactility/src/services/gui/gui_keyboard.c index 77832ed0..dd239780 100644 --- a/tactility/src/services/gui/gui_keyboard.c +++ b/tactility/src/services/gui/gui_keyboard.c @@ -25,10 +25,6 @@ void gui_keyboard_show(lv_obj_t* textarea) { if (gui->keyboard) { lv_obj_clear_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN); lv_keyboard_set_textarea(gui->keyboard, textarea); - - if (gui->toolbar) { - lv_obj_add_flag(gui->toolbar, LV_OBJ_FLAG_HIDDEN); - } } gui_unlock(); @@ -39,9 +35,6 @@ void gui_keyboard_hide() { if (gui->keyboard) { lv_obj_add_flag(gui->keyboard, LV_OBJ_FLAG_HIDDEN); - if (gui->toolbar) { - lv_obj_clear_flag(gui->toolbar, LV_OBJ_FLAG_HIDDEN); - } } gui_unlock(); diff --git a/tactility/src/tactility.c b/tactility/src/tactility.c index 34370512..4123ebb7 100644 --- a/tactility/src/tactility.c +++ b/tactility/src/tactility.c @@ -26,12 +26,14 @@ static const ServiceManifest* const system_services[] = { extern const AppManifest desktop_app; extern const AppManifest display_app; +extern const AppManifest files_app; extern const AppManifest settings_app; extern const AppManifest system_info_app; static const AppManifest* const system_apps[] = { &desktop_app, &display_app, + &files_app, &settings_app, &system_info_app }; diff --git a/tactility/src/ui/toolbar.c b/tactility/src/ui/toolbar.c index 10a8d7bc..75c4f6f1 100644 --- a/tactility/src/ui/toolbar.c +++ b/tactility/src/ui/toolbar.c @@ -6,49 +6,105 @@ #include "lvgl.h" -#define TOOLBAR_HEIGHT 40 -#define TOOLBAR_FONT_HEIGHT 18 +static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj); -static void on_nav_pressed(lv_event_t* event) { - NavAction action = (NavAction)event->user_data; - action(); +static const lv_obj_class_t toolbar_class = { + .constructor_cb = &toolbar_constructor, + .destructor_cb = NULL, + .width_def = LV_PCT(100), + .height_def = TOOLBAR_HEIGHT, + .group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE, + .instance_size = sizeof(Toolbar), + .base_class = &lv_obj_class +}; + +static void stop_app(TT_UNUSED lv_event_t* event) { + loader_stop_app(); } -lv_obj_t* tt_lv_toolbar_create(lv_obj_t* parent, const Toolbar* toolbar) { - lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_height(wrapper, TOOLBAR_HEIGHT); - tt_lv_obj_set_style_no_padding(wrapper); - lv_obj_center(wrapper); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); +static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) { + LV_UNUSED(class_p); + LV_TRACE_OBJ_CREATE("begin"); + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(obj, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + LV_TRACE_OBJ_CREATE("finished"); +} - lv_coord_t title_offset_x = (TOOLBAR_HEIGHT - TOOLBAR_FONT_HEIGHT - 8) / 4 * 3; - lv_coord_t title_offset_y = (TOOLBAR_HEIGHT - TOOLBAR_FONT_HEIGHT - 8) / 2; +lv_obj_t* tt_toolbar_create(lv_obj_t* parent, const char* title) { + LV_LOG_INFO("begin"); + lv_obj_t* obj = lv_obj_class_create_obj(&toolbar_class, parent); + lv_obj_class_init_obj(obj); - lv_obj_t* close_button = lv_btn_create(wrapper); - lv_obj_set_size(close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4); - tt_lv_obj_set_style_no_padding(close_button); - lv_obj_add_event_cb(close_button, &on_nav_pressed, LV_EVENT_CLICKED, toolbar->nav_action); - lv_obj_t* close_button_image = lv_img_create(close_button); - lv_img_set_src(close_button_image, toolbar->nav_icon); // e.g. LV_SYMBOL_CLOSE - lv_obj_align(close_button_image, LV_ALIGN_CENTER, 0, 0); + Toolbar* toolbar = (Toolbar*)obj; + + tt_lv_obj_set_style_no_padding(obj); + lv_obj_center(obj); + lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW); + + lv_coord_t title_offset_x = (TOOLBAR_HEIGHT - TOOLBAR_TITLE_FONT_HEIGHT - 8) / 4 * 3; + lv_coord_t title_offset_y = (TOOLBAR_HEIGHT - TOOLBAR_TITLE_FONT_HEIGHT - 8) / 2; + + toolbar->close_button = lv_btn_create(obj); + lv_obj_set_size(toolbar->close_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4); + tt_lv_obj_set_style_no_padding(toolbar->close_button); + toolbar->close_button_image = lv_img_create(toolbar->close_button); + lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0); // Need spacer to avoid button press glitch animation - tt_lv_spacer_create(wrapper, title_offset_x, 1); + tt_lv_spacer_create(obj, title_offset_x, 1); - lv_obj_t* label_container = lv_obj_create(wrapper); + lv_obj_t* label_container = lv_obj_create(obj); tt_lv_obj_set_style_no_padding(label_container); lv_obj_set_style_border_width(label_container, 0, 0); lv_obj_set_height(label_container, LV_PCT(100)); // 2% less due to 4px translate (it's not great, but it works) lv_obj_set_flex_grow(label_container, 1); - lv_obj_t* title_label = lv_label_create(label_container); - lv_label_set_text(title_label, toolbar->title); - lv_obj_set_style_text_font(title_label, &lv_font_montserrat_18, 0); // TODO replace with size 18 - lv_obj_set_height(title_label, TOOLBAR_FONT_HEIGHT); + toolbar->title_label = lv_label_create(label_container); + lv_obj_set_style_text_font(toolbar->title_label, &lv_font_montserrat_18, 0); // TODO replace with size 18 + lv_obj_set_height(toolbar->title_label, TOOLBAR_TITLE_FONT_HEIGHT); + lv_label_set_text(toolbar->title_label, title); + lv_obj_set_pos(toolbar->title_label, 0, title_offset_y); + lv_obj_set_style_text_align(toolbar->title_label, LV_TEXT_ALIGN_LEFT, 0); - lv_obj_set_pos(title_label, 0, title_offset_y); - lv_obj_set_style_text_align(title_label, LV_TEXT_ALIGN_LEFT, 0); + toolbar->action_container = lv_obj_create(obj); + lv_obj_set_width(toolbar->action_container, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(toolbar->action_container, 0, 0); + lv_obj_set_style_border_width(toolbar->action_container, 0, 0); - return wrapper; + return obj; +} + +lv_obj_t* tt_toolbar_create_for_app(lv_obj_t* parent, App app) { + const AppManifest* manifest = tt_app_get_manifest(app); + lv_obj_t* toolbar = tt_toolbar_create(parent, manifest->name); + tt_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, &stop_app, NULL); + return toolbar; +} + +void tt_toolbar_set_title(lv_obj_t* obj, const char* title) { + Toolbar* toolbar = (Toolbar*)obj; + lv_label_set_text(toolbar->title_label, title); +} + +void tt_toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data) { + Toolbar* toolbar = (Toolbar*)obj; + lv_obj_add_event_cb(toolbar->close_button, callback, LV_EVENT_CLICKED, user_data); + lv_img_set_src(toolbar->close_button_image, icon); // e.g. LV_SYMBOL_CLOSE +} + +uint8_t tt_toolbar_add_action(lv_obj_t* obj, const char* icon, const char* text, lv_event_cb_t callback, void* user_data) { + Toolbar* toolbar = (Toolbar*)obj; + uint8_t id = toolbar->action_count; + tt_check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached"); + toolbar->action_count++; + + lv_obj_t* action_button = lv_btn_create(toolbar->action_container); + lv_obj_set_size(action_button, TOOLBAR_HEIGHT - 4, TOOLBAR_HEIGHT - 4); + tt_lv_obj_set_style_no_padding(action_button); + lv_obj_add_event_cb(action_button, callback, LV_EVENT_CLICKED, user_data); + lv_obj_t* action_button_image = lv_img_create(action_button); + lv_img_set_src(action_button_image, icon); + lv_obj_align(action_button_image, LV_ALIGN_CENTER, 0, 0); + + return id; } diff --git a/tactility/src/ui/toolbar.h b/tactility/src/ui/toolbar.h index 7e853c03..7d873f52 100644 --- a/tactility/src/ui/toolbar.h +++ b/tactility/src/ui/toolbar.h @@ -1,20 +1,40 @@ #pragma once #include "lvgl.h" +#include "app.h" #ifdef __cplusplus extern "C" { #endif -typedef void(*NavAction)(); +#define TOOLBAR_HEIGHT 40 +#define TOOLBAR_ACTION_LIMIT 8 +#define TOOLBAR_TITLE_FONT_HEIGHT 18 + +typedef void(*ToolbarActionCallback)(void* _Nullable context); typedef struct { - const char* _Nullable title; - const char* _Nullable nav_icon; // LVGL compatible definition (e.g. local file or embedded icon from LVGL) - NavAction nav_action; + const char* icon; + const char* text; + ToolbarActionCallback callback; + void* _Nullable callback_context; +} ToolbarAction; + +typedef struct { + lv_obj_t obj; + lv_obj_t* title_label; + lv_obj_t* close_button; + lv_obj_t* close_button_image; + lv_obj_t* action_container; + ToolbarAction* action_array[TOOLBAR_ACTION_LIMIT]; + uint8_t action_count; } Toolbar; -lv_obj_t* tt_lv_toolbar_create(lv_obj_t* parent, const Toolbar* toolbar); +lv_obj_t* tt_toolbar_create(lv_obj_t* parent, const char* title); +lv_obj_t* tt_toolbar_create_for_app(lv_obj_t* parent, App app); +void tt_toolbar_set_title(lv_obj_t* obj, const char* title); +void tt_toolbar_set_nav_action(lv_obj_t* obj, const char* icon, lv_event_cb_t callback, void* user_data); +uint8_t tt_toolbar_add_action(lv_obj_t* obj, const char* icon, const char* text, lv_event_cb_t callback, void* user_data); #ifdef __cplusplus }