diff --git a/Data/assets/desktop_icon_apps.png b/Data/assets/desktop_icon_apps.png new file mode 100644 index 00000000..9381aaa4 Binary files /dev/null and b/Data/assets/desktop_icon_apps.png differ diff --git a/Data/assets/desktop_icon_files.png b/Data/assets/desktop_icon_files.png new file mode 100644 index 00000000..8e6c6055 Binary files /dev/null and b/Data/assets/desktop_icon_files.png differ diff --git a/Data/assets/desktop_icon_settings.png b/Data/assets/desktop_icon_settings.png new file mode 100644 index 00000000..cdb52d7b Binary files /dev/null and b/Data/assets/desktop_icon_settings.png differ diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 35c75925..49f7b27a 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -1,4 +1,6 @@ # TODOs +- Crash logs stored on sdcard or elsewhere: perhaps show crash screen after recovering from crash (with QR code? https://github.com/ricmoo/QRCode) +- Logging - AppContext's onResult should pass the app id (or launch request id!) that was started, so we can differentiate between multiple types of apps being launched - Loader: Use Timer instead of Thread, and move API to `tt::app::` - Gpio: Use Timer instead of Thread diff --git a/Documentation/pics/app-lifecycle.png~ b/Documentation/pics/app-lifecycle.png~ deleted file mode 100644 index 874e863c..00000000 Binary files a/Documentation/pics/app-lifecycle.png~ and /dev/null differ diff --git a/Documentation/pics/screenshot-AppList.png b/Documentation/pics/screenshot-AppList.png new file mode 100644 index 00000000..dfa444c4 Binary files /dev/null and b/Documentation/pics/screenshot-AppList.png differ diff --git a/Documentation/pics/screenshot-Desktop.png b/Documentation/pics/screenshot-Desktop.png index a1c2fd54..d46e3dc1 100644 Binary files a/Documentation/pics/screenshot-Desktop.png and b/Documentation/pics/screenshot-Desktop.png differ diff --git a/Documentation/pics/screenshot-Gpio.png b/Documentation/pics/screenshot-Gpio.png deleted file mode 100644 index 0e6084cf..00000000 Binary files a/Documentation/pics/screenshot-Gpio.png and /dev/null differ diff --git a/README.md b/README.md index edfea1a5..5c394b06 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ ## Overview -Tactility is a front-end application platform for ESP32. -It is currently intended for touchscreen devices, but the goal is to also support different types of input in the future. -Tactility provides an application framework that borrows concepts from [Flipper Zero](https://github.com/flipperdevices/flipperzero-firmware/) and mobile phone operating systems. +Tactility is an operating system that is focusing on the ESP32 microcontroller. ![photo of devices running Tactility](Documentation/pics/tactility-devices.webp)  ![screenshot of desktop app](Documentation/pics/screenshot-Desktop.png) @@ -14,7 +12,7 @@ Next to desktop functionality, Tactility makes it easy to manage system settings There are also built-in apps: -![screenshot of GPIO app](Documentation/pics/screenshot-Gpio.png) ![screenshot of files app](Documentation/pics/screenshot-Files.png) +![screenshot off app list app](Documentation/pics/screenshot-AppList.png) ![screenshot of files app](Documentation/pics/screenshot-Files.png) Play with the built-in apps or build your own! Use one of the supported devices or set up the drivers for your own hardware platform. @@ -26,8 +24,9 @@ Noteworthy features: - Includes a PC simulator build target to speed up development. Requirements: -- ESP32 (any?) with a touchscreen +- ESP32 (any?) - [esp-idf 5.3](https://docs.espressif.com/projects/esp-idf/en/release-v5.3/esp32/get-started/index.html) or a newer v5.3.x +- (for PC simulator) SDL2 library, including SDL image ## Making apps is easy! diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index b5234a52..1cb5d3cb 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -36,6 +36,7 @@ static const std::vector system_services = { namespace app { namespace alertdialog { extern const AppManifest manifest; } + namespace applist { extern const AppManifest manifest; } namespace boot { extern const AppManifest manifest; } namespace desktop { extern const AppManifest manifest; } namespace files { extern const AppManifest manifest; } @@ -64,6 +65,7 @@ extern const app::AppManifest screenshot_app; static const std::vector system_apps = { &app::alertdialog::manifest, + &app::applist::manifest, &app::boot::manifest, &app::desktop::manifest, &app::display::manifest, diff --git a/Tactility/Source/app/applist/AppList.cpp b/Tactility/Source/app/applist/AppList.cpp new file mode 100644 index 00000000..b569cc58 --- /dev/null +++ b/Tactility/Source/app/applist/AppList.cpp @@ -0,0 +1,64 @@ +#include "app/ManifestRegistry.h" +#include "Assets.h" +#include "Check.h" +#include "lvgl.h" +#include +#include "service/loader/Loader.h" +#include "lvgl/Toolbar.h" + +namespace tt::app::applist { + +static void onAppPressed(lv_event_t* e) { + lv_event_code_t code = lv_event_get_code(e); + if (code == LV_EVENT_CLICKED) { + const auto* manifest = static_cast(lv_event_get_user_data(e)); + service::loader::startApp(manifest->id, false); + } +} + +static void createAppWidget(const AppManifest* manifest, void* parent) { + tt_check(parent); + auto* list = static_cast(parent); + const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; + lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); + lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_CLICKED, (void*)manifest); +} + +static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { + auto* toolbar = lvgl::toolbar_create(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + lv_obj_t* list = lv_list_create(parent); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_align_to(list, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0); + + auto toolbar_height = lv_obj_get_height(toolbar); + auto parent_content_height = lv_obj_get_content_height(parent); + lv_obj_set_height(list, parent_content_height - toolbar_height); + + auto manifests = getApps(); + std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); + + lv_list_add_text(list, "User"); + for (const auto& manifest: manifests) { + if (manifest->type == TypeUser) { + createAppWidget(manifest, list); + } + } + + lv_list_add_text(list, "System"); + for (const auto& manifest: manifests) { + if (manifest->type == TypeSystem) { + createAppWidget(manifest, list); + } + } +} + +extern const AppManifest manifest = { + .id = "AppList", + .name = "Apps", + .type = TypeHidden, + .onShow = onShow, +}; + +} // namespace diff --git a/Tactility/Source/app/desktop/Desktop.cpp b/Tactility/Source/app/desktop/Desktop.cpp index 2291c43a..bd901167 100644 --- a/Tactility/Source/app/desktop/Desktop.cpp +++ b/Tactility/Source/app/desktop/Desktop.cpp @@ -1,49 +1,67 @@ #include "app/ManifestRegistry.h" -#include "Assets.h" #include "Check.h" #include "lvgl.h" -#include #include "service/loader/Loader.h" namespace tt::app::desktop { -static void onAppPressed(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - if (code == LV_EVENT_CLICKED) { - const auto* manifest = static_cast(lv_event_get_user_data(e)); - service::loader::startApp(manifest->id, false); - } +static void onAppPressed(TT_UNUSED lv_event_t* e) { + auto* appId = (const char*)lv_event_get_user_data(e); + service::loader::startApp(appId, false); } -static void createAppWidget(const AppManifest* manifest, void* parent) { - tt_check(parent); - auto* list = static_cast(parent); - const void* icon = !manifest->icon.empty() ? manifest->icon.c_str() : TT_ASSETS_APP_ICON_FALLBACK; - lv_obj_t* btn = lv_list_add_button(list, icon, manifest->name.c_str()); - lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_CLICKED, (void*)manifest); +static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char* imageFile, const char* appId, int32_t buttonPaddingLeft) { + auto* wrapper = lv_obj_create(parent); + lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(wrapper, 0, 0); + lv_obj_set_style_pad_left(wrapper, buttonPaddingLeft, 0); + lv_obj_set_style_pad_right(wrapper, 0, 0); + lv_obj_set_style_border_width(wrapper, 0, 0); + + auto* apps_button = lv_button_create(wrapper); + lv_obj_set_style_pad_hor(apps_button, 0, 0); + lv_obj_set_style_pad_top(apps_button, 0, 0); + lv_obj_set_style_pad_bottom(apps_button, 16, 0); + lv_obj_set_style_shadow_width(apps_button, 0, 0); + lv_obj_set_style_border_width(apps_button, 0, 0); + lv_obj_set_style_bg_color(apps_button, lv_color_white(), 0); + + auto* button_image = lv_image_create(apps_button); + lv_image_set_src(button_image, imageFile); + lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_CLICKED, (void*)appId); + lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), 0); + lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, 0); + + auto* label = lv_label_create(wrapper); + lv_label_set_text(label, title); + lv_obj_align(label, LV_ALIGN_BOTTOM_MID, 0, 0); + + return wrapper; } static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { - lv_obj_t* list = lv_list_create(parent); - lv_obj_set_size(list, LV_PCT(100), LV_PCT(100)); - lv_obj_center(list); + auto* wrapper = lv_obj_create(parent); - auto manifests = getApps(); - std::sort(manifests.begin(), manifests.end(), SortAppManifestByName); + lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_style_pad_all(wrapper, 0, 0); + lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_flex_grow(wrapper, 1); - lv_list_add_text(list, "User"); - for (const auto& manifest: manifests) { - if (manifest->type == TypeUser) { - createAppWidget(manifest, list); - } + auto* display = lv_obj_get_display(parent); + auto orientation = lv_display_get_rotation(display); + if (orientation == LV_DISPLAY_ROTATION_0 || orientation == LV_DISPLAY_ROTATION_180) { + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + } else { + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); } - lv_list_add_text(list, "System"); - for (const auto& manifest: manifests) { - if (manifest->type == TypeSystem) { - createAppWidget(manifest, list); - } - } + int32_t available_width = lv_display_get_horizontal_resolution(display) - (3 * 80); + int32_t padding = TT_MIN(available_width / 4, 64); + + createAppButton(wrapper, "Apps", "A:/assets/desktop_icon_apps.png", "AppList", 0); + createAppButton(wrapper, "Files", "A:/assets/desktop_icon_files.png", "Files", padding); + createAppButton(wrapper, "Settings", "A:/assets/desktop_icon_settings.png", "Settings", padding); } extern const AppManifest manifest = { diff --git a/Tactility/Source/app/files/Files.cpp b/Tactility/Source/app/files/Files.cpp index 6e0e3447..97dfb87d 100644 --- a/Tactility/Source/app/files/Files.cpp +++ b/Tactility/Source/app/files/Files.cpp @@ -256,7 +256,7 @@ extern const AppManifest manifest = { .id = "Files", .name = "Files", .icon = TT_ASSETS_APP_ICON_FILES, - .type = TypeSystem, + .type = TypeHidden, .onStart = onStart, .onShow = onShow, }; diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp index 56ea531e..19a6fac3 100644 --- a/Tactility/Source/app/settings/Settings.cpp +++ b/Tactility/Source/app/settings/Settings.cpp @@ -46,7 +46,7 @@ extern const AppManifest manifest = { .id = "Settings", .name = "Settings", .icon = TT_ASSETS_APP_ICON_SETTINGS, - .type = TypeSystem, + .type = TypeHidden, .onShow = onShow, };