From 79ef9d6a35910fe4b680f91850373e39346829d7 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Mon, 15 Jun 2026 17:01:42 -0400 Subject: [PATCH] Add touch reader controls for page navigation Implements tap zones on touch devices for page back/forward navigation and press-and-hold actions, mirroring physical button behavior. Adds isXteinkDevice() helper to distinguish Xteink X3/X4 boards from touch devices. The sunlight fading fix setting is now exclusive to Xteink devices, while touch devices get the new touch reader controls toggle instead. --- lib/I18n/translations/english.yaml | 1 + lib/hal/HalGPIO.cpp | 5 +++ lib/hal/HalGPIO.h | 7 ++++ src/CrossPointSettings.h | 3 ++ src/SettingsList.h | 21 ++++++++++++ src/activities/reader/EpubReaderActivity.cpp | 13 ++++++-- src/activities/reader/ReaderUtils.h | 35 ++++++++++++++++++++ src/activities/reader/TxtReaderActivity.cpp | 8 ++++- src/activities/reader/XtcReaderActivity.cpp | 14 ++++++-- 9 files changed, 101 insertions(+), 6 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 2ad915ba..2d55c6bd 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -73,6 +73,7 @@ STR_IMAGES_SUPPRESS: "Suppress" STR_SHORT_PWR_BTN: "Short Power Button Click" STR_ORIENTATION: "Reading Orientation" STR_SIDE_BTN_LAYOUT: "Side Button Layout (reader)" +STR_TOUCH_READER_CONTROLS: "Touch Reader Controls" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orient front buttons" STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior" STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 6b952c30..9f07b1df 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -251,6 +251,11 @@ unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); } +bool HalGPIO::isXteinkDevice() const { + const auto board = BoardConfig::ACTIVE.board; + return board == BoardConfig::Board::XteinkX3 || board == BoardConfig::Board::XteinkX4; +} + void HalGPIO::startDeepSleep() { // Ensure that the power button has been released to avoid immediately turning back on if you're holding it while (inputMgr.isPressed(BTN_POWER)) { diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index fb446fbe..2d89fae1 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -59,6 +59,13 @@ class HalGPIO { inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; } inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; } + // True on the Xteink X3/X4 transflective C3 boards. Distinct from + // deviceIsX3/X4 (which only tell the two C3 variants apart and both stay + // "X4" on non-C3 boards): this keys off BoardConfig::ACTIVE.board, so it is + // the reliable "is this an Xteink device" check used to gate features that + // are Xteink-only (sunlight fading fix) or non-Xteink-only (touch controls). + bool isXteinkDevice() const; + // Start button GPIO and setup SPI for screen and SD card void begin(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 07413595..3fe88304 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -230,6 +230,9 @@ class CrossPointSettings { uint8_t uiTheme = LYRA; // Sunlight fading compensation uint8_t fadingFix = 0; + // Touch reader controls: tap zones for page back/forward + press-and-hold, + // mirroring the physical buttons. Touch devices only (hidden elsewhere). + uint8_t touchReaderControls = 1; // Power button return from footnotes (1 = enabled, 0 = disabled) uint8_t pwrBtnFootnoteBack = 1; // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) diff --git a/src/SettingsList.h b/src/SettingsList.h index acdba5bb..f8ce3e1e 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -252,6 +253,26 @@ inline std::vector getSettingsList(const SdCardFontRegistry* regist SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced", StrId::STR_CUSTOMISE_STATUS_BAR), }; + // The sunlight fading fix targets the Xteink X3/X4 transflective panel, so + // it only appears on those devices. Every other (touch) device instead gets + // the touch reader controls toggle (tap page back/forward + press-and-hold, + // mirroring the physical buttons). Keyed off the board identity + // (gpio.isXteinkDevice()), not gpio.hasTouch(), so the reader runtime gate + // and this visibility gate share one source of truth. + if (!gpio.isXteinkDevice()) { + v.erase(std::remove_if(v.begin(), v.end(), + [](const SettingInfo& s) { return s.nameId == StrId::STR_SUNLIGHT_FADING_FIX; }), + v.end()); + for (auto it = v.begin(); it != v.end(); ++it) { + if (it->nameId == StrId::STR_SIDE_BTN_LAYOUT) { + v.insert(it, SettingInfo::Toggle(StrId::STR_TOUCH_READER_CONTROLS, + &CrossPointSettings::touchReaderControls, "touchReaderControls", + StrId::STR_CAT_CONTROLS)); + break; + } + } + } + // Only show tilt page turn setting when the QMI8658 IMU is present (X3) if (halTiltSensor.isAvailable()) { // Insert after the short power button setting (end of Controls section) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 08378ed3..f6965f2f 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -338,7 +338,15 @@ void EpubReaderActivity::loop() { return; } - const auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); + // 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; if (!prevTriggered && !nextTriggered) { return; } @@ -356,7 +364,8 @@ void EpubReaderActivity::loop() { return; } - const bool longPress = !fromTilt && mappedInput.getHeldTime() > ReaderUtils::SKIP_HOLD_MS; + const unsigned long heldMs = (touch.prev || touch.next) ? touch.heldMs : mappedInput.getHeldTime(); + const bool longPress = !fromTilt && heldMs > ReaderUtils::SKIP_HOLD_MS; // Don't skip chapter after screenshot if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) { diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h index 9d0a4aba..22b94a44 100644 --- a/src/activities/reader/ReaderUtils.h +++ b/src/activities/reader/ReaderUtils.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -61,6 +62,40 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) { return {prev, next, tiltPrev || tiltNext}; } +// 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. +struct TouchPageTurn { + bool prev; + bool next; + unsigned long heldMs; +}; + +inline TouchPageTurn detectTouchPageTurn(GfxRenderer& renderer) { + TouchPageTurn result{false, false, 0}; + if (gpio.isXteinkDevice() || !SETTINGS.touchReaderControls) { + return result; + } + float nx = 0.0f, ny = 0.0f; + if (!gpio.wasTouchTap(nx, ny)) { + return result; + } + int lx = 0, ly = 0; + renderer.tapToLogical(nx, ny, lx, ly); + const int third = renderer.getScreenWidth() / 3; + if (lx < third) { + result.prev = true; + } else if (lx >= 2 * third) { + result.next = true; + } + result.heldMs = gpio.lastTouchHeldMs(); + return result; +} + inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) { if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(HalDisplay::HALF_REFRESH); diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 0f3a7992..76344e01 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -72,7 +72,13 @@ void TxtReaderActivity::loop() { return; } - const auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); + // Touch reader controls mirror the side page buttons (left third = back, + // right third = forward). 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; if (!prevTriggered && !nextTriggered) { return; } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index c78aeea1..7fd51ab3 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -79,7 +79,14 @@ void XtcReaderActivity::loop() { return; } - const auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); + // 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; if (!prevTriggered && !nextTriggered) { return; } @@ -95,8 +102,9 @@ void XtcReaderActivity::loop() { return; } - const bool skipPages = !fromTilt && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP && - mappedInput.getHeldTime() > ReaderUtils::SKIP_HOLD_MS; + const unsigned long heldMs = (touch.prev || touch.next) ? touch.heldMs : mappedInput.getHeldTime(); + const bool skipPages = + !fromTilt && SETTINGS.longPressButtonBehavior == SETTINGS.CHAPTER_SKIP && heldMs > ReaderUtils::SKIP_HOLD_MS; const int skipAmount = skipPages ? 10 : 1; if (prevTriggered) {