#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace tt::app::appearancesettings { constexpr auto* TAG = "AppearanceSettings"; extern const ::AppManifest manifest; namespace { constexpr uint16_t MIN_FONT_SIZE = 10; constexpr uint16_t MAX_FONT_SIZE = 32; constexpr size_t APPLY_THREAD_STACK_SIZE = 8192; enum class FontSlot { Regular, Mono }; struct NamedColor { const char* name; lv_palette_t palette; }; constexpr NamedColor PALETTE_COLORS[] = { { "Red", LV_PALETTE_RED }, { "Pink", LV_PALETTE_PINK }, { "Purple", LV_PALETTE_PURPLE }, { "Deep purple", LV_PALETTE_DEEP_PURPLE }, { "Indigo", LV_PALETTE_INDIGO }, { "Blue", LV_PALETTE_BLUE }, { "Light blue", LV_PALETTE_LIGHT_BLUE }, { "Cyan", LV_PALETTE_CYAN }, { "Teal", LV_PALETTE_TEAL }, { "Green", LV_PALETTE_GREEN }, { "Light green", LV_PALETTE_LIGHT_GREEN }, { "Lime", LV_PALETTE_LIME }, { "Yellow", LV_PALETTE_YELLOW }, { "Amber", LV_PALETTE_AMBER }, { "Orange", LV_PALETTE_ORANGE }, { "Deep orange", LV_PALETTE_DEEP_ORANGE }, { "Brown", LV_PALETTE_BROWN }, { "Blue grey", LV_PALETTE_BLUE_GREY }, { "Grey", LV_PALETTE_GREY }, }; constexpr size_t COLOR_COUNT = 3; constexpr const char* COLOR_TITLES[COLOR_COUNT] = { "Primary color", "Secondary color", "Error color" }; struct ColorRowWidgets { lv_obj_t* row = nullptr; lv_obj_t* swatch = nullptr; }; struct FontRowWidgets { lv_obj_t* defaultButton = nullptr; lv_obj_t* fileLabel = nullptr; lv_obj_t* warningLabel = nullptr; }; struct Context { uint32_t appInstanceId = 0; TaskEventGroup* eventGroup = nullptr; uint32_t selectRegularBit = 0; uint32_t selectMonoBit = 0; uint32_t selectColorBit = 0; uint32_t applyBit = 0; uint32_t cacheUpdatedBit = 0; settings::appearance::AppearanceSettings savedSettings; /** Accessed with the LVGL lock held */ settings::appearance::AppearanceSettings pendingSettings; /** Characters in the pending font files, accessed with the LVGL lock held */ size_t regularCharacterCount = 0; size_t monoCharacterCount = 0; bool applyFailed = false; std::atomic applying = false; Thread* applyThread = nullptr; lvgl::FontConfiguration applyConfiguration; std::atomic cacheUpdateSucceeded = false; uint32_t selectLaunchId = 0; /** The colour that the selection dialog is for */ size_t selectColorIndex = 0; uint32_t colorSelectLaunchId = 0; FontSlot selectSlot = FontSlot::Regular; AppStream selectStream {}; uint8_t selectBuffer[256] {}; // Valid while the window is shown lv_obj_t* applyButton = nullptr; lv_obj_t* spinner = nullptr; FontRowWidgets regularRow; FontRowWidgets monoRow; lv_obj_t* errorLabel = nullptr; lv_obj_t* lightChip = nullptr; lv_obj_t* darkChip = nullptr; lv_obj_t* regularThemeChip = nullptr; lv_obj_t* monoThemeChip = nullptr; ColorRowWidgets colorRows[COLOR_COUNT]; /** The display can only show the mono theme */ bool isMonoDisplay = false; }; lvgl::FontConfiguration toFontConfiguration(const settings::appearance::AppearanceSettings& settings) { return { .defaultSize = settings.fontSize != 0 ? settings.fontSize : static_cast(TT_FONT_DEFAULT_SIZE), .regularFontPath = settings.regularFontPath, .monoFontPath = settings.monoFontPath, }; } std::string& getFontPath(settings::appearance::AppearanceSettings& settings, FontSlot slot) { return slot == FontSlot::Regular ? settings.regularFontPath : settings.monoFontPath; } size_t& getCharacterCount(Context* ctx, FontSlot slot) { return slot == FontSlot::Regular ? ctx->regularCharacterCount : ctx->monoCharacterCount; } size_t getCharacterCount(const std::string& path) { return path.empty() ? 0 : lvgl::getTtfCharacterCount(path); } void setHidden(lv_obj_t* object, bool hidden) { if (hidden) { lv_obj_add_flag(object, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_remove_flag(object, LV_OBJ_FLAG_HIDDEN); } } void updateFontRow(const FontRowWidgets& row, const std::string& path, size_t characterCount) { lv_label_set_text(row.fileLabel, path.empty() ? "System font" : file::getLastPathSegment(path).c_str()); setHidden(row.defaultButton, path.empty()); const bool large = !path.empty() && characterCount > lvgl::LARGE_FONT_CHARACTER_COUNT; if (large) { lv_label_set_text(row.warningLabel, std::format("Contains {} characters: creating the fonts can take long and use a lot of memory.", characterCount).c_str()); } setHidden(row.warningLabel, !large); } bool isDeviceDefaultDark() { LvglThemeSettings defaults; lvgl_theme_get_default_settings(&defaults); return defaults.is_dark; } bool isDark(const settings::appearance::AppearanceSettings& settings) { switch (settings.themeMode) { case settings::appearance::ThemeMode::Light: return false; case settings::appearance::ThemeMode::Dark: return true; default: return isDeviceDefaultDark(); } } void setChecked(lv_obj_t* object, bool checked) { if (checked) { lv_obj_add_state(object, LV_STATE_CHECKED); } else { lv_obj_remove_state(object, LV_STATE_CHECKED); } } void updateColorRow(Context* ctx, size_t index); void updateThemeWidgets(Context* ctx) { const bool dark = isDark(ctx->pendingSettings); setChecked(ctx->lightChip, !dark); setChecked(ctx->darkChip, dark); if (ctx->regularThemeChip != nullptr) { setChecked(ctx->regularThemeChip, !ctx->pendingSettings.monoTheme); setChecked(ctx->monoThemeChip, ctx->pendingSettings.monoTheme); } // The mono theme doesn't use colours for (size_t i = 0; i < COLOR_COUNT; i++) { if (ctx->colorRows[i].row != nullptr) { setHidden(ctx->colorRows[i].row, ctx->pendingSettings.monoTheme); updateColorRow(ctx, i); } } } /** Updates the widgets to the pending settings. Requires the LVGL lock. */ void updateWidgets(Context* ctx) { if (ctx->applyButton == nullptr) { return; } const bool applying = ctx->applying; setHidden(ctx->applyButton, applying || ctx->pendingSettings == ctx->savedSettings); setHidden(ctx->spinner, !applying); updateFontRow(ctx->regularRow, ctx->pendingSettings.regularFontPath, ctx->regularCharacterCount); updateFontRow(ctx->monoRow, ctx->pendingSettings.monoFontPath, ctx->monoCharacterCount); setHidden(ctx->errorLabel, !ctx->applyFailed); updateThemeWidgets(ctx); } void setDark(Context* ctx, bool dark) { // The device default is kept when it matches, so it doesn't count as a change if (dark == isDeviceDefaultDark()) { ctx->pendingSettings.themeMode = settings::appearance::ThemeMode::DeviceDefault; } else { ctx->pendingSettings.themeMode = dark ? settings::appearance::ThemeMode::Dark : settings::appearance::ThemeMode::Light; } updateWidgets(ctx); } void onLightPressed(lv_event_t* event) { setDark(static_cast(lv_event_get_user_data(event)), false); } void onDarkPressed(lv_event_t* event) { setDark(static_cast(lv_event_get_user_data(event)), true); } void onRegularThemePressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); ctx->pendingSettings.monoTheme = false; updateWidgets(ctx); } void onMonoThemePressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); ctx->pendingSettings.monoTheme = true; updateWidgets(ctx); } void onAnimationsChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* checkbox = static_cast(lv_event_get_target(event)); ctx->pendingSettings.animationsEnabled = lv_obj_has_state(checkbox, LV_STATE_CHECKED); updateWidgets(ctx); } uint32_t getPaletteColor(lv_palette_t palette) { return lv_color_to_u32(lv_palette_main(palette)) & 0xFFFFFF; } constexpr size_t PALETTE_COLOR_COUNT = sizeof(PALETTE_COLORS) / sizeof(PALETTE_COLORS[0]); std::optional& getColor(settings::appearance::AppearanceSettings& settings, size_t index) { switch (index) { case 0: return settings.primaryColor; case 1: return settings.secondaryColor; default: return settings.errorColor; } } uint32_t getDefaultColor(size_t index) { LvglThemeSettings defaults; lvgl_theme_get_default_settings(&defaults); const lv_color_t colors[COLOR_COUNT] = { defaults.color_primary, defaults.color_secondary, defaults.color_error }; return lv_color_to_u32(colors[index]) & 0xFFFFFF; } void updateColorRow(Context* ctx, size_t index) { const auto& widgets = ctx->colorRows[index]; const auto& color = getColor(ctx->pendingSettings, index); lv_obj_set_style_bg_color(widgets.swatch, lv_color_hex(color.value_or(getDefaultColor(index))), LV_STATE_DEFAULT); } void onColorButtonPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* button = static_cast(lv_event_get_target(event)); ctx->selectColorIndex = reinterpret_cast(lv_obj_get_user_data(button)); task_event_group_signal(ctx->eventGroup, ctx->selectColorBit); } void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); app_event_emit_close(ctx->appInstanceId); } void onApplyPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); task_event_group_signal(ctx->eventGroup, ctx->applyBit); } void onFontSizeChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* spinbox = static_cast(lv_event_get_target(event)); const auto size = static_cast(lv_spinbox_get_value(spinbox)); // The device's default size is stored as "unset", so it follows the device default ctx->pendingSettings.fontSize = size == TT_FONT_DEFAULT_SIZE ? 0 : size; updateWidgets(ctx); } void onFontSizeDecrementPressed(lv_event_t* event) { lv_spinbox_decrement(static_cast(lv_event_get_user_data(event))); } void onFontSizeIncrementPressed(lv_event_t* event) { lv_spinbox_increment(static_cast(lv_event_get_user_data(event))); } lv_obj_t* createStepButton(lv_obj_t* parent, const char* symbol, lv_event_cb_t callback, lv_obj_t* spinbox) { auto* button = lv_button_create(parent); auto* label = lv_label_create(button); lv_label_set_text(label, symbol); lv_obj_center(label); lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, spinbox); lv_obj_add_event_cb(button, callback, LV_EVENT_LONG_PRESSED_REPEAT, spinbox); return button; } void onSelectRegularPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); task_event_group_signal(ctx->eventGroup, ctx->selectRegularBit); } void onSelectMonoPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); task_event_group_signal(ctx->eventGroup, ctx->selectMonoBit); } void onDefaultRegularPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); ctx->pendingSettings.regularFontPath.clear(); ctx->regularCharacterCount = 0; updateWidgets(ctx); } void onDefaultMonoPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); ctx->pendingSettings.monoFontPath.clear(); ctx->monoCharacterCount = 0; updateWidgets(ctx); } /** An unstyled row, so it doesn't paint over the card. Its items are spaced like the card's content. */ lv_obj_t* createRow(lv_obj_t* parent) { auto* row = lv_obj_create(parent); lv_obj_remove_style_all(row); lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(row, lv_obj_get_style_pad_column(parent, LV_PART_MAIN), LV_STATE_DEFAULT); // Room for the focus outline of the buttons, which is clipped to the row in compact mode lv_obj_set_style_pad_ver(row, 3, LV_STATE_DEFAULT); lv_obj_set_style_pad_hor(row, 3, LV_STATE_DEFAULT); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); return row; } /** A title above a card, and the card */ lv_obj_t* createSection(lv_obj_t* parent, const char* title) { auto* title_label = lv_label_create(parent); lv_label_set_text(title_label, title); auto* card = lvgl_card_create(parent); lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); return card; } lv_obj_t* createLabeledRow(lv_obj_t* parent, const char* text) { auto* row = createRow(parent); auto* label = lv_label_create(row); lv_label_set_text(label, text); lv_obj_set_flex_grow(label, 1); return row; } lv_obj_t* createChip(lv_obj_t* parent, const char* text, lv_event_cb_t callback, Context* ctx) { auto* chip = lvgl_chip_create(parent); auto* label = lv_label_create(chip); lv_label_set_text(label, text); lv_obj_add_event_cb(chip, callback, LV_EVENT_SHORT_CLICKED, ctx); return chip; } ColorRowWidgets createColorRow(lv_obj_t* parent, size_t index, Context* ctx) { ColorRowWidgets widgets; // "Title [swatch] [Change]" widgets.row = createLabeledRow(parent, COLOR_TITLES[index]); // The colour preview: its colour is the setting, its border uses the theme's text colour widgets.swatch = lv_obj_create(widgets.row); lv_obj_remove_style_all(widgets.swatch); lv_obj_remove_flag(widgets.swatch, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(widgets.swatch, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_style_bg_opa(widgets.swatch, LV_OPA_COVER, LV_STATE_DEFAULT); lv_obj_set_style_border_width(widgets.swatch, 1, LV_STATE_DEFAULT); lv_obj_set_style_border_color(widgets.swatch, lv_obj_get_style_text_color(widgets.swatch, LV_PART_MAIN), LV_STATE_DEFAULT); lv_obj_set_style_radius(widgets.swatch, LV_DPX(4), LV_STATE_DEFAULT); auto* button = lv_button_create(widgets.row); auto* button_label = lv_label_create(button); lv_label_set_text(button_label, "Change"); lv_obj_set_user_data(button, reinterpret_cast(static_cast(index))); lv_obj_add_event_cb(button, onColorButtonPressed, LV_EVENT_SHORT_CLICKED, ctx); // As high as the button lv_obj_update_layout(button); const int32_t swatch_size = lv_obj_get_height(button); lv_obj_set_size(widgets.swatch, swatch_size, swatch_size); return widgets; } void createThemeCard(lv_obj_t* parent, Context* ctx) { auto* card = createSection(parent, "Theme"); auto* mode_row = createLabeledRow(card, "Mode"); ctx->lightChip = createChip(mode_row, "Light", onLightPressed, ctx); ctx->darkChip = createChip(mode_row, "Dark", onDarkPressed, ctx); // Monochrome and greyscale displays always use the mono theme, and it doesn't use colours if (!ctx->isMonoDisplay) { auto* style_row = createLabeledRow(card, "Style"); ctx->regularThemeChip = createChip(style_row, "Regular", onRegularThemePressed, ctx); ctx->monoThemeChip = createChip(style_row, "Mono", onMonoThemePressed, ctx); } auto* animations_checkbox = lv_checkbox_create(card); lv_checkbox_set_text(animations_checkbox, "Animations"); setChecked(animations_checkbox, ctx->pendingSettings.animationsEnabled); lv_obj_add_event_cb(animations_checkbox, onAnimationsChanged, LV_EVENT_VALUE_CHANGED, ctx); if (!ctx->isMonoDisplay) { for (size_t i = 0; i < COLOR_COUNT; i++) { ctx->colorRows[i] = createColorRow(card, i, ctx); } } } lv_obj_t* createButton(lv_obj_t* parent, const char* text, lv_event_cb_t callback, Context* ctx) { auto* button = lv_button_create(parent); auto* label = lv_label_create(button); lv_label_set_text(label, text); lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, ctx); return button; } FontRowWidgets createFontRow(lv_obj_t* parent, const char* title, lv_event_cb_t onSelect, lv_event_cb_t onDefault, Context* ctx) { FontRowWidgets widgets; // "Title [Default]" auto* title_row = createRow(parent); auto* label = lv_label_create(title_row); lv_label_set_text(label, title); lv_obj_set_flex_grow(label, 1); lv_obj_set_style_pad_ver(label, 0, LV_STATE_DEFAULT); widgets.defaultButton = createButton(title_row, "Default", onDefault, ctx); // " file.ttf [Select]" auto* file_row = createRow(parent); widgets.fileLabel = lv_label_create(file_row); lv_obj_set_flex_grow(widgets.fileLabel, 1); lv_label_set_long_mode(widgets.fileLabel, LV_LABEL_LONG_MODE_DOTS); lv_obj_set_style_pad_left(widgets.fileLabel, 12, LV_STATE_DEFAULT); createButton(file_row, "Select", onSelect, ctx); widgets.warningLabel = lv_label_create(parent); lv_obj_set_width(widgets.warningLabel, LV_PCT(100)); lv_label_set_long_mode(widgets.warningLabel, LV_LABEL_LONG_MODE_WRAP); lv_obj_set_style_text_color(widgets.warningLabel, lv_palette_main(LV_PALETTE_ORANGE), LV_STATE_DEFAULT); return widgets; } void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto* toolbar = lvgl_toolbar_create(parent, "Appearance"); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); ctx->spinner = lvgl_toolbar_add_spinner_action(toolbar); ctx->applyButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_OK, onApplyPressed, ctx); auto* content = lv_obj_create(parent); lv_obj_set_width(content, LV_PCT(100)); lv_obj_set_flex_grow(content, 1); lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_border_width(content, 0, LV_STATE_DEFAULT); const auto color_format = lv_display_get_color_format(lv_obj_get_display(parent)); ctx->isMonoDisplay = color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8; createThemeCard(content, ctx); auto* fonts_card = createSection(content, "Fonts"); auto* font_size_row = createRow(fonts_card); auto* font_size_label = lv_label_create(font_size_row); lv_label_set_text(font_size_label, "Font size"); lv_obj_set_flex_grow(font_size_label, 1); // [-] [size] [+] auto* font_size_spinbox = lv_spinbox_create(font_size_row); lv_spinbox_set_range(font_size_spinbox, MIN_FONT_SIZE, MAX_FONT_SIZE); lv_spinbox_set_digit_format(font_size_spinbox, 2, 0); lv_spinbox_set_step(font_size_spinbox, 1); // Wide enough for the digits and the room that the text field keeps for its cursor, so it never scrolls the digits out of view. // The size is set before the value, because setting the value scrolls to the cursor using the current size. const lv_font_t* font_size_font = lv_obj_get_style_text_font(font_size_spinbox, LV_PART_MAIN); lv_point_t digits_size; lv_text_get_size(&digits_size, "00", font_size_font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE); const int32_t font_size_content_width = digits_size.x + lv_font_get_line_height(font_size_font); lv_obj_set_width(font_size_spinbox, font_size_content_width + lv_obj_get_style_pad_left(font_size_spinbox, LV_PART_MAIN) + lv_obj_get_style_pad_right(font_size_spinbox, LV_PART_MAIN) + 2 * lv_obj_get_style_border_width(font_size_spinbox, LV_PART_MAIN)); lv_obj_set_style_text_align(font_size_spinbox, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN); const uint16_t font_size = ctx->pendingSettings.fontSize != 0 ? ctx->pendingSettings.fontSize : static_cast(TT_FONT_DEFAULT_SIZE); lv_spinbox_set_value(font_size_spinbox, font_size); // The value is changed with the buttons, so the digit cursor isn't shown lv_obj_set_style_bg_opa(font_size_spinbox, LV_OPA_TRANSP, LV_PART_CURSOR); lv_obj_add_event_cb(font_size_spinbox, onFontSizeChanged, LV_EVENT_VALUE_CHANGED, ctx); auto* decrement_button = createStepButton(font_size_row, LV_SYMBOL_MINUS, onFontSizeDecrementPressed, font_size_spinbox); lv_obj_move_to_index(decrement_button, lv_obj_get_index(font_size_spinbox)); createStepButton(font_size_row, LV_SYMBOL_PLUS, onFontSizeIncrementPressed, font_size_spinbox); ctx->regularRow = createFontRow(fonts_card, "Regular font", onSelectRegularPressed, onDefaultRegularPressed, ctx); ctx->monoRow = createFontRow(fonts_card, "Mono font", onSelectMonoPressed, onDefaultMonoPressed, ctx); ctx->errorLabel = lv_label_create(fonts_card); lv_obj_set_width(ctx->errorLabel, LV_PCT(100)); lv_label_set_long_mode(ctx->errorLabel, LV_LABEL_LONG_MODE_WRAP); lv_label_set_text(ctx->errorLabel, "Failed to create the fonts. Check that the selected files are TrueType fonts."); lv_obj_set_style_text_color(ctx->errorLabel, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT); updateWidgets(ctx); } void destroyWidgets(void* userData) { auto* ctx = static_cast(userData); ctx->applyButton = nullptr; ctx->spinner = nullptr; ctx->regularRow = {}; ctx->monoRow = {}; ctx->errorLabel = nullptr; ctx->lightChip = nullptr; ctx->darkChip = nullptr; ctx->regularThemeChip = nullptr; ctx->monoThemeChip = nullptr; for (auto& row : ctx->colorRows) { row = {}; } } int32_t applyThreadMain(void* context) { auto* ctx = static_cast(context); ctx->cacheUpdateSucceeded = lvgl::updateFontCache(ctx->applyConfiguration); task_event_group_signal(ctx->eventGroup, ctx->cacheUpdatedBit); return 0; } /** Ends an apply without a font error, keeping the changes pending */ void cancelApply(Context* ctx) { lvgl_lock(); ctx->applying = false; updateWidgets(ctx); lvgl_unlock(); } void finishApply(Context* ctx); void startApply(Context* ctx) { if (ctx->applying || ctx->applyThread != nullptr) { return; } lvgl_lock(); ctx->applyConfiguration = toFontConfiguration(ctx->pendingSettings); const bool fonts_changed = ctx->applyConfiguration != toFontConfiguration(ctx->savedSettings); ctx->applying = true; ctx->applyFailed = false; updateWidgets(ctx); lvgl_unlock(); // Theme changes don't need new fonts if (!fonts_changed) { ctx->cacheUpdateSucceeded = true; finishApply(ctx); return; } ctx->applyThread = thread_alloc(); if (ctx->applyThread == nullptr) { LOG_E(TAG, "Failed to create thread"); cancelApply(ctx); return; } thread_set_name(ctx->applyThread, "font_cache"); thread_set_stack_size(ctx->applyThread, APPLY_THREAD_STACK_SIZE); thread_set_main_function(ctx->applyThread, applyThreadMain, ctx); if (thread_start(ctx->applyThread) != ERROR_NONE) { LOG_E(TAG, "Failed to start thread"); thread_free(ctx->applyThread); ctx->applyThread = nullptr; cancelApply(ctx); } } void joinApplyThread(Context* ctx) { if (ctx->applyThread != nullptr) { thread_join(ctx->applyThread, portMAX_DELAY, pdMS_TO_TICKS(10)); thread_free(ctx->applyThread); ctx->applyThread = nullptr; } } void finishApply(Context* ctx) { joinApplyThread(ctx); if (!ctx->cacheUpdateSucceeded) { LOG_E(TAG, "Failed to update the font cache"); lvgl_lock(); ctx->applying = false; ctx->applyFailed = true; updateWidgets(ctx); lvgl_unlock(); return; } lvgl_lock(); const auto applied_settings = ctx->pendingSettings; lvgl_unlock(); if (!settings::appearance::save(applied_settings)) { // Fonts that aren't saved would revert on the next boot: keep the changes pending for a retry LOG_E(TAG, "Failed to save settings"); cancelApply(ctx); return; } lvgl_lock(); ctx->savedSettings = applied_settings; ctx->applying = false; lvgl_unlock(); // The window is rebuilt from the saved settings when LVGL starts again LOG_I(TAG, "Restarting LVGL with the new settings"); lvgl::stop(); lvgl::configureTheme(applied_settings); lvgl::loadFonts(ctx->applyConfiguration); lvgl::start(); } void startFileSelection(Context* ctx, FontSlot slot) { if (ctx->selectLaunchId != 0 || ctx->applying) { return; } ctx->selectSlot = slot; ctx->selectLaunchId = fileselection::startForExistingFile(ctx->appInstanceId, ctx->selectStream, ctx->selectBuffer, sizeof(ctx->selectBuffer), ctx->eventGroup); } void startColorSelection(Context* ctx) { if (ctx->colorSelectLaunchId != 0 || ctx->applying) { return; } std::vector items; items.reserve(PALETTE_COLOR_COUNT + 1); items.emplace_back("Default"); for (const auto& named_color : PALETTE_COLORS) { items.emplace_back(named_color.name); } ctx->colorSelectLaunchId = selectiondialog::start(ctx->appInstanceId, COLOR_TITLES[ctx->selectColorIndex], items); } void onColorSelectionResult(Context* ctx, const AppEvent& event) { ctx->colorSelectLaunchId = 0; // Index 0 is "Default", the others are the palette colours. Other results mean that the dialog was dismissed. const int32_t selected = event.result.result; if (selected >= 0 && selected <= static_cast(PALETTE_COLOR_COUNT)) { lvgl_lock(); auto& color = getColor(ctx->pendingSettings, ctx->selectColorIndex); if (selected == 0) { color.reset(); } else { color = getPaletteColor(PALETTE_COLORS[selected - 1].palette); } updateWidgets(ctx); lvgl_unlock(); } app_manager_stop(event.result.launch_id); } void onFileSelectionResult(Context* ctx, const AppEvent& event) { ctx->selectLaunchId = 0; if (event.result.result == 0 /* Ok */) { char path[sizeof(ctx->selectBuffer)]; const size_t length = app_stream_read(&ctx->selectStream, path, sizeof(path)); app_stream_unsubscribe(&ctx->selectStream); if (length > 0) { const auto selected_path = std::string(path, length); const size_t character_count = getCharacterCount(selected_path); lvgl_lock(); getFontPath(ctx->pendingSettings, ctx->selectSlot) = selected_path; getCharacterCount(ctx, ctx->selectSlot) = character_count; updateWidgets(ctx); lvgl_unlock(); } } else { app_stream_unsubscribe(&ctx->selectStream); } app_manager_stop(event.result.launch_id); } int32_t appMain(int argc, char* argv[]) { Context ctx; ctx.appInstanceId = app_scheduler_current_app_id(); ctx.savedSettings = settings::appearance::loadOrGetDefault(); ctx.pendingSettings = ctx.savedSettings; ctx.regularCharacterCount = getCharacterCount(ctx.pendingSettings.regularFontPath); ctx.monoCharacterCount = getCharacterCount(ctx.pendingSettings.monoFontPath); TaskEventGroup event_group {}; task_event_group_construct(&event_group); ctx.eventGroup = &event_group; check(task_event_group_claim_bit(&event_group, &ctx.selectRegularBit) == ERROR_NONE); check(task_event_group_claim_bit(&event_group, &ctx.selectMonoBit) == ERROR_NONE); check(task_event_group_claim_bit(&event_group, &ctx.selectColorBit) == ERROR_NONE); check(task_event_group_claim_bit(&event_group, &ctx.applyBit) == ERROR_NONE); check(task_event_group_claim_bit(&event_group, &ctx.cacheUpdatedBit) == ERROR_NONE); AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create_ext(ctx.appInstanceId, createWidgets, destroyWidgets, &ctx); bool should_close = false; while (!should_close) { uint32_t flags = 0; task_event_group_wait_any(&event_group, &flags, portMAX_DELAY); if (flags & ctx.selectRegularBit) { startFileSelection(&ctx, FontSlot::Regular); } if (flags & ctx.selectMonoBit) { startFileSelection(&ctx, FontSlot::Mono); } if (flags & ctx.selectColorBit) { startColorSelection(&ctx); } if (flags & ctx.applyBit) { startApply(&ctx); } if (flags & ctx.cacheUpdatedBit) { finishApply(&ctx); } AppEvent event {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { if (event.type == APP_EVENT_CLOSE) { should_close = true; } else if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.selectLaunchId) { onFileSelectionResult(&ctx, event); } else if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.colorSelectLaunchId) { onColorSelectionResult(&ctx, event); } } } // The thread uses the context joinApplyThread(&ctx); 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.appearance", .name = "Appearance", .category = APP_CATEGORY_SETTINGS, .location = { APP_LOCATION_MEMORY, reinterpret_cast(appMain) }, .flags = 0, .stack = {} }; } // namespace tt::app::appearancesettings