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.
This commit is contained in:
Justin Mitchell
2026-06-15 17:01:42 -04:00
parent 0036a220be
commit 79ef9d6a35
9 changed files with 101 additions and 6 deletions
+1
View File
@@ -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"
+5
View File
@@ -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)) {
+7
View File
@@ -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();
+3
View File
@@ -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)
+21
View File
@@ -1,6 +1,7 @@
#pragma once
#include <HalClock.h>
#include <HalGPIO.h>
#include <HalTiltSensor.h>
#include <I18n.h>
#include <SdCardFontRegistry.h>
@@ -252,6 +253,26 @@ inline std::vector<SettingInfo> 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)
+11 -2
View File
@@ -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)) {
+35
View File
@@ -2,6 +2,7 @@
#include <CrossPointSettings.h>
#include <GfxRenderer.h>
#include <HalGPIO.h>
#include <HalTiltSensor.h>
#include <Logging.h>
@@ -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);
+7 -1
View File
@@ -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;
}
+11 -3
View File
@@ -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) {