mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-02-18 19:03:16 +00:00
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
This commit is contained in:
parent
93e4378a9e
commit
5880e841a3
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.2 KiB |
26
tactility-core/src/string_utils.c
Normal file
26
tactility-core/src/string_utils.c
Normal file
@ -0,0 +1,26 @@
|
||||
#include "string_utils.h"
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
tactility-core/src/string_utils.h
Normal file
29
tactility-core/src/string_utils.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#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
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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=)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,7 +18,6 @@ typedef enum {
|
||||
typedef union {
|
||||
struct {
|
||||
bool show_statusbar : 1;
|
||||
bool show_toolbar : 1;
|
||||
};
|
||||
unsigned char flags;
|
||||
} AppFlags;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
67
tactility/src/apps/system/files/file_utils.c
Normal file
67
tactility/src/apps/system/files/file_utils.c
Normal file
@ -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;
|
||||
};
|
||||
68
tactility/src/apps/system/files/file_utils.h
Normal file
68
tactility/src/apps/system/files/file_utils.h
Normal file
@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <dirent.h>
|
||||
|
||||
/** 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
|
||||
152
tactility/src/apps/system/files/files.c
Normal file
152
tactility/src/apps/system/files/files.c
Normal file
@ -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 <dirent.h>
|
||||
|
||||
#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
|
||||
};
|
||||
118
tactility/src/apps/system/files/files_data.c
Normal file
118
tactility/src/apps/system/files/files_data.c
Normal file
@ -0,0 +1,118 @@
|
||||
#include "files_data.h"
|
||||
#include "file_utils.h"
|
||||
#include "tactility_core.h"
|
||||
#include <string_utils.h>
|
||||
|
||||
#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
|
||||
}
|
||||
29
tactility/src/apps/system/files/files_data.h
Normal file
29
tactility/src/apps/system/files/files_data.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <dirent.h>
|
||||
#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
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user