mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
553 lines
22 KiB
C++
553 lines
22 KiB
C++
#include <lvgl/lvgl.h>
|
|
#include <tactility/check.h>
|
|
#include <tactility/device.h>
|
|
#include <tactility/drivers/backlight.h>
|
|
#include <tactility/drivers/display.h>
|
|
#include <tactility/drivers/imu.h>
|
|
#include <tactility/error.h>
|
|
#include <tactility/log.h>
|
|
|
|
#include <Tactility/Tactility.h>
|
|
#include <Tactility/app/selectiondialog/SelectionDialog.h>
|
|
#ifdef ESP_PLATFORM
|
|
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
|
#endif
|
|
#include <Tactility/service/autorotate/AutoRotateService.h>
|
|
#include <Tactility/settings/DisplaySettings.h>
|
|
|
|
#include <app/event.h>
|
|
#include <app/manager.h>
|
|
#include <app/manifest.h>
|
|
#include <app/scheduler.h>
|
|
|
|
#include <lvgl_window_manager/window_manager.h>
|
|
|
|
#include <lvgl/fonts.h>
|
|
#include <lvgl/grid_navigation.h>
|
|
#include <lvgl/icons/shared.h>
|
|
#include <lvgl/widgets/card.h>
|
|
#include <lvgl/widgets/chip.h>
|
|
#include <lvgl/widgets/sliderbox.h>
|
|
#include <lvgl/widgets/toolbar.h>
|
|
|
|
#include <lvgl.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#ifdef ESP_PLATFORM
|
|
#include <sdkconfig.h>
|
|
#endif
|
|
|
|
namespace tt::app::display {
|
|
|
|
extern const ::AppManifest manifest;
|
|
|
|
constexpr auto* TAG = "Display";
|
|
|
|
namespace {
|
|
|
|
struct TimeoutOption {
|
|
uint32_t ms;
|
|
const char* name;
|
|
};
|
|
|
|
constexpr TimeoutOption TIMEOUT_OPTIONS[] = {
|
|
{ 15000, "15 seconds" },
|
|
{ 30000, "30 seconds" },
|
|
{ 60000, "1 minute" },
|
|
{ 120000, "2 minutes" },
|
|
{ 300000, "5 minutes" },
|
|
{ 0, "Never" },
|
|
};
|
|
|
|
// The icon is an upright phone with its bar at the bottom, rotated clockwise (0.1 degree units) to the screen's shape.
|
|
// In settings::display::Orientation order: Landscape, Portrait, LandscapeFlipped, PortraitFlipped
|
|
constexpr int32_t ORIENTATION_ICON_ROTATIONS[] = { 2700, 0, 900, 1800 };
|
|
|
|
// In settings::display::ScreensaverType order
|
|
constexpr const char* SCREENSAVER_NAMES[] = { "None", "Bouncing Balls", "Mystify", "Matrix Rain", "StackChan" };
|
|
|
|
struct Context {
|
|
uint32_t appInstanceId;
|
|
settings::display::DisplaySettings displaySettings;
|
|
bool displaySettingsUpdated = false;
|
|
TaskEventGroup* eventGroup = nullptr;
|
|
uint32_t selectTimeoutBit = 0;
|
|
uint32_t selectScreensaverBit = 0;
|
|
uint32_t timeoutDialogId = 0;
|
|
uint32_t screensaverDialogId = 0;
|
|
// Widgets, valid while the window is shown
|
|
lv_obj_t* timeoutSwitch = nullptr;
|
|
// Shown when the auto screen off is enabled: the timeout and screensaver
|
|
lv_obj_t* timeoutCard = nullptr;
|
|
lv_obj_t* timeoutValueLabel = nullptr;
|
|
lv_obj_t* screensaverValueLabel = nullptr;
|
|
// Shown when auto-rotate is off or unavailable: the orientation label and chips
|
|
lv_obj_t* orientationGroup = nullptr;
|
|
lv_obj_t* orientationChips = nullptr;
|
|
lv_obj_t* autoRotateSwitch = 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<Context*>(lv_event_get_user_data(event));
|
|
app_event_emit_close(ctx->appInstanceId);
|
|
}
|
|
|
|
// The slider shows a percentage of the backlight's brightness range
|
|
int32_t brightnessToPercent(Device* backlight, int32_t brightness) {
|
|
const int32_t min = backlight_get_min_brightness(backlight);
|
|
const int32_t range = backlight_get_max_brightness(backlight) - min;
|
|
return range > 0 ? ((brightness - min) * 100 + range / 2) / range : 100;
|
|
}
|
|
|
|
int32_t percentToBrightness(Device* backlight, int32_t percent) {
|
|
const int32_t min = backlight_get_min_brightness(backlight);
|
|
const int32_t range = backlight_get_max_brightness(backlight) - min;
|
|
const int32_t brightness = min + (percent * range + 50) / 100;
|
|
// A brightness of 0 turns the backlight off, so 0% is the dimmest setting that is still on
|
|
return brightness < 1 ? 1 : brightness;
|
|
}
|
|
|
|
void onBacklightSliderEvent(lv_event_t* event) {
|
|
auto* slider_box = lv_event_get_current_target_obj(event);
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
auto* backlight = getBacklightDevice();
|
|
assert(backlight != nullptr);
|
|
|
|
const int32_t percent = lvgl_sliderbox_get_value(slider_box);
|
|
ctx->displaySettings.backlightDuty = static_cast<uint8_t>(percentToBrightness(backlight, percent));
|
|
ctx->displaySettingsUpdated = true;
|
|
backlight_set_brightness(backlight, ctx->displaySettings.backlightDuty);
|
|
device_put(backlight);
|
|
}
|
|
|
|
void forEachChild(lv_obj_t* parent, auto&& action) {
|
|
const uint32_t count = lv_obj_get_child_count(parent);
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
action(lv_obj_get_child(parent, static_cast<int32_t>(i)), i);
|
|
}
|
|
}
|
|
|
|
void setCheckedChip(lv_obj_t* chips, settings::display::Orientation orientation) {
|
|
forEachChild(chips, [orientation](lv_obj_t* chip, uint32_t index) {
|
|
lv_obj_set_state(chip, LV_STATE_CHECKED, index == static_cast<uint32_t>(orientation));
|
|
});
|
|
}
|
|
|
|
/** Shows or hides the manual orientation chips, depending on whether auto-rotate is on */
|
|
void updateOrientationVisibility(Context* ctx) {
|
|
const bool auto_rotate = ctx->autoRotateSwitch != nullptr && ctx->displaySettings.autoRotateEnabled;
|
|
lv_obj_set_hidden(ctx->orientationGroup, auto_rotate);
|
|
}
|
|
|
|
void onOrientationChipPressed(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
auto* chip = lv_event_get_target_obj(event);
|
|
const auto selected_index = lv_obj_get_index(chip);
|
|
LOG_I(TAG, "Selected %d", (int)selected_index);
|
|
auto selected_orientation = static_cast<settings::display::Orientation>(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));
|
|
setCheckedChip(ctx->orientationChips, selected_orientation);
|
|
}
|
|
}
|
|
|
|
void onAutoRotateSwitch(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
|
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
|
ctx->displaySettings.autoRotateEnabled = enabled;
|
|
ctx->displaySettingsUpdated = true;
|
|
updateOrientationVisibility(ctx);
|
|
}
|
|
|
|
/** Creates a transparent column in a card with a title, which centers the title and the content below it */
|
|
lv_obj_t* createCardGroup(lv_obj_t* card, const char* title) {
|
|
auto* group = lv_obj_create(card);
|
|
lv_obj_set_size(group, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(group, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(group, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_bg_opa(group, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_border_width(group, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_pad_all(group, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_scrollable(group, false);
|
|
|
|
auto* label = lv_label_create(group);
|
|
lv_label_set_text(label, title);
|
|
return group;
|
|
}
|
|
|
|
/**
|
|
* Creates a row of chips with a phone icon per orientation, in settings::display::Orientation order.
|
|
* The row is centered in its parent while the chips fit, and scrolls when they don't.
|
|
*/
|
|
lv_obj_t* createOrientationChips(lv_obj_t* parent, settings::display::Orientation selected, lv_event_cb_t onPressed, Context* ctx) {
|
|
auto* chips = lv_obj_create(parent);
|
|
lv_obj_set_size(chips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_max_width(chips, LV_PCT(100), LV_STATE_DEFAULT);
|
|
lv_obj_set_flex_flow(chips, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_scroll_dir(chips, LV_DIR_HOR);
|
|
lv_obj_set_style_bg_opa(chips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_border_width(chips, 0, LV_STATE_DEFAULT);
|
|
// The chips' margins leave room for their focus rings and space them apart
|
|
lv_obj_set_style_pad_all(chips, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_pad_column(chips, 0, LV_STATE_DEFAULT);
|
|
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
auto* chip = lvgl_chip_create(chips);
|
|
lv_obj_add_event_cb(chip, onPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
|
auto* icon = lv_label_create(chip);
|
|
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
|
lv_label_set_text(icon, LVGL_ICON_SHARED_PHONE_ANDROID);
|
|
lv_obj_set_style_transform_pivot_x(icon, LV_PCT(50), LV_STATE_DEFAULT);
|
|
lv_obj_set_style_transform_pivot_y(icon, LV_PCT(50), LV_STATE_DEFAULT);
|
|
lv_obj_set_style_transform_rotation(icon, ORIENTATION_ICON_ROTATIONS[i], LV_STATE_DEFAULT);
|
|
}
|
|
lvgl_grid_navigation_add(chips);
|
|
setCheckedChip(chips, selected);
|
|
return chips;
|
|
}
|
|
|
|
void onTimeoutSwitch(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
|
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
|
ctx->displaySettings.backlightTimeoutEnabled = enabled;
|
|
ctx->displaySettingsUpdated = true;
|
|
if (ctx->timeoutCard) {
|
|
lv_obj_set_hidden(ctx->timeoutCard, !enabled);
|
|
}
|
|
}
|
|
|
|
const char* getTimeoutName(uint32_t ms) {
|
|
for (const auto& option : TIMEOUT_OPTIONS) {
|
|
if (option.ms == ms) {
|
|
return option.name;
|
|
}
|
|
}
|
|
return "Custom";
|
|
}
|
|
|
|
const char* getScreensaverName(settings::display::ScreensaverType type) {
|
|
const auto index = static_cast<size_t>(type);
|
|
return index < std::size(SCREENSAVER_NAMES) ? SCREENSAVER_NAMES[index] : "";
|
|
}
|
|
|
|
/** Requires the LVGL lock */
|
|
void updateTimeoutLabels(Context* ctx) {
|
|
if (ctx->timeoutValueLabel != nullptr) {
|
|
lv_label_set_text(ctx->timeoutValueLabel, getTimeoutName(ctx->displaySettings.backlightTimeoutMs));
|
|
lv_label_set_text(ctx->screensaverValueLabel, getScreensaverName(ctx->displaySettings.screensaverType));
|
|
}
|
|
}
|
|
|
|
void onChangeTimeoutPressed(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
task_event_group_signal(ctx->eventGroup, ctx->selectTimeoutBit);
|
|
}
|
|
|
|
void onChangeScreensaverPressed(lv_event_t* event) {
|
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
task_event_group_signal(ctx->eventGroup, ctx->selectScreensaverBit);
|
|
}
|
|
|
|
void startTimeoutSelection(Context* ctx) {
|
|
if (ctx->timeoutDialogId != 0) {
|
|
return;
|
|
}
|
|
std::vector<std::string> items;
|
|
for (const auto& option : TIMEOUT_OPTIONS) {
|
|
items.emplace_back(option.name);
|
|
}
|
|
ctx->timeoutDialogId = selectiondialog::start(ctx->appInstanceId, "Timeout", items);
|
|
}
|
|
|
|
void startScreensaverSelection(Context* ctx) {
|
|
if (ctx->screensaverDialogId != 0) {
|
|
return;
|
|
}
|
|
const std::vector<std::string> items(std::begin(SCREENSAVER_NAMES), std::end(SCREENSAVER_NAMES));
|
|
ctx->screensaverDialogId = selectiondialog::start(ctx->appInstanceId, "Screensaver", items);
|
|
}
|
|
|
|
// Other results than an index mean that the dialog was dismissed
|
|
void onTimeoutSelectionResult(Context* ctx, int32_t selected) {
|
|
ctx->timeoutDialogId = 0;
|
|
if (selected >= 0 && selected < static_cast<int32_t>(std::size(TIMEOUT_OPTIONS))) {
|
|
ctx->displaySettings.backlightTimeoutMs = TIMEOUT_OPTIONS[selected].ms;
|
|
ctx->displaySettingsUpdated = true;
|
|
}
|
|
}
|
|
|
|
void onScreensaverSelectionResult(Context* ctx, int32_t selected) {
|
|
ctx->screensaverDialogId = 0;
|
|
if (selected >= 0 && selected < static_cast<int32_t>(settings::display::ScreensaverType::Count)) {
|
|
ctx->displaySettings.screensaverType = static_cast<settings::display::ScreensaverType>(selected);
|
|
ctx->displaySettingsUpdated = true;
|
|
}
|
|
}
|
|
|
|
/** A row in a card: "Title Value [Change]" */
|
|
lv_obj_t* createValueRow(lv_obj_t* card, const char* title, lv_event_cb_t onChange, Context* ctx) {
|
|
auto* row = lv_obj_create(card);
|
|
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_all(row, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
|
|
// The card provides the background
|
|
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
lv_obj_set_scrollable(row, false);
|
|
|
|
auto* title_label = lv_label_create(row);
|
|
lv_label_set_text(title_label, title);
|
|
lv_obj_set_flex_grow(title_label, 1);
|
|
|
|
auto* value_label = lv_label_create(row);
|
|
|
|
auto* button = lv_button_create(row);
|
|
auto* icon = lv_label_create(button);
|
|
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
|
lv_label_set_text(icon, LVGL_ICON_SHARED_LIST_ALT);
|
|
lv_obj_add_event_cb(button, onChange, LV_EVENT_SHORT_CLICKED, ctx);
|
|
return value_label;
|
|
}
|
|
|
|
void createWidgets(lv_obj_t* parent, void* userData) {
|
|
auto* ctx = static_cast<Context*>(userData);
|
|
|
|
bool has_imu = device_exists_of_type(&IMU_TYPE);
|
|
|
|
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_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
|
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_label = lv_label_create(main_wrapper);
|
|
lv_label_set_text(brightness_label, "Brightness");
|
|
|
|
auto* brightness_card = lvgl_card_create(main_wrapper);
|
|
lv_obj_set_size(brightness_card, LV_PCT(100), LV_SIZE_CONTENT);
|
|
|
|
const int32_t percent = brightnessToPercent(backlight, ctx->displaySettings.backlightDuty);
|
|
auto* brightness_slider = lvgl_sliderbox_create(brightness_card, 0, 100, 10, percent);
|
|
lv_obj_set_width(brightness_slider, LV_PCT(100));
|
|
lvgl_sliderbox_add_value_changed_cb(brightness_slider, onBacklightSliderEvent, ctx);
|
|
}
|
|
// Only compared against nullptr below, never dereferenced again, so releasing it here is safe.
|
|
device_put(backlight);
|
|
}
|
|
|
|
// Orientation
|
|
|
|
// Auto-rotate (IMU-driven), which decides what the card below shows
|
|
if (has_imu) {
|
|
auto* auto_rotate_row = lv_obj_create(main_wrapper);
|
|
lv_obj_set_size(auto_rotate_row, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(auto_rotate_row, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(auto_rotate_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_border_width(auto_rotate_row, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_pad_all(auto_rotate_row, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_scrollable(auto_rotate_row, false);
|
|
|
|
auto* auto_rotate_label = lv_label_create(auto_rotate_row);
|
|
lv_label_set_text(auto_rotate_label, "Auto-rotate");
|
|
lv_obj_set_flex_grow(auto_rotate_label, 1);
|
|
|
|
ctx->autoRotateSwitch = lv_switch_create(auto_rotate_row);
|
|
lv_obj_set_state(ctx->autoRotateSwitch, LV_STATE_CHECKED, ctx->displaySettings.autoRotateEnabled);
|
|
lv_obj_add_event_cb(ctx->autoRotateSwitch, onAutoRotateSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
|
}
|
|
|
|
// The card shows the manual orientation; auto-rotate hides it while it drives the rotation
|
|
auto* orientation_card = lvgl_card_create(main_wrapper);
|
|
lv_obj_set_size(orientation_card, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(orientation_card, LV_FLEX_FLOW_COLUMN);
|
|
|
|
ctx->orientationGroup = createCardGroup(orientation_card, "Orientation");
|
|
ctx->orientationChips = createOrientationChips(ctx->orientationGroup, ctx->displaySettings.orientation, onOrientationChipPressed, ctx);
|
|
|
|
updateOrientationVisibility(ctx);
|
|
|
|
// 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_flex_flow(timeout_wrapper, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(timeout_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_all(timeout_wrapper, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_border_width(timeout_wrapper, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_scrollable(timeout_wrapper, false);
|
|
|
|
auto* timeout_label = lv_label_create(timeout_wrapper);
|
|
lv_label_set_text(timeout_label, "Auto screen off");
|
|
lv_obj_set_flex_grow(timeout_label, 1);
|
|
|
|
ctx->timeoutSwitch = lv_switch_create(timeout_wrapper);
|
|
if (ctx->displaySettings.backlightTimeoutEnabled) {
|
|
lv_obj_add_state(ctx->timeoutSwitch, LV_STATE_CHECKED);
|
|
}
|
|
lv_obj_add_event_cb(ctx->timeoutSwitch, onTimeoutSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
|
|
|
auto* timeout_card = lvgl_card_create(main_wrapper);
|
|
lv_obj_set_size(timeout_card, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(timeout_card, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_hidden(timeout_card, !ctx->displaySettings.backlightTimeoutEnabled);
|
|
ctx->timeoutCard = timeout_card;
|
|
|
|
ctx->timeoutValueLabel = createValueRow(timeout_card, "Timeout", onChangeTimeoutPressed, ctx);
|
|
ctx->screensaverValueLabel = createValueRow(timeout_card, "Screensaver", onChangeScreensaverPressed, ctx);
|
|
updateTimeoutLabels(ctx);
|
|
}
|
|
}
|
|
|
|
void destroyWidgets(void* userData) {
|
|
auto* ctx = static_cast<Context*>(userData);
|
|
ctx->timeoutSwitch = nullptr;
|
|
ctx->timeoutCard = nullptr;
|
|
ctx->timeoutValueLabel = nullptr;
|
|
ctx->screensaverValueLabel = nullptr;
|
|
ctx->orientationGroup = nullptr;
|
|
ctx->orientationChips = nullptr;
|
|
ctx->autoRotateSwitch = nullptr;
|
|
}
|
|
|
|
// 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
|
|
// Notify AutoRotate service to reload settings
|
|
auto autoRotate = service::autorotate::findService();
|
|
if (autoRotate) {
|
|
autoRotate->reloadSettings();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
int32_t appMain(int argc, char* argv[]) {
|
|
uint32_t appInstanceId = app_scheduler_current_app_id();
|
|
Context ctx {};
|
|
ctx.appInstanceId = appInstanceId;
|
|
// Loaded once: the window is rebuilt from it, e.g. after a dialog closed
|
|
ctx.displaySettings = settings::display::loadOrGetDefault();
|
|
|
|
TaskEventGroup event_group {};
|
|
task_event_group_construct(&event_group);
|
|
ctx.eventGroup = &event_group;
|
|
check(task_event_group_claim_bit(&event_group, &ctx.selectTimeoutBit) == ERROR_NONE);
|
|
check(task_event_group_claim_bit(&event_group, &ctx.selectScreensaverBit) == ERROR_NONE);
|
|
|
|
AppEventSubscription sub {};
|
|
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
|
|
|
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
|
|
|
bool shouldClose = false;
|
|
while (!shouldClose) {
|
|
uint32_t flags = 0;
|
|
task_event_group_wait_any(&event_group, &flags, portMAX_DELAY);
|
|
|
|
if (flags & ctx.selectTimeoutBit) {
|
|
startTimeoutSelection(&ctx);
|
|
}
|
|
if (flags & ctx.selectScreensaverBit) {
|
|
startScreensaverSelection(&ctx);
|
|
}
|
|
|
|
AppEvent event {};
|
|
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
|
switch (event.type) {
|
|
case APP_EVENT_CLOSE:
|
|
persistIfUpdated(ctx);
|
|
shouldClose = true;
|
|
break;
|
|
case APP_EVENT_RESULT:
|
|
if (event.result.launch_id == ctx.timeoutDialogId) {
|
|
onTimeoutSelectionResult(&ctx, event.result.result);
|
|
} else if (event.result.launch_id == ctx.screensaverDialogId) {
|
|
onScreensaverSelectionResult(&ctx, event.result.result);
|
|
}
|
|
lvgl_lock();
|
|
updateTimeoutLabels(&ctx);
|
|
lvgl_unlock();
|
|
app_manager_stop(event.result.launch_id);
|
|
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<void*>(appMain) },
|
|
.flags = 0,
|
|
.stack = {}
|
|
};
|
|
|
|
} // namespace tt::app::display
|