Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Tactility/Include/Tactility/hal/display/DisplayDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<touch::TouchDevice> getTouchDevice() = 0;

Expand Down
2 changes: 2 additions & 0 deletions Tactility/Source/Tactility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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);
Expand Down
21 changes: 8 additions & 13 deletions Tactility/Source/app/launcher/Launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ class LauncherApp final : public App {
return apps_button;
}

static void onAppPressed(lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
start(appId);
}

static bool shouldShowPowerButton() {
bool show_power_button = false;
hal::findDevices<hal::power::PowerDevice>(hal::Device::Type::Power, [&show_power_button](const auto& device) {
Expand All @@ -79,18 +84,6 @@ class LauncherApp final : public App {
return show_power_button;
}

static void onAppPressed(lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
start(appId);
}

static void onPowerOffPressed(lv_event_t* e) {
auto power = hal::findFirstDevice<hal::power::PowerDevice>(hal::Device::Type::Power);
if (power != nullptr && power->supportsPowerOff()) {
power->powerOff();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not link to the PowerOff app from here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app looks fine by the way!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 944689e — the button now just calls start("PowerOff") via the same handler the other launcher buttons use, so the confirmation and shutdown-screen flow lives only in the app. Re-tested on my T-Deck Max: launcher button opens the confirmation, power-off leaves the "Powered off" screen on the e-paper.

}
}

// 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
Expand Down Expand Up @@ -202,11 +195,13 @@ class LauncherApp final : public App {
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);

// Some devices (e.g. T-Lora Pager) have no other way to power off, so the
// button stays in the launcher; the confirmation flow lives in the PowerOff app.
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_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)"PowerOff");
lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(power_button, 0, LV_PART_MAIN);

Expand Down
104 changes: 104 additions & 0 deletions Tactility/Source/app/poweroff/PowerOff.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/power/PowerDevice.h>
#include <Tactility/service/loader/Loader.h>

#include <lvgl.h>
#include <tactility/hal/Device.h>
#include <tactility/lvgl_fonts.h>
#include <tactility/lvgl_icon_shared.h>

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::power::PowerDevice>(hal::Device::Type::Power);
if (power == nullptr || !power->supportsPowerOff()) {
return;
}

auto display = hal::findFirstDevice<hal::display::DisplayDevice>(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<PowerOffApp>
};

} // namespace
Loading