Ken Van Hoeylandt 7638602201 Theming WIP
2026-10-06 23:04:31 +02:00

296 lines
12 KiB
C++

#include <app/event.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>
#include <cstring>
#include <lvgl.h>
#include <lvgl/icons/launcher.h>
#include <lvgl/fonts.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/icon_button.h>
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/pointer.h>
#include <tactility/drivers/power_supply.h>
#include <tactility/freertos/semphr.h>
#include <tactility/freertos/task.h>
#include <tactility/log.h>
#include <tactility/memory.h>
#include <Tactility/app/setup/Setup.h>
#include <Tactility/settings/BootSettings.h>
#include <Tactility/Tactility.h>
namespace tt::app::launcher {
constexpr auto* TAG = "Launcher";
namespace {
int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) {
const int32_t usable = std::max<int32_t>(0, available_span - (3 * total_button_size));
return std::min<int32_t>(usable / 16, total_button_size / 2);
}
// Spreads the buttons along the display's longest side. The button padding comes from the theme.
void applyButtonMargins(lv_obj_t* buttonsWrapper, bool isLandscape) {
const uint32_t child_count = lv_obj_get_child_count(buttonsWrapper);
if (child_count == 0) {
return;
}
auto* first_button = lv_obj_get_child(buttonsWrapper, 0);
const auto total_button_size = static_cast<int32_t>(lvgl_get_launcher_icon_font_height()) +
lv_obj_get_style_pad_left(first_button, LV_PART_MAIN) + lv_obj_get_style_pad_right(first_button, LV_PART_MAIN);
const auto* display = lv_obj_get_display(buttonsWrapper);
const auto span = isLandscape ? lv_display_get_horizontal_resolution(display) : lv_display_get_vertical_resolution(display);
const int32_t margin = computeButtonMargin(span, total_button_size);
for (uint32_t i = 0; i < child_count; i++) {
auto* button = lv_obj_get_child(buttonsWrapper, i);
lv_obj_set_style_margin_hor(button, isLandscape ? margin : 0, LV_STATE_DEFAULT);
lv_obj_set_style_margin_ver(button, isLandscape ? 0 : margin, LV_STATE_DEFAULT);
}
}
void onAppPressed(lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
uint32_t instance_id = 0;
AppStartContext context;
if (app_start_context_from_id(appId, &context) == ERROR_NONE) {
app_start_with_context(&context, &instance_id);
}
}
lv_obj_t* createAppButton(lv_obj_t* parent, LvglIconButtonVariant variant, const char* imageFile, const char* appId) {
const auto button_size = lvgl_get_launcher_icon_font_height();
auto* apps_button = lvgl_icon_button_create_variant(parent, variant);
// create the image first
auto* button_image = lv_image_create(apps_button);
lv_obj_set_style_text_font(button_image, lvgl_get_launcher_icon_font(), LV_STATE_DEFAULT);
lv_image_set_src(button_image, imageFile);
// Ensure it's square (Material Symbols are slightly wider than tall)
lv_obj_set_size(button_image, button_size, button_size);
lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
return apps_button;
}
bool shouldShowPowerButton() {
bool show_power_button = false;
device_for_each_of_type(&POWER_SUPPLY_TYPE, &show_power_button, [](Device* device, void* context) {
if (device_is_ready(device) && power_supply_supports_power_off(device)) {
*static_cast<bool*>(context) = true;
return false; // stop iterating
} else {
return true; // continue iterating
}
});
return show_power_button;
}
void onButtonsWrapperResized(lv_event_t* e);
// The screen object outlives this window's own widgets (lvgl-window-manager deletes and
// recreates only the topmost window's widget on every app switch, not the screen itself), so
// the LV_EVENT_SIZE_CHANGED callback registered on it must be removed once buttons_wrapper is
// destroyed, to avoid a dangling user-data pointer the next time the display rotates while a
// different window is topmost.
void onButtonsWrapperDeleted(lv_event_t* e) {
auto* buttons_wrapper = lv_event_get_target_obj(e);
auto* screen = lv_obj_get_screen(buttons_wrapper);
lv_obj_remove_event_cb_with_user_data(screen, onButtonsWrapperResized, buttons_wrapper);
}
// Re-applies the flex direction and per-button margins when the display orientation changes
// while the launcher is the visible window (these are decided once at createWidgets() based on
// the resolution at that time, so a later rotation needs this to catch up).
void onButtonsWrapperResized(lv_event_t* e) {
auto* buttons_wrapper = static_cast<lv_obj_t*>(lv_event_get_user_data(e));
const auto* display = lv_obj_get_display(buttons_wrapper);
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
const auto vertical_px = lv_display_get_vertical_resolution(display);
const bool is_landscape_display = horizontal_px >= vertical_px;
const auto current_flow = lv_obj_get_style_flex_flow(buttons_wrapper, LV_PART_MAIN);
const bool was_landscape = current_flow == LV_FLEX_FLOW_ROW;
if (is_landscape_display == was_landscape) {
return;
}
lv_obj_set_flex_flow(buttons_wrapper, is_landscape_display ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
applyButtonMargins(buttons_wrapper, is_landscape_display);
}
void createWidgets(lv_obj_t* parent, void*) {
auto* buttons_wrapper = lv_obj_create(parent);
lv_obj_align(buttons_wrapper, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(buttons_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_border_width(buttons_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_grow(buttons_wrapper, 1);
// Fix for button selection
lv_obj_set_style_pad_all(buttons_wrapper, 6, LV_STATE_DEFAULT);
const auto* display = lv_obj_get_display(parent);
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
const auto vertical_px = lv_display_get_vertical_resolution(display);
const bool is_landscape_display = horizontal_px >= vertical_px;
if (is_landscape_display) {
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_ROW);
} else {
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_COLUMN);
}
// The app list is the main action, so it stands out more than the others
auto* app_list_button = createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_APPS, "tactility.applist");
createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files");
createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings");
applyButtonMargins(buttons_wrapper, is_landscape_display);
// The launcher's container is several levels below the screen, and LVGL only sends
// LV_EVENT_SIZE_CHANGED to the screen object itself on a resolution change - so the
// handler is attached there, with buttons_wrapper passed through as user data.
lv_obj_add_event_cb(lv_obj_get_screen(parent), onButtonsWrapperResized, LV_EVENT_SIZE_CHANGED, buttons_wrapper);
lv_obj_add_event_cb(buttons_wrapper, onButtonsWrapperDeleted, LV_EVENT_DELETE, nullptr);
// Some devices (e.g. T-Lora Pager) have no other way to power off, so the
// button stays in the launcher; the confirmation flow lives in the PowerOff app.
if (shouldShowPowerButton()) {
auto* power_button = lvgl_icon_button_create(parent);
lv_obj_set_style_pad_all(power_button, 8, 0);
lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)"tactility.poweroff");
auto* power_label = lv_label_create(power_button);
lv_label_set_text(power_label, LV_SYMBOL_POWER);
}
// If we don't have a touch device, we assume there's some other kind of input like a keyboard, an encoder or button control
// In that scenario we want to automatically have the app list button selected so the user doesn't have to press the widget selection
// an extra time.
if (!device_has_active_by_type(&POINTER_TYPE)) {
// lv_obj_update_layout(parent); // Resolve flex layout first, so focus/state invalidate against final coords
lv_group_focus_obj(app_list_button);
lv_obj_add_state(app_list_button, LV_STATE_FOCUS_KEY);
}
}
void runAutoStart() {
settings::BootSettings boot_properties;
AppStartContext context;
if (
// Auto-start due to built-in requirement
strcmp(CONFIG_TT_AUTO_START_APP_ID, "") != 0 &&
app_start_context_from_id(CONFIG_TT_AUTO_START_APP_ID, &context) == ERROR_NONE
) {
LOG_I(TAG, "Starting %s", CONFIG_TT_AUTO_START_APP_ID);
uint32_t app_launch_id;
app_start_with_context(&context, &app_launch_id);
} else if (
// Auto-start due to user configuration
settings::loadBootSettings(boot_properties) &&
!boot_properties.autoStartAppId.empty() &&
app_start_context_from_id(boot_properties.autoStartAppId.c_str(), &context) == ERROR_NONE
) {
LOG_I(TAG, "Starting %s", boot_properties.autoStartAppId.c_str());
uint32_t app_launch_id;
app_start_with_context(&context, &app_launch_id);
} else {
// No auto-start, consider running system setup
if (!setup::isCompleted()) {
setup::start();
}
}
}
constexpr size_t AUTO_START_STACK_DEPTH = 4096 / sizeof(StackType_t);
struct AutoStartTaskContext {
SemaphoreHandle_t done;
};
void autoStartTaskMain(void* param) {
auto* context = static_cast<AutoStartTaskContext*>(param);
runAutoStart();
xSemaphoreGive(context->done);
vTaskDelete(nullptr);
}
// runAutoStart() reads from flash, and apps run on PSRAM when available.
// Launcher stays in memory, so we prefer to keep the PSRAM task and temporarily run an IRAM task for auto start logic.
void runAutoStartIsolated() {
AutoStartTaskContext context { .done = xSemaphoreCreateBinary() };
check(context.done != nullptr);
TaskHandle_t auto_start_task = nullptr;
check(xTaskCreate(autoStartTaskMain, "LauncherAutoStart", AUTO_START_STACK_DEPTH, &context, tskIDLE_PRIORITY, &auto_start_task) == pdPASS);
xSemaphoreTake(context.done, portMAX_DELAY);
vSemaphoreDelete(context.done);
}
int32_t appMain(int argc, char* argv[]) {
uint32_t appInstanceId = app_scheduler_current_app_id();
TaskEventGroup event_group {};
task_event_group_construct(&event_group);
AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
runAutoStartIsolated();
// The launcher is meant to stay resident (it's the home screen) - it only gives up its
// thread when app-module's scheduler asks it to (e.g. another new-model app is started).
while (true) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
bool shouldClose = false;
AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) {
if (event.type == APP_EVENT_CLOSE) {
shouldClose = true;
break;
}
}
if (shouldClose) break;
}
window_manager_remove(window);
check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_destruct(&event_group);
return 0;
}
} // namespace
extern const ::AppManifest manifest = {
.id = "tactility.launcher",
.name = "Launcher",
.category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN,
// No file IO, so callstack can be in external RAM
.stack = { .depth = 3072 , .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL }
};
// Kept for Tactility/Private/Tactility/app/launcher/Launcher.h's existing declaration (still
// used by the old, unconverted CrashDiagnostics app to return to the launcher after a crash).
uint32_t start() {
uint32_t instance_id = 0;
AppStartContext context = app_start_context_for_manifest(&manifest);
app_start_with_context(&context, &instance_id);
return instance_id;
}
} // namespace