From 5880e841a3ec8e7485eca6c39644d6f820ddb0ed Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 6 Feb 2024 23:18:34 +0100 Subject: [PATCH] Implemented Files app (#33) - Created Files app to browse PC and ESP32 files. - Refactored toolbars so it's now a proper widget and allows for changing its properties from the app - Toolbar now has extra action buttons - Settings app now has a proper icon - Minor cleanup in Desktop app --- README.md | 5 + app-esp/src/hello_world/hello_world.c | 4 + app-sim/src/hello_world/hello_world.c | 5 +- app-sim/src/lv_conf.h | 4 +- docs/ideas.md | 4 + docs/pics/hello-world.png | Bin 10373 -> 5300 bytes tactility-core/src/string_utils.c | 26 +++ tactility-core/src/string_utils.h | 29 ++++ .../system/wifi_connect/wifi_connect_view.c | 22 +-- .../system/wifi_connect/wifi_connect_view.h | 1 - .../src/apps/system/wifi_manage/wifi_manage.c | 2 +- .../system/wifi_manage/wifi_manage_view.c | 18 ++- .../system/wifi_manage/wifi_manage_view.h | 3 +- tactility/CMakeLists.txt | 1 + tactility/src/app.c | 18 +-- tactility/src/app.h | 1 - tactility/src/apps/desktop/desktop.c | 3 +- tactility/src/apps/settings/display/display.c | 22 +-- tactility/src/apps/settings/settings.c | 11 +- tactility/src/apps/system/files/file_utils.c | 67 ++++++++ tactility/src/apps/system/files/file_utils.h | 68 ++++++++ tactility/src/apps/system/files/files.c | 152 ++++++++++++++++++ tactility/src/apps/system/files/files_data.c | 118 ++++++++++++++ tactility/src/apps/system/files/files_data.h | 29 ++++ .../src/apps/system/system_info/system_info.c | 15 +- tactility/src/services/gui/gui_draw.c | 22 --- tactility/src/services/gui/gui_i.h | 1 - tactility/src/services/gui/gui_keyboard.c | 7 - tactility/src/tactility.c | 2 + tactility/src/ui/toolbar.c | 116 +++++++++---- tactility/src/ui/toolbar.h | 30 +++- 31 files changed, 689 insertions(+), 117 deletions(-) create mode 100644 tactility-core/src/string_utils.c create mode 100644 tactility-core/src/string_utils.h create mode 100644 tactility/src/apps/system/files/file_utils.c create mode 100644 tactility/src/apps/system/files/file_utils.h create mode 100644 tactility/src/apps/system/files/files.c create mode 100644 tactility/src/apps/system/files/files_data.c create mode 100644 tactility/src/apps/system/files/files_data.h 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 21f6ab00ae85402576be039c083447c1d815b005..f4651fb3764ab82be331111ad6ccefd72dff5c46 100644 GIT binary patch literal 5300 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 literal 10373 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 at-No~b2n5(Bl!J*L0`v6yAWxA`9A>MFSdUG 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 }