diff --git a/Tactility/Include/Tactility/hal/display/DisplayDevice.h b/Tactility/Include/Tactility/hal/display/DisplayDevice.h index 64c243c79..7918bfb7e 100644 --- a/Tactility/Include/Tactility/hal/display/DisplayDevice.h +++ b/Tactility/Include/Tactility/hal/display/DisplayDevice.h @@ -29,6 +29,14 @@ class DisplayDevice : public Device { /** For e-paper screens */ virtual void requestFullRefresh() {} + /** Blocks until any frame already handed to this display has physically + * finished drawing. Displays that draw synchronously within their flush + * callback can rely on the default no-op; displays with an asynchronous + * refresh pipeline (e.g. e-paper, where a full refresh can take seconds) + * should override this so callers can safely do something irreversible + * (like cutting power) right after a screen update. */ + virtual void waitForFlushComplete() {} + /** Could return nullptr if not started */ virtual std::shared_ptr getTouchDevice() = 0; diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 8fbb966c5..b79f49887 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -103,6 +103,7 @@ namespace app { namespace localesettings { extern const AppManifest manifest; } namespace notes { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; } + namespace poweroff { extern const AppManifest manifest; } namespace selectiondialog { extern const AppManifest manifest; } namespace settings { extern const AppManifest manifest; } namespace setup { extern const AppManifest manifest; } @@ -157,6 +158,7 @@ static void registerInternalApps() { addAppManifest(app::launcher::manifest); addAppManifest(app::localesettings::manifest); addAppManifest(app::notes::manifest); + addAppManifest(app::poweroff::manifest); addAppManifest(app::settings::manifest); addAppManifest(app::selectiondialog::manifest); addAppManifest(app::setup::manifest); diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index a5ee66906..729376cd7 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -66,31 +65,11 @@ class LauncherApp final : public App { return apps_button; } - static bool shouldShowPowerButton() { - bool show_power_button = false; - hal::findDevices(hal::Device::Type::Power, [&show_power_button](const auto& device) { - if (device->supportsPowerOff()) { - show_power_button = true; - return false; // stop iterating - } else { - return true; // continue iterating - } - }); - return show_power_button; - } - static void onAppPressed(lv_event_t* e) { auto* appId = static_cast(lv_event_get_user_data(e)); start(appId); } - static void onPowerOffPressed(lv_event_t* e) { - auto power = hal::findFirstDevice(hal::Device::Type::Power); - if (power != nullptr && power->supportsPowerOff()) { - power->powerOff(); - } - } - // The screen object outlives the launcher's views (it's recreated by GuiService::redraw() // via lv_obj_clean() on every app switch), so the LV_EVENT_SIZE_CHANGED callback registered // on it must be removed once buttons_wrapper is destroyed, to avoid a dangling user-data @@ -201,19 +180,6 @@ class LauncherApp final : public App { // 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); - - if (shouldShowPowerButton()) { - auto* power_button = lv_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, onPowerOffPressed, LV_EVENT_SHORT_CLICKED, nullptr); - lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT); - lv_obj_set_style_bg_opa(power_button, 0, LV_PART_MAIN); - - auto* power_label = lv_label_create(power_button); - lv_label_set_text(power_label, LV_SYMBOL_POWER); - lv_obj_set_style_text_color(power_label, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT); - } } }; diff --git a/Tactility/Source/app/poweroff/PowerOff.cpp b/Tactility/Source/app/poweroff/PowerOff.cpp new file mode 100644 index 000000000..04e8c3c4d --- /dev/null +++ b/Tactility/Source/app/poweroff/PowerOff.cpp @@ -0,0 +1,104 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace tt::app::poweroff { + +extern const AppManifest manifest; + +class PowerOffApp final : public App { + + /** Replaces the screen with a plain "powered off" message, forces it to draw + * synchronously, and waits for the display to confirm the draw physically + * finished. On e-paper this is what's left showing once power cuts, so it + * doubles as visual confirmation that the device shut down cleanly rather + * than crashed or hung. Called from an LVGL button click callback, so it's + * already running on the LVGL thread. */ + static void showPoweredOffScreenAndWait(hal::display::DisplayDevice* display) { + auto* screen = lv_obj_create(nullptr); + lv_obj_set_style_bg_color(screen, lv_color_white(), 0); + lv_obj_set_flex_flow(screen, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(screen, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + auto* title = lv_label_create(screen); + lv_label_set_text(title, "Tactility OS"); + lv_obj_set_style_text_font(title, lvgl_get_text_font(FONT_SIZE_LARGE), 0); + lv_obj_set_style_text_color(title, lv_color_black(), 0); + + auto* subtitle = lv_label_create(screen); + lv_label_set_text(subtitle, "Powered off"); + lv_obj_set_style_text_color(subtitle, lv_color_black(), 0); + + lv_screen_load(screen); + + if (display != nullptr) { + auto* lvgl_display = display->getLvglDisplay(); + if (lvgl_display != nullptr) { + lv_refr_now(lvgl_display); + } + display->waitForFlushComplete(); + } + } + + static void onYesPressed(lv_event_t* /*event*/) { + auto power = hal::findFirstDevice(hal::Device::Type::Power); + if (power == nullptr || !power->supportsPowerOff()) { + return; + } + + auto display = hal::findFirstDevice(hal::Device::Type::Display); + showPoweredOffScreenAndWait(display.get()); + + power->powerOff(); + } + + static void onNoPressed(lv_event_t* /*event*/) { + stop(manifest.appId); + } + +public: + + void onShow(AppContext& /*app*/, lv_obj_t* parent) override { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + auto* label = lv_label_create(parent); + lv_label_set_text(label, "Power off?"); + lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_LARGE), 0); + + auto* button_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(button_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_size(button_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(button_wrapper, 0, 0); + lv_obj_set_style_border_width(button_wrapper, 0, 0); + lv_obj_set_flex_align(button_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + auto* yes_button = lv_button_create(button_wrapper); + auto* yes_label = lv_label_create(yes_button); + lv_label_set_text(yes_label, "Yes"); + lv_obj_add_event_cb(yes_button, onYesPressed, LV_EVENT_SHORT_CLICKED, nullptr); + + auto* no_button = lv_button_create(button_wrapper); + auto* no_label = lv_label_create(no_button); + lv_label_set_text(no_label, "No"); + lv_obj_add_event_cb(no_button, onNoPressed, LV_EVENT_SHORT_CLICKED, nullptr); + } +}; + +extern const AppManifest manifest = { + .appId = "PowerOff", + .appName = "Power Off", + .appIcon = LVGL_ICON_SHARED_POWER_SETTINGS_NEW, + .appCategory = Category::Settings, + .appFlags = AppManifest::Flags::HideStatusBar, + .createApp = create +}; + +} // namespace