From 85da419a9855daf7e2899beb05bbda021c99173e Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Sat, 12 Sep 2026 08:17:45 -0400 Subject: [PATCH] feat(display): night-dim backlight window for screensaver - DisplaySettings: nightDimEnabled/startHour/endHour/duty (default 22:00-07:00 at duty 20), persisted in display.properties - Display settings page: Night dim switch + Dim from/until hour dropdowns + Dim brightness slider - DisplayIdle: screensaver runs at the dimmed level when the local hour is inside the window (wrap-around aware); falls back to the regular duty when time is unknown (year < 2025) --- .../Tactility/settings/DisplaySettings.h | 8 + Tactility/Source/app/display/Display.cpp | 145 ++++++++++++++++++ .../service/displayidle/DisplayIdle.cpp | 32 +++- Tactility/Source/settings/DisplaySettings.cpp | 42 ++++- 4 files changed, 225 insertions(+), 2 deletions(-) diff --git a/Tactility/Include/Tactility/settings/DisplaySettings.h b/Tactility/Include/Tactility/settings/DisplaySettings.h index d86d2cba..7fc118f8 100644 --- a/Tactility/Include/Tactility/settings/DisplaySettings.h +++ b/Tactility/Include/Tactility/settings/DisplaySettings.h @@ -37,6 +37,14 @@ struct DisplaySettings { bool backlightTimeoutEnabled; uint32_t backlightTimeoutMs; // 0 = Never ScreensaverType screensaverType = ScreensaverType::BouncingBalls; + // Night dim: when enabled and the local hour falls inside + // [nightDimStartHour, nightDimEndHour), the screensaver runs at + // nightDimDuty instead of backlightDuty. A start == end window + // means the whole day. + bool nightDimEnabled = false; + uint8_t nightDimStartHour = 22; + uint8_t nightDimEndHour = 7; + uint8_t nightDimDuty = 20; }; /** Compares default settings with the function parameter to return the difference */ diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index f4e09861..dbe5b239 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -42,6 +42,10 @@ struct Context { 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; }; @@ -154,6 +158,82 @@ void onScreensaverChanged(lv_event_t* event) { } } +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); @@ -314,6 +394,71 @@ void createWidgets(lv_obj_t* parent, void* userData) { 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); } } diff --git a/Tactility/Source/service/displayidle/DisplayIdle.cpp b/Tactility/Source/service/displayidle/DisplayIdle.cpp index 6407c360..6938d762 100644 --- a/Tactility/Source/service/displayidle/DisplayIdle.cpp +++ b/Tactility/Source/service/displayidle/DisplayIdle.cpp @@ -61,6 +61,34 @@ static bool hasDisplayWithBacklight() { return result; } +/** + * Effective backlight duty for the screensaver: the dimmed night level when + * night dim is enabled and the local hour is inside the configured window, + * otherwise the regular duty. A start == end window covers the whole day. + */ +static uint8_t screensaverBacklightDuty(const settings::display::DisplaySettings& displaySettings) { + if (!displaySettings.nightDimEnabled) { + return displaySettings.backlightDuty; + } + const std::time_t now = std::time(nullptr); + std::tm local_time {}; + if (localtime_r(&now, &local_time) == nullptr || local_time.tm_year < 125) { + return displaySettings.backlightDuty; + } + const uint8_t hour = static_cast(local_time.tm_hour); + const uint8_t start = displaySettings.nightDimStartHour; + const uint8_t end = displaySettings.nightDimEndHour; + bool in_window; + if (start == end) { + in_window = true; + } else if (start < end) { + in_window = hour >= start && hour < end; + } else { + in_window = hour >= start || hour < end; + } + return in_window ? displaySettings.nightDimDuty : displaySettings.backlightDuty; +} + void DisplayIdleService::stopScreensaver() { if (!lvgl_try_lock(100)) { // Lock failed - keep flag set to retry on next tick @@ -142,6 +170,8 @@ void DisplayIdleService::activateScreensaver() { if (screensaver) { screensaver->start(screensaverOverlay, screenW, screenH); } + + setBacklightBrightness(screensaverBacklightDuty(cachedDisplaySettings)); } void DisplayIdleService::updateScreensaver() { @@ -301,7 +331,7 @@ void DisplayIdleService::startMcpScreensaver() { screensaverActiveCounter = 0; backlightOff = false; setBacklightBrightness(cachedDisplaySettings.backlightDuty == 0 - ? 255 : cachedDisplaySettings.backlightDuty); + ? 255 : screensaverBacklightDuty(cachedDisplaySettings)); lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr); lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr); diff --git a/Tactility/Source/settings/DisplaySettings.cpp b/Tactility/Source/settings/DisplaySettings.cpp index a8208a3e..ac84cd06 100644 --- a/Tactility/Source/settings/DisplaySettings.cpp +++ b/Tactility/Source/settings/DisplaySettings.cpp @@ -26,6 +26,10 @@ constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty"; constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled"; constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs"; constexpr auto* SETTINGS_KEY_SCREENSAVER_TYPE = "screensaverType"; +constexpr auto* SETTINGS_KEY_NIGHT_DIM_ENABLED = "nightDimEnabled"; +constexpr auto* SETTINGS_KEY_NIGHT_DIM_START_HOUR = "nightDimStartHour"; +constexpr auto* SETTINGS_KEY_NIGHT_DIM_END_HOUR = "nightDimEndHour"; +constexpr auto* SETTINGS_KEY_NIGHT_DIM_DUTY = "nightDimDuty"; static Orientation getDefaultOrientation() { auto* display = lv_display_get_default(); @@ -202,6 +206,30 @@ bool load(DisplaySettings& settings) { fromString(screensaver_entry->second, screensaver_type); } + auto night_dim_enabled_entry = map.find(SETTINGS_KEY_NIGHT_DIM_ENABLED); + bool night_dim_enabled = false; + if (night_dim_enabled_entry != map.end()) { + night_dim_enabled = (night_dim_enabled_entry->second == "1" || night_dim_enabled_entry->second == "true" || night_dim_enabled_entry->second == "True"); + } + + auto night_dim_start_entry = map.find(SETTINGS_KEY_NIGHT_DIM_START_HOUR); + int night_dim_start_hour = 22; + if (night_dim_start_entry != map.end()) { + night_dim_start_hour = atoi(night_dim_start_entry->second.c_str()); + } + + auto night_dim_end_entry = map.find(SETTINGS_KEY_NIGHT_DIM_END_HOUR); + int night_dim_end_hour = 7; + if (night_dim_end_entry != map.end()) { + night_dim_end_hour = atoi(night_dim_end_entry->second.c_str()); + } + + auto night_dim_duty_entry = map.find(SETTINGS_KEY_NIGHT_DIM_DUTY); + int night_dim_duty = 20; + if (night_dim_duty_entry != map.end()) { + night_dim_duty = atoi(night_dim_duty_entry->second.c_str()); + } + settings.orientation = orientation; settings.fontSize = font_size; settings.gammaCurve = gamma_curve; @@ -209,6 +237,10 @@ bool load(DisplaySettings& settings) { settings.backlightTimeoutEnabled = timeout_enabled; settings.backlightTimeoutMs = timeout_ms; settings.screensaverType = screensaver_type; + settings.nightDimEnabled = night_dim_enabled; + settings.nightDimStartHour = static_cast(night_dim_start_hour < 0 ? 0 : (night_dim_start_hour > 23 ? 23 : night_dim_start_hour)); + settings.nightDimEndHour = static_cast(night_dim_end_hour < 0 ? 0 : (night_dim_end_hour > 23 ? 23 : night_dim_end_hour)); + settings.nightDimDuty = static_cast(night_dim_duty < 0 ? 0 : (night_dim_duty > 255 ? 255 : night_dim_duty)); return true; } @@ -221,7 +253,11 @@ DisplaySettings getDefault() { .backlightDuty = 200, .backlightTimeoutEnabled = false, .backlightTimeoutMs = 60000, - .screensaverType = ScreensaverType::BouncingBalls + .screensaverType = ScreensaverType::BouncingBalls, + .nightDimEnabled = false, + .nightDimStartHour = 22, + .nightDimEndHour = 7, + .nightDimDuty = 20 }; } @@ -242,6 +278,10 @@ bool save(const DisplaySettings& settings) { map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0"; map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs); map[SETTINGS_KEY_SCREENSAVER_TYPE] = toString(settings.screensaverType); + map[SETTINGS_KEY_NIGHT_DIM_ENABLED] = settings.nightDimEnabled ? "1" : "0"; + map[SETTINGS_KEY_NIGHT_DIM_START_HOUR] = std::to_string(settings.nightDimStartHour); + map[SETTINGS_KEY_NIGHT_DIM_END_HOUR] = std::to_string(settings.nightDimEndHour); + map[SETTINGS_KEY_NIGHT_DIM_DUTY] = std::to_string(settings.nightDimDuty); auto settings_path = getSettingsFilePath(); if (!file::findOrCreateParentDirectory(settings_path, 0755)) { return false;