diff --git a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp index e7bda6d..eed5a74 100644 --- a/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp +++ b/Firmware/sdk/apps/lockscreen/src/lockscreen_app.cpp @@ -17,7 +17,8 @@ namespace { using cardboy::sdk::AppContext; -constexpr std::uint32_t kRefreshIntervalMs = 500; +constexpr std::uint32_t kRefreshIntervalMs = 100; +constexpr std::uint32_t kUnlockHoldMs = 1500; using Framebuffer = typename AppContext::Framebuffer; using Clock = typename AppContext::Clock; @@ -40,8 +41,10 @@ public: void onStart() override { cancelRefreshTimer(); - lastSnapshot = {}; - dirty = true; + lastSnapshot = {}; + holdActive = false; + holdProgressMs = 0; + dirty = true; const auto snap = captureTime(); renderIfNeeded(snap); lastSnapshot = snap; @@ -53,12 +56,13 @@ public: void handleEvent(const cardboy::sdk::AppEvent& event) override { switch (event.type) { case cardboy::sdk::AppEventType::Button: - if (anyNewPress(event.button)) - context.requestAppSwitchByName(kMenuAppName); + handleButtonEvent(event.button); break; case cardboy::sdk::AppEventType::Timer: - if (event.timer.handle == refreshTimer) + if (event.timer.handle == refreshTimer) { + advanceHoldProgress(); updateDisplay(); + } break; } } @@ -71,6 +75,8 @@ private: bool dirty = false; cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer; TimeSnapshot lastSnapshot{}; + bool holdActive = false; + std::uint32_t holdProgressMs = 0; void cancelRefreshTimer() { if (refreshTimer != cardboy::sdk::kInvalidAppTimer) { @@ -79,12 +85,45 @@ private: } } - static bool anyNewPress(const cardboy::sdk::AppButtonEvent& button) { - const auto& current = button.current; - const auto& previous = button.previous; - return (current.a && !previous.a) || (current.b && !previous.b) || (current.start && !previous.start) || - (current.select && !previous.select) || (current.up && !previous.up) || (current.down && !previous.down) || - (current.left && !previous.left) || (current.right && !previous.right); + static bool comboPressed(const cardboy::sdk::InputState& state) { return state.a && state.select; } + + void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) { + const bool comboNow = comboPressed(button.current); + updateHoldState(comboNow); + updateDisplay(); + } + + void updateHoldState(bool comboNow) { + if (comboNow) { + if (!holdActive) { + holdActive = true; + dirty = true; + } + } else { + if (holdActive || holdProgressMs != 0) { + holdActive = false; + holdProgressMs = 0; + dirty = true; + } + } + } + + void advanceHoldProgress() { + if (holdActive) { + const std::uint32_t next = + std::min(holdProgressMs + kRefreshIntervalMs, kUnlockHoldMs); + if (next != holdProgressMs) { + holdProgressMs = next; + dirty = true; + } + if (holdProgressMs >= kUnlockHoldMs) { + holdActive = false; + context.requestAppSwitchByName(kMenuAppName); + } + } else if (holdProgressMs != 0) { + holdProgressMs = 0; + dirty = true; + } } void updateDisplay() { @@ -133,6 +172,29 @@ private: font16x8::drawText(fb, x, y, text, scale, true, letterSpacing); } + static void drawRectOutline(Framebuffer& fb, int x, int y, int w, int h) { + if (w <= 0 || h <= 0) + return; + for (int dx = 0; dx < w; ++dx) { + fb.drawPixel(x + dx, y, true); + fb.drawPixel(x + dx, y + h - 1, true); + } + for (int dy = 0; dy < h; ++dy) { + fb.drawPixel(x, y + dy, true); + fb.drawPixel(x + w - 1, y + dy, true); + } + } + + static void fillRect(Framebuffer& fb, int x, int y, int w, int h) { + if (w <= 0 || h <= 0) + return; + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + fb.drawPixel(x + dx, y + dy, true); + } + } + } + static std::string formatDate(const TimeSnapshot& snap) { static const char* kWeekdays[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; if (!snap.hasWallTime) @@ -169,7 +231,24 @@ private: const std::string dateLine = formatDate(snap); drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 24, dateLine, scaleSmall, 1); - drawCenteredText(framebuffer, framebuffer.height() - 40, "PRESS ANY BUTTON", scaleSmall, 1); + const char* instruction = holdActive ? "KEEP HOLDING A+SELECT" : "HOLD A+SELECT"; + drawCenteredText(framebuffer, framebuffer.height() - 52, instruction, scaleSmall, 1); + + if (holdActive || holdProgressMs > 0) { + const int barWidth = framebuffer.width() - 64; + const int barHeight = 14; + const int barX = (framebuffer.width() - barWidth) / 2; + const int barY = framebuffer.height() - 32; + const int innerWidth = barWidth - 2; + const int innerHeight = barHeight - 2; + drawRectOutline(framebuffer, barX, barY, barWidth, barHeight); + + const float ratio = + std::clamp(holdProgressMs / static_cast(kUnlockHoldMs), 0.0f, 1.0f); + const int fillWidth = static_cast(ratio * innerWidth + 0.5f); + if (fillWidth > 0) + fillRect(framebuffer, barX + 1, barY + 1, fillWidth, innerHeight); + } framebuffer.sendFrame(); }