#include #include #include #include #include #include #include #include #ifdef ESP_PLATFORM #include #endif #include #include #include #include #include #include #include #include #ifdef ESP_PLATFORM #include #endif namespace tt::app::display { extern const ::AppManifest manifest; constexpr auto* TAG = "Display"; namespace { struct Context { uint32_t appInstanceId; settings::display::DisplaySettings displaySettings; bool displaySettingsUpdated = false; lv_obj_t* timeoutSwitch = nullptr; lv_obj_t* timeoutDropdown = nullptr; lv_obj_t* screensaverDropdown = nullptr; lv_obj_t* nightDimSwitch = nullptr; lv_obj_t* nightDimStartDropdown = nullptr; lv_obj_t* nightDimEndDropdown = nullptr; lv_obj_t* nightDimDutySlider = nullptr; }; Device* getBacklightDevice() { Device* display; check(device_get_first_by_type(&DISPLAY_TYPE, &display) == ERROR_NONE); // Boards not yet migrated to the kernel display driver register a placeholder device (so the // devicetree node resolves) with a NULL api - nothing for display_get_backlight() to act on. if (device_get_driver(display)->api == nullptr) { device_put(display); return nullptr; } Device* backlight = nullptr; display_get_backlight(display, &backlight); device_put(display); return backlight; } void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); app_event_emit_close(ctx->appInstanceId); } void onBacklightSliderEvent(lv_event_t* event) { auto* slider = static_cast(lv_event_get_target(event)); auto* ctx = static_cast(lv_event_get_user_data(event)); auto* backlight = getBacklightDevice(); assert(backlight != nullptr); int32_t slider_value = lv_slider_get_value(slider); ctx->displaySettings.backlightDuty = static_cast(slider_value); ctx->displaySettingsUpdated = true; backlight_set_brightness(backlight, ctx->displaySettings.backlightDuty); device_put(backlight); } void onOrientationSet(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* dropdown = static_cast(lv_event_get_target(event)); uint32_t selected_index = lv_dropdown_get_selected(dropdown); LOG_I(TAG, "Selected %u", (unsigned)selected_index); auto selected_orientation = static_cast(selected_index); if (selected_orientation != ctx->displaySettings.orientation) { ctx->displaySettings.orientation = selected_orientation; ctx->displaySettingsUpdated = true; lv_display_set_rotation(lv_display_get_default(), settings::display::toLvglDisplayRotation(selected_orientation)); } } void onFontSizeChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* dropdown = static_cast(lv_event_get_target(event)); uint32_t selected_index = lv_dropdown_get_selected(dropdown); if (selected_index >= static_cast(settings::display::FontSize::Count)) { return; } auto selected_size = static_cast(selected_index); if (selected_size != ctx->displaySettings.fontSize) { ctx->displaySettings.fontSize = selected_size; ctx->displaySettingsUpdated = true; lvgl::applyFontSize(selected_size); } } void onTimeoutSwitch(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* sw = static_cast(lv_event_get_target(event)); bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED); ctx->displaySettings.backlightTimeoutEnabled = enabled; ctx->displaySettingsUpdated = true; if (ctx->timeoutDropdown) { if (enabled) { lv_obj_clear_state(ctx->timeoutDropdown, LV_STATE_DISABLED); if (ctx->screensaverDropdown) { lv_obj_clear_state(ctx->screensaverDropdown, LV_STATE_DISABLED); } } else { lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED); if (ctx->screensaverDropdown) { lv_obj_add_state(ctx->screensaverDropdown, LV_STATE_DISABLED); } } } } void onTimeoutChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* dropdown = static_cast(lv_event_get_target(event)); uint32_t idx = lv_dropdown_get_selected(dropdown); // Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=15m,6=30m,7=Never static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 900000, 1800000, 0}; if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) { ctx->displaySettings.backlightTimeoutMs = values_ms[idx]; ctx->displaySettingsUpdated = true; } } void onScreensaverChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* dropdown = static_cast(lv_event_get_target(event)); uint32_t idx = lv_dropdown_get_selected(dropdown); // Validate index bounds before casting to enum if (idx >= static_cast(settings::display::ScreensaverType::Count)) { return; } auto selected_type = static_cast(idx); if (selected_type != ctx->displaySettings.screensaverType) { ctx->displaySettings.screensaverType = selected_type; ctx->displaySettingsUpdated = true; } } static void setNightDimControlsEnabled(Context* ctx, bool enabled) { if (ctx->nightDimStartDropdown != nullptr) { if (enabled) { lv_obj_clear_state(ctx->nightDimStartDropdown, LV_STATE_DISABLED); } else { lv_obj_add_state(ctx->nightDimStartDropdown, LV_STATE_DISABLED); } } if (ctx->nightDimEndDropdown != nullptr) { if (enabled) { lv_obj_clear_state(ctx->nightDimEndDropdown, LV_STATE_DISABLED); } else { lv_obj_add_state(ctx->nightDimEndDropdown, LV_STATE_DISABLED); } } if (ctx->nightDimDutySlider != nullptr) { if (enabled) { lv_obj_clear_state(ctx->nightDimDutySlider, LV_STATE_DISABLED); } else { lv_obj_add_state(ctx->nightDimDutySlider, LV_STATE_DISABLED); } } } static const char* hourOptions() { // 00..23, one per line static const char* options = "00\n01\n02\n03\n04\n05\n06\n07\n08\n09\n10\n11\n" "12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23"; return options; } void onNightDimSwitch(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* sw = static_cast(lv_event_get_target(event)); bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED); ctx->displaySettings.nightDimEnabled = enabled; ctx->displaySettingsUpdated = true; setNightDimControlsEnabled(ctx, enabled); } void onNightDimStartChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* dropdown = static_cast(lv_event_get_target(event)); uint32_t idx = lv_dropdown_get_selected(dropdown); if (idx > 23) { return; } ctx->displaySettings.nightDimStartHour = static_cast(idx); ctx->displaySettingsUpdated = true; } void onNightDimEndChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* dropdown = static_cast(lv_event_get_target(event)); uint32_t idx = lv_dropdown_get_selected(dropdown); if (idx > 23) { return; } ctx->displaySettings.nightDimEndHour = static_cast(idx); ctx->displaySettingsUpdated = true; } void onNightDimDutyChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* slider = static_cast(lv_event_get_target(event)); int32_t value = lv_slider_get_value(slider); if (value < 0) { value = 0; } else if (value > 255) { value = 255; } ctx->displaySettings.nightDimDuty = static_cast(value); ctx->displaySettingsUpdated = true; } void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); ctx->displaySettings = settings::display::loadOrGetDefault(); auto ui_density = lvgl_get_ui_density(); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); auto* backlight = getBacklightDevice(); auto* toolbar = lvgl_toolbar_create(parent, "Display"); // The global toolbar nav callback only knows how to stop old-model apps. lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); // Backlight slider // Note: no gamma slider here - unlike HalDisplayApp (app/display/Display.cpp), the kernel // DisplayApi has no gamma curve control yet. if (backlight != nullptr) { bool is_on_off_brightness = backlight_get_min_brightness(backlight) == 0 && backlight_get_max_brightness(backlight) == 1; if (!is_on_off_brightness) { auto* brightness_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(brightness_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_hor(brightness_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(brightness_wrapper, 0, LV_STATE_DEFAULT); if (ui_density != LVGL_UI_DENSITY_COMPACT) { lv_obj_set_style_pad_ver(brightness_wrapper, 4, LV_STATE_DEFAULT); } auto* brightness_label = lv_label_create(brightness_wrapper); lv_label_set_text(brightness_label, "Brightness"); lv_obj_align(brightness_label, LV_ALIGN_LEFT_MID, 0, 0); auto* brightness_slider = lv_slider_create(brightness_wrapper); lv_obj_set_width(brightness_slider, LV_PCT(50)); lv_obj_align(brightness_slider, LV_ALIGN_RIGHT_MID, 0, 0); lv_slider_set_range(brightness_slider, backlight_get_min_brightness(backlight), backlight_get_max_brightness(backlight)); lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, ctx); lv_slider_set_value(brightness_slider, ctx->displaySettings.backlightDuty, LV_ANIM_OFF); } // Only compared against nullptr below, never dereferenced again, so releasing it here is safe. device_put(backlight); } // Orientation auto* orientation_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(orientation_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(orientation_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(orientation_wrapper, 0, LV_STATE_DEFAULT); auto* orientation_label = lv_label_create(orientation_wrapper); lv_label_set_text(orientation_label, "Orientation"); lv_obj_align(orientation_label, LV_ALIGN_LEFT_MID, 0, 0); auto* orientation_dropdown = lv_dropdown_create(orientation_wrapper); // Note: order correlates with settings::display::Orientation item order lv_dropdown_set_options(orientation_dropdown, "Landscape\nPortrait Right\nLandscape Flipped\nPortrait Left"); lv_obj_align(orientation_dropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, ctx); // Set the dropdown to match current orientation enum lv_dropdown_set_selected(orientation_dropdown, static_cast(ctx->displaySettings.orientation)); // Font size auto* font_size_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(font_size_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(font_size_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(font_size_wrapper, 0, LV_STATE_DEFAULT); auto* font_size_label = lv_label_create(font_size_wrapper); lv_label_set_text(font_size_label, "Font size"); lv_obj_align(font_size_label, LV_ALIGN_LEFT_MID, 0, 0); auto* font_size_dropdown = lv_dropdown_create(font_size_wrapper); lv_dropdown_set_options(font_size_dropdown, "Small\nDefault\nLarge"); lv_obj_align(font_size_dropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(font_size_dropdown, onFontSizeChanged, LV_EVENT_VALUE_CHANGED, ctx); lv_dropdown_set_selected(font_size_dropdown, static_cast(ctx->displaySettings.fontSize)); // Screen timeout // Note: DisplayIdleService doesn't act on these settings for kernel-driver displays yet // (it only looks up the deprecated tt::hal::display::DisplayDevice), so these currently // just get saved without taking effect. Kept for parity/forward-compatibility. if (backlight != nullptr) { auto* timeout_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(timeout_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(timeout_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(timeout_wrapper, 0, LV_STATE_DEFAULT); auto* timeout_label = lv_label_create(timeout_wrapper); lv_label_set_text(timeout_label, "Auto screen off"); lv_obj_align(timeout_label, LV_ALIGN_LEFT_MID, 0, 0); ctx->timeoutSwitch = lv_switch_create(timeout_wrapper); if (ctx->displaySettings.backlightTimeoutEnabled) { lv_obj_add_state(ctx->timeoutSwitch, LV_STATE_CHECKED); } lv_obj_align(ctx->timeoutSwitch, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->timeoutSwitch, onTimeoutSwitch, LV_EVENT_VALUE_CHANGED, ctx); auto* timeout_select_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(timeout_select_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(timeout_select_wrapper, 0, LV_STATE_DEFAULT); auto* timeout_value_label = lv_label_create(timeout_select_wrapper); lv_label_set_text(timeout_value_label, "Timeout"); lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0); ctx->timeoutDropdown = lv_dropdown_create(timeout_select_wrapper); lv_dropdown_set_options(ctx->timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\n15 minutes\n30 minutes\nNever"); lv_obj_align(ctx->timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, ctx); // Initialize dropdown selection from settings uint32_t ms = ctx->displaySettings.backlightTimeoutMs; uint32_t idx = 2; // default 1 minute if (ms == 15000) idx = 0; else if (ms == 30000) idx = 1; else if (ms == 60000) idx = 2; else if (ms == 120000) idx = 3; else if (ms == 300000) idx = 4; else if (ms == 900000) idx = 5; else if (ms == 1800000) idx = 6; else if (ms == 0) idx = 7; lv_dropdown_set_selected(ctx->timeoutDropdown, idx); if (!ctx->displaySettings.backlightTimeoutEnabled) { lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED); } // Screensaver type auto* screensaver_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(screensaver_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(screensaver_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(screensaver_wrapper, 0, LV_STATE_DEFAULT); auto* screensaver_label = lv_label_create(screensaver_wrapper); lv_label_set_text(screensaver_label, "Screensaver"); lv_obj_align(screensaver_label, LV_ALIGN_LEFT_MID, 0, 0); ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper); // Note: order correlates with settings::display::ScreensaverType enum order lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan\nMCP Screen\nBible Verse"); lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx); lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast(ctx->displaySettings.screensaverType)); if (!ctx->displaySettings.backlightTimeoutEnabled) { lv_obj_add_state(ctx->screensaverDropdown, LV_STATE_DISABLED); } // Night dim: dim the backlight while the screensaver is up, during a time window auto* night_dim_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(night_dim_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(night_dim_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(night_dim_wrapper, 0, LV_STATE_DEFAULT); auto* night_dim_label = lv_label_create(night_dim_wrapper); lv_label_set_text(night_dim_label, "Night dim"); lv_obj_align(night_dim_label, LV_ALIGN_LEFT_MID, 0, 0); ctx->nightDimSwitch = lv_switch_create(night_dim_wrapper); if (ctx->displaySettings.nightDimEnabled) { lv_obj_add_state(ctx->nightDimSwitch, LV_STATE_CHECKED); } lv_obj_align(ctx->nightDimSwitch, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->nightDimSwitch, onNightDimSwitch, LV_EVENT_VALUE_CHANGED, ctx); auto* night_dim_start_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(night_dim_start_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(night_dim_start_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(night_dim_start_wrapper, 0, LV_STATE_DEFAULT); auto* night_dim_start_label = lv_label_create(night_dim_start_wrapper); lv_label_set_text(night_dim_start_label, "Dim from"); lv_obj_align(night_dim_start_label, LV_ALIGN_LEFT_MID, 0, 0); ctx->nightDimStartDropdown = lv_dropdown_create(night_dim_start_wrapper); lv_dropdown_set_options(ctx->nightDimStartDropdown, hourOptions()); lv_obj_align(ctx->nightDimStartDropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->nightDimStartDropdown, onNightDimStartChanged, LV_EVENT_VALUE_CHANGED, ctx); lv_dropdown_set_selected(ctx->nightDimStartDropdown, ctx->displaySettings.nightDimStartHour > 23 ? 0 : ctx->displaySettings.nightDimStartHour); auto* night_dim_end_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(night_dim_end_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(night_dim_end_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(night_dim_end_wrapper, 0, LV_STATE_DEFAULT); auto* night_dim_end_label = lv_label_create(night_dim_end_wrapper); lv_label_set_text(night_dim_end_label, "Dim until"); lv_obj_align(night_dim_end_label, LV_ALIGN_LEFT_MID, 0, 0); ctx->nightDimEndDropdown = lv_dropdown_create(night_dim_end_wrapper); lv_dropdown_set_options(ctx->nightDimEndDropdown, hourOptions()); lv_obj_align(ctx->nightDimEndDropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->nightDimEndDropdown, onNightDimEndChanged, LV_EVENT_VALUE_CHANGED, ctx); lv_dropdown_set_selected(ctx->nightDimEndDropdown, ctx->displaySettings.nightDimEndHour > 23 ? 0 : ctx->displaySettings.nightDimEndHour); auto* night_dim_duty_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(night_dim_duty_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(night_dim_duty_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(night_dim_duty_wrapper, 0, LV_STATE_DEFAULT); auto* night_dim_duty_label = lv_label_create(night_dim_duty_wrapper); lv_label_set_text(night_dim_duty_label, "Dim brightness"); lv_obj_align(night_dim_duty_label, LV_ALIGN_LEFT_MID, 0, 0); ctx->nightDimDutySlider = lv_slider_create(night_dim_duty_wrapper); lv_obj_set_width(ctx->nightDimDutySlider, LV_PCT(50)); lv_obj_align(ctx->nightDimDutySlider, LV_ALIGN_RIGHT_MID, 0, 0); lv_slider_set_range(ctx->nightDimDutySlider, 0, 255); lv_slider_set_value(ctx->nightDimDutySlider, ctx->displaySettings.nightDimDuty, LV_ANIM_OFF); lv_obj_add_event_cb(ctx->nightDimDutySlider, onNightDimDutyChanged, LV_EVENT_VALUE_CHANGED, ctx); setNightDimControlsEnabled(ctx, ctx->displaySettings.nightDimEnabled); } } // Mirrors the old onHide() behaviour: persist the settings (regardless of whether the app is // giving up its thread for a save/resume cycle, or closing for good) whenever they changed. void persistIfUpdated(Context& ctx) { if (ctx.displaySettingsUpdated) { // Dispatch it, so file IO doesn't block the UI const settings::display::DisplaySettings settings_to_save = ctx.displaySettings; getMainDispatcher().dispatch([settings_to_save] { settings::display::save(settings_to_save); #ifdef ESP_PLATFORM // Notify DisplayIdle service to reload settings auto displayIdle = service::displayidle::findService(); if (displayIdle) { displayIdle->reloadSettings(); } #endif }); } } int32_t appMain(int argc, char* argv[]) { uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; 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, &ctx); bool shouldClose = false; while (!shouldClose) { task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); AppEvent event {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { switch (event.type) { case APP_EVENT_CLOSE: persistIfUpdated(ctx); shouldClose = true; break; default: 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.display", .name = "Display", .category = APP_CATEGORY_SETTINGS, .location = { APP_LOCATION_MEMORY, reinterpret_cast(appMain) } }; } // namespace tt::app::display