From 9f3dddabe6be17487465917cb4dc11db31168f7f Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Mon, 15 Jun 2026 17:16:59 -0400 Subject: [PATCH] Add touch gesture to open reader menu Implements a press-and-hold gesture in the center touch zone (400ms threshold) that opens the reader menu, mirroring the Confirm button behavior. The center third of the screen is now reserved for this menu gesture, while left and right thirds continue to handle page turns. Applied to both EPUB and XTC readers. --- src/activities/reader/EpubReaderActivity.cpp | 18 +++++++------- src/activities/reader/ReaderUtils.h | 25 +++++++++++++++----- src/activities/reader/XtcReaderActivity.cpp | 15 ++++++------ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index f6965f2f..af4da25b 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -256,8 +256,16 @@ void EpubReaderActivity::loop() { requestUpdate(); } - // Enter reader menu activity. - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + // Touch reader controls mirror the side page buttons (left third = back, right + // third = forward, press-and-hold = long-press behavior) and the Confirm + // button (center press-and-hold = open menu). The top-left Back corner is + // consumed by wasReleased(Back) earlier, so it never reaches here. No-op on + // Xteink / when the setting is off. wasTouchTap is idempotent within a frame, + // so reading it here and again below is safe. + const auto touch = ReaderUtils::detectTouchPageTurn(renderer); + + // Enter reader menu activity (Confirm release, or a center touch-and-hold). + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || ReaderUtils::isTouchMenuGesture(touch)) { if (ignoreNextConfirmRelease) { ignoreNextConfirmRelease = false; } else { @@ -338,12 +346,6 @@ void EpubReaderActivity::loop() { return; } - // Touch reader controls mirror the side page buttons (left third = back, - // right third = forward, press-and-hold = long-press behavior). The top-left - // Back corner is consumed by wasReleased(Back) earlier, so it never reaches - // here. No-op on Xteink / when the setting is off. - const auto touch = ReaderUtils::detectTouchPageTurn(renderer); - auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); prevTriggered = prevTriggered || touch.prev; nextTriggered = nextTriggered || touch.next; diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 22b94a44..cf12006f 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -14,6 +14,9 @@ constexpr unsigned long GO_HOME_MS = 1000; constexpr unsigned long SKIP_HOLD_MS = 700; constexpr unsigned long BOOKMARK_HOLD_MS = 400; constexpr unsigned long BOOKMARK_MESSAGE_DURATION_MS = 2500; +// Press-and-hold in the center touch zone (see detectTouchPageTurn) opens the +// reader menu, the touch analogue of releasing the Confirm button. +constexpr unsigned long TOUCH_MENU_HOLD_MS = 400; inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) { switch (orientation) { @@ -64,19 +67,21 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) { // Touch reader controls: a tap on the left third of the (oriented) screen turns // back a page, the right third turns forward, mirroring the side page buttons. -// heldMs carries the contact duration so callers can apply the same long-press -// behavior (chapter skip / orientation change) as the buttons. The center column -// is left for the menu/Back gesture handled by each reader. Gated off the Xteink -// devices (no touch) and behind the touchReaderControls setting; returns all-false -// otherwise, so non-touch readers pay a single branch. +// The center third opens the reader menu on press-and-hold (see center/heldMs; +// the menu/Back gesture is handled by each reader). heldMs carries the contact +// duration so callers can apply the same long-press behavior (chapter skip / +// orientation change) as the buttons. Gated off the Xteink devices (no touch) +// and behind the touchReaderControls setting; returns all-false otherwise, so +// non-touch readers pay a single branch. struct TouchPageTurn { bool prev; bool next; + bool center; unsigned long heldMs; }; inline TouchPageTurn detectTouchPageTurn(GfxRenderer& renderer) { - TouchPageTurn result{false, false, 0}; + TouchPageTurn result{false, false, false, 0}; if (gpio.isXteinkDevice() || !SETTINGS.touchReaderControls) { return result; } @@ -91,11 +96,19 @@ inline TouchPageTurn detectTouchPageTurn(GfxRenderer& renderer) { result.prev = true; } else if (lx >= 2 * third) { result.next = true; + } else { + result.center = true; } result.heldMs = gpio.lastTouchHeldMs(); return result; } +// True when the center zone was pressed and held long enough to open the reader +// menu (touch analogue of a Confirm release). +inline bool isTouchMenuGesture(const TouchPageTurn& touch) { + return touch.center && touch.heldMs >= TOUCH_MENU_HOLD_MS; +} + inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) { if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(HalDisplay::HALF_REFRESH); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 7fd51ab3..d398f2e5 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -53,8 +53,14 @@ void XtcReaderActivity::onExit() { } void XtcReaderActivity::loop() { - // Enter chapter selection activity - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + // Touch reader controls mirror the side page buttons (left third = back, right + // third = forward, press-and-hold = chapter skip) and the Confirm button + // (center press-and-hold = chapter selection). No-op on Xteink / when the + // setting is off. + const auto touch = ReaderUtils::detectTouchPageTurn(renderer); + + // Enter chapter selection activity (Confirm release, or a center touch-and-hold). + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || ReaderUtils::isTouchMenuGesture(touch)) { if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { startActivityForResult( std::make_unique(renderer, mappedInput, xtc, currentPage), @@ -79,11 +85,6 @@ void XtcReaderActivity::loop() { return; } - // Touch reader controls mirror the side page buttons (left third = back, - // right third = forward, press-and-hold = chapter skip). No-op on Xteink / - // when the setting is off. - const auto touch = ReaderUtils::detectTouchPageTurn(renderer); - auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); prevTriggered = prevTriggered || touch.prev; nextTriggered = nextTriggered || touch.next;