Clarify touch gesture and input API comments

Simplify and clarify documentation for touch gesture detection (back gesture, item taps, long-press) and board detection flags. No functional changes, just improved comment readability.
This commit is contained in:
Justin Mitchell
2026-06-15 19:42:09 -04:00
parent 9f3dddabe6
commit cef0d267b7
13 changed files with 40 additions and 83 deletions
+3 -5
View File
@@ -59,11 +59,9 @@ class HalGPIO {
inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; } inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; }
inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; } inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; }
// True on the Xteink X3/X4 transflective C3 boards. Distinct from // True on the Xteink X3/X4 boards. Unlike deviceIsX3/X4 (which both stay "X4"
// deviceIsX3/X4 (which only tell the two C3 variants apart and both stay // on non-C3 boards), this keys off BoardConfig::ACTIVE.board, so it reliably
// "X4" on non-C3 boards): this keys off BoardConfig::ACTIVE.board, so it is // gates Xteink-only features (sunlight fix) and non-Xteink-only ones (touch).
// 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; bool isXteinkDevice() const;
// Start button GPIO and setup SPI for screen and SD card // Start button GPIO and setup SPI for screen and SD card
+3 -6
View File
@@ -57,22 +57,19 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
return false; return false;
} }
// Top-left corner of the panel (panel-native, normalized). Generous so it's easy // Top-left corner fallback, as a fraction of the logical screen. Generous to hit.
// to hit; v1 is not yet orientation-mapped (see wasBackGesture NOTE in header).
static constexpr float BACK_GESTURE_FRAC_X = 0.22f; static constexpr float BACK_GESTURE_FRAC_X = 0.22f;
static constexpr float BACK_GESTURE_FRAC_Y = 0.12f; static constexpr float BACK_GESTURE_FRAC_Y = 0.12f;
bool MappedInputManager::wasBackGesture() const { bool MappedInputManager::wasBackGesture() const {
float nx = 0.0f, ny = 0.0f; float nx = 0.0f, ny = 0.0f;
if (!gpio.wasTouchTap(nx, ny)) return false; if (!gpio.wasTouchTap(nx, ny)) return false;
// A tap on the theme's header back area (orientation-mapped) acts as Back.
int lx = 0, ly = 0; int lx = 0, ly = 0;
renderer.tapToLogical(nx, ny, lx, ly); renderer.tapToLogical(nx, ny, lx, ly);
// A tap on the theme's header Back target acts as Back.
int id = 0; int id = 0;
if (TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Back, id)) return true; if (TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Back, id)) return true;
// Fallback corner gesture: top-left of the LOGICAL screen (orientation-mapped), so // Else the top-left corner, for screens with no Back target (e.g. the reader).
// it lands on the visual top-left in every orientation and works on screens with no
// header/Back target (e.g. the reader).
return lx <= renderer.getScreenWidth() * BACK_GESTURE_FRAC_X && return lx <= renderer.getScreenWidth() * BACK_GESTURE_FRAC_X &&
ly <= renderer.getScreenHeight() * BACK_GESTURE_FRAC_Y; ly <= renderer.getScreenHeight() * BACK_GESTURE_FRAC_Y;
} }
+10 -18
View File
@@ -21,28 +21,20 @@ class MappedInputManager {
bool wasPressed(Button button) const; bool wasPressed(Button button) const;
bool wasReleased(Button button) const; bool wasReleased(Button button) const;
bool isPressed(Button button) const; bool isPressed(Button button) const;
// Reusable touch "back" gesture: a tap released in the top-left corner, OR a tap // Touch "back" gesture: a tap on the theme's header Back target, or in the
// on the header back area registered by the theme. Folded into Back's press/ // top-left corner. Folded into Back's edges, so every screen gets it for free.
// release edges, so every screen gets it with no per-activity code. False on
// non-touch devices.
bool wasBackGesture() const; bool wasBackGesture() const;
// One-shot: if a tap this frame hit a registered interactive element (theme // True (and writes the id) if a tap this frame hit a TouchRegistry item.
// draw methods register them via TouchRegistry), returns true and writes the // Activities treat the id as "select + activate". False on non-touch devices.
// element's id. Activities treat the id as "select + activate". False on
// non-touch devices or when the tap missed every target.
bool wasItemTapped(int& id) const; bool wasItemTapped(int& id) const;
// Press-edge analogue of wasItemTapped: fires on touch-DOWN over an item, so the // Press-edge of wasItemTapped: fires on touch-DOWN over an item so the activity
// activity can move its selection to that item (showing the selected state) before // can show it selected before release. Mirrors button nav (move, then confirm).
// release. Release still activates via wasItemTapped. Mirrors button nav (move
// selection, then confirm).
bool wasItemTouchedDown(int& id) const; bool wasItemTouchedDown(int& id) const;
// Long-press variant of wasItemTapped: true on release of a touch over an item // Subset of wasItemTapped's releases held past the long-press threshold (check
// held past the long-press threshold (a subset of wasItemTapped's releases, so // this first). Distinguishes tap vs press-and-hold.
// check this first). Lets a screen distinguish tap vs press-and-hold on touch.
bool wasItemLongPressed(int& id) const; bool wasItemLongPressed(int& id) const;
// Like wasItemTapped, but for tab-bar tabs (id = tab index) and cover/card // wasItemTapped for tab-bar tabs (id = tab index) and cover/card targets
// targets (id = item index). Distinct kinds so screens with both a list and a // (id = item index). Distinct kinds so a screen with both doesn't confuse them.
// tab bar / cover (Home, Settings) don't confuse them.
bool wasTabTapped(int& id) const; bool wasTabTapped(int& id) const;
bool wasCoverTapped(int& id) const; bool wasCoverTapped(int& id) const;
bool wasAnyPressed() const; bool wasAnyPressed() const;
+2 -6
View File
@@ -253,12 +253,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced", SettingInfo::Toggle(StrId::STR_CLOCK_SYNCED, &CrossPointSettings::clockHasBeenSynced, "clockHasBeenSynced",
StrId::STR_CUSTOMISE_STATUS_BAR), StrId::STR_CUSTOMISE_STATUS_BAR),
}; };
// The sunlight fading fix targets the Xteink X3/X4 transflective panel, so // Sunlight fading fix is Xteink-only (transflective panel); other devices get
// it only appears on those devices. Every other (touch) device instead gets // the touch reader controls toggle instead. Same gate as the reader runtime.
// 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()) { if (!gpio.isXteinkDevice()) {
v.erase(std::remove_if(v.begin(), v.end(), v.erase(std::remove_if(v.begin(), v.end(),
[](const SettingInfo& s) { return s.nameId == StrId::STR_SUNLIGHT_FADING_FIX; }), [](const SettingInfo& s) { return s.nameId == StrId::STR_SUNLIGHT_FADING_FIX; }),
+3 -6
View File
@@ -179,12 +179,9 @@ void HomeActivity::loop() {
requestUpdate(); requestUpdate();
}); });
// A tap on a menu button selects + activates it. The button menu registers // Tap a menu button to select + activate it. The button menu registers
// menu-local ids (it is drawn with selectorIndex offset by recentBooks.size()), // menu-local ids (drawn with selectorIndex offset by recentBooks.size()), so map
// so map the tapped id back into the global selector space. (The recent-book // back into the global selector space. Touch-down shows it selected; release opens.
// cover is a separate, single-item draw path — tappable in a later phase.)
// Touch-down moves the selector to the pressed menu button (shows selected state),
// like Up/Down; release opens it below.
int downId = -1; int downId = -1;
if (mappedInput.wasItemTouchedDown(downId)) { if (mappedInput.wasItemTouchedDown(downId)) {
selectorIndex = static_cast<int>(recentBooks.size()) + downId; selectorIndex = static_cast<int>(recentBooks.size()) + downId;
+2 -6
View File
@@ -256,12 +256,8 @@ void EpubReaderActivity::loop() {
requestUpdate(); requestUpdate();
} }
// Touch reader controls mirror the side page buttons (left third = back, right // Touch page nav (idempotent within a frame, so read here and again below). The
// third = forward, press-and-hold = long-press behavior) and the Confirm // top-left Back corner is consumed by wasReleased(Back) above, never reaching here.
// 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); const auto touch = ReaderUtils::detectTouchPageTurn(renderer);
// Enter reader menu activity (Confirm release, or a center touch-and-hold). // Enter reader menu activity (Confirm release, or a center touch-and-hold).
+4 -8
View File
@@ -65,14 +65,10 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
return {prev, next, tiltPrev || tiltNext}; return {prev, next, tiltPrev || tiltNext};
} }
// Touch reader controls: a tap on the left third of the (oriented) screen turns // Touch reader controls: left third = page back, right third = forward, center =
// back a page, the right third turns forward, mirroring the side page buttons. // open menu on press-and-hold (see isTouchMenuGesture). heldMs is the contact
// The center third opens the reader menu on press-and-hold (see center/heldMs; // duration, for the same long-press behavior as the buttons (chapter skip).
// the menu/Back gesture is handled by each reader). heldMs carries the contact // All-false on Xteink (no touch) or when the setting is off.
// 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 { struct TouchPageTurn {
bool prev; bool prev;
bool next; bool next;
+1 -2
View File
@@ -72,8 +72,7 @@ void TxtReaderActivity::loop() {
return; return;
} }
// Touch reader controls mirror the side page buttons (left third = back, // Touch page nav (left third = back, right third = forward).
// right third = forward). No-op on Xteink / when the setting is off.
const auto touch = ReaderUtils::detectTouchPageTurn(renderer); const auto touch = ReaderUtils::detectTouchPageTurn(renderer);
auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput); auto [prevTriggered, nextTriggered, fromTilt] = ReaderUtils::detectPageTurn(mappedInput);
+1 -4
View File
@@ -53,10 +53,7 @@ void XtcReaderActivity::onExit() {
} }
void XtcReaderActivity::loop() { void XtcReaderActivity::loop() {
// Touch reader controls mirror the side page buttons (left third = back, right // Touch page nav; center hold opens chapter selection (the Confirm analogue).
// 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); const auto touch = ReaderUtils::detectTouchPageTurn(renderer);
// Enter chapter selection activity (Confirm release, or a center touch-and-hold). // Enter chapter selection activity (Confirm release, or a center touch-and-hold).
+3 -5
View File
@@ -115,11 +115,9 @@ void SettingsActivity::onExit() {
void SettingsActivity::loop() { void SettingsActivity::loop() {
bool hasChangedCategory = false; bool hasChangedCategory = false;
// A tap on a settings row selects + activates it in one gesture. The list is drawn // Tap a settings row to select + activate it. Row 0 is the tab bar, so the list
// with selectedIndex = selectedSettingIndex - 1 (row 0 is the category tab), so map // is drawn at selectedSettingIndex - 1; map the tapped row back by +1. Touch-down
// the tapped 0-based row back by +1. (Category tab bar is tappable in a later phase.) // shows it selected; release toggles/activates below.
// Touch-down moves the selection to the pressed row (shows selected state); release
// toggles/activates it below. (Row 0 is the tab bar, so settings list id 0 -> index 1.)
int downId = -1; int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) { if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
selectedSettingIndex = downId + 1; selectedSettingIndex = downId + 1;
@@ -337,11 +337,9 @@ void KeyboardEntryActivity::loop() {
} }
} }
// A tap selects the key and presses it. Encoded id = row*100+col (bottom function // Tap a key to press it (encoded id = row*100+col). Touch-and-hold inserts the
// row = getContentRowCount()). A touch-and-hold inserts the alternate character // alternate char, like the button long-press (check wasItemLongPressed first, it's
// (numbers/symbols on a letter), mirroring the button long-press — wasItemLongPressed // a subset of these releases). Skipped in cursor mode (ambiguous with editing).
// is checked first since it's a subset of wasItemTapped's releases. Skipped in
// cursor mode, where a tap on a key would be ambiguous with cursor editing.
int tappedKey = -1; int tappedKey = -1;
if (!cursorMode && mappedInput.wasItemTapped(tappedKey)) { if (!cursorMode && mappedInput.wasItemTapped(tappedKey)) {
selectedRow = tappedKey / 100; selectedRow = tappedKey / 100;
+4 -9
View File
@@ -8,15 +8,10 @@
#include "components/themes/BaseTheme.h" // Rect #include "components/themes/BaseTheme.h" // Rect
// Frame-scoped registry of tappable UI elements. Theme draw methods record each // Frame-scoped registry of tappable UI elements. Theme draw methods record each
// interactive element's LOGICAL rect + id during render() (on the render task); // element's logical rect + id during render() (render task); the input layer
// the input layer hit-tests a tap against it during the next loop() (main task). // hit-tests a tap in the next loop() (main task). Lock-free via double buffering:
// // render writes the back buffer, publish() flips it live. No per-frame heap.
// Lock-free single-writer (render) / single-reader (loop) via double buffering: // Runtime-gated off on boards without touch (setEnabled).
// render writes the back buffer and publish() atomically flips it to live, so the
// reader never observes a half-built frame. No per-frame heap allocation.
//
// Runtime-gated: disabled on boards without touch (setEnabled(gpio.hasTouch())),
// so add()/hitTest() are a single branch on the C3.
class TouchRegistry { class TouchRegistry {
public: public:
enum Kind : uint8_t { Item = 0, Back = 1, Tab = 2, Cover = 3 }; enum Kind : uint8_t { Item = 0, Back = 1, Tab = 2, Cover = 3 };
+1 -3
View File
@@ -355,9 +355,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
} }
void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const { void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
// Left strip of the header is a tap-to-go-back zone (orientation-mapped; the // Left strip of the header is a tap-to-go-back zone (title is centered, so it's empty).
// title is centered so this area is normally empty). Mirrors the legacy corner
// gesture but as a real, theme-positioned target.
TouchRegistry::getInstance().add(Rect{rect.x, rect.y, 64, rect.height + 8}, -1, TouchRegistry::Back); TouchRegistry::getInstance().add(Rect{rect.x, rect.y, 64, rect.height + 8}, -1, TouchRegistry::Back);
// Hide last battery draw // Hide last battery draw