Add touch-down and long-press input detection

Implement touch-down event detection to provide immediate visual feedback when touching list items, mirroring button navigation behavior. Add long-press detection (500ms threshold) to distinguish between tap and hold gestures. Apply touch-down selection updates to file browser, home menu, and recent books activities.
This commit is contained in:
Justin Mitchell
2026-06-15 16:12:07 -04:00
parent 9176e76f5a
commit d739827db2
23 changed files with 223 additions and 33 deletions
@@ -206,6 +206,14 @@ void FileBrowserActivity::loop() {
const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing;
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved);
// Touch-down moves the selector to the pressed entry (shows selected state); release
// opens it below.
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(files.size())) {
selectorIndex = downId;
requestUpdate();
}
// A tap opens the tapped entry (held-time is 0 on a tap, so it takes the short-press
// open path below, never the long-press delete).
int tappedId = -1;
+8
View File
@@ -183,6 +183,14 @@ void HomeActivity::loop() {
// menu-local ids (it is drawn with selectorIndex offset by recentBooks.size()),
// so map the tapped id back into the global selector space. (The recent-book
// 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;
if (mappedInput.wasItemTouchedDown(downId)) {
selectorIndex = static_cast<int>(recentBooks.size()) + downId;
requestUpdate();
}
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped) {
@@ -63,6 +63,12 @@ void RecentBooksActivity::loop() {
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(recentBooks.size())) {
selectorIndex = downId;
requestUpdate();
}
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(recentBooks.size())) selectorIndex = tappedId;
@@ -4,13 +4,13 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h>
#include <esp_task_wdt.h>
#include "MappedInputManager.h"
#include "SilentRestart.h"
#include "WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/TaskWatchdog.h"
namespace {
constexpr const char* HOSTNAME = "crosspoint";
@@ -110,12 +110,12 @@ void CalibreConnectActivity::loop() {
LOG_DBG("CAL", "WARNING: %lu ms gap since last handleClient", timeSinceLastHandleClient);
}
esp_task_wdt_reset();
feedTaskWatchdog();
constexpr int MAX_ITERATIONS = 80;
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
webServer->handleClient();
if ((i & 0x07) == 0x07) {
esp_task_wdt_reset();
feedTaskWatchdog();
}
if ((i & 0x0F) == 0x0F) {
yield();
@@ -5,7 +5,6 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h>
#include <esp_task_wdt.h>
#include <cstddef>
@@ -16,6 +15,7 @@
#include "activities/network/CalibreConnectActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "network/TaskWatchdog.h"
#include "util/QrUtils.h"
namespace {
@@ -328,7 +328,7 @@ void CrossPointWebServerActivity::loop() {
}
// Reset watchdog BEFORE processing - HTTP header parsing can be slow
esp_task_wdt_reset();
feedTaskWatchdog();
// Process HTTP requests in tight loop for maximum throughput
// More iterations = more data processed per main loop cycle
@@ -337,7 +337,7 @@ void CrossPointWebServerActivity::loop() {
webServer->handleClient();
// Reset watchdog every 32 iterations
if ((i & 0x1F) == 0x1F) {
esp_task_wdt_reset();
feedTaskWatchdog();
}
// Yield and check for exit button every 64 iterations
if ((i & 0x3F) == 0x3F) {
@@ -30,6 +30,12 @@ void NetworkModeSelectionActivity::loop() {
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(MENU_ITEM_COUNT)) {
selectedIndex = downId;
requestUpdate();
}
// Handle confirm button (or a tap) - select current option
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
@@ -409,6 +409,19 @@ void WifiSelectionActivity::loop() {
// Handle network list state
if (state == WifiSelectionState::NETWORK_LIST) {
// Touch: down-select highlights the pressed network, tap selects it (like Confirm).
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(networks.size())) {
selectedNetworkIndex = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < static_cast<int>(networks.size())) {
selectedNetworkIndex = tappedId;
selectNetwork(selectedNetworkIndex);
return;
}
// Check for Back button to exit (cancel)
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onComplete(false);
@@ -103,6 +103,14 @@ void EpubReaderBookmarksActivity::loop() {
}
}
if (confirmingDelete < DELETE_MODE_DISPLAY) {
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(bookmarks.size())) {
selectorIndex = downId;
requestUpdate();
}
}
int tappedId = -1;
const bool tapped = (confirmingDelete < DELETE_MODE_DISPLAY) && mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(bookmarks.size())) selectorIndex = tappedId;
@@ -31,6 +31,12 @@ void EpubReaderChapterSelectionActivity::loop() {
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
const int totalItems = getTotalItems();
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < totalItems) {
selectorIndex = downId;
requestUpdate();
}
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < totalItems) selectorIndex = tappedId;
@@ -57,6 +57,14 @@ void EpubReaderMenuActivity::loop() {
requestUpdate();
});
// Touch-down moves the selection to the pressed item (shows selected state), like
// moving with Up/Down; release activates it below.
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(menuItems.size())) {
selectedIndex = downId;
requestUpdate();
}
// A tap selects the item and activates it in one gesture (falls into Confirm below).
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
@@ -54,6 +54,12 @@ void FontSelectionActivity::loop() {
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) {
selectedIndex_ = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex_ = tappedId;
@@ -32,6 +32,12 @@ void LanguageSelectActivity::loop() {
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) {
selectedIndex = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex = tappedId;
@@ -41,6 +41,12 @@ void OpdsServerListActivity::loop() {
return;
}
int downId = -1;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0) {
selectedIndex = downId;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex = tappedId;
@@ -118,6 +118,14 @@ void SettingsActivity::loop() {
// A tap on a settings row selects + activates it in one gesture. The list is drawn
// with selectedIndex = selectedSettingIndex - 1 (row 0 is the category tab), so map
// the tapped 0-based row back by +1. (Category tab bar is tappable in a later phase.)
// 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;
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
selectedSettingIndex = downId + 1;
requestUpdate();
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) {
selectedSettingIndex = tappedId + 1;
@@ -133,6 +141,7 @@ void SettingsActivity::loop() {
selectedCategoryIndex = tabId;
selectedSettingIndex = 0;
hasChangedCategory = true;
requestUpdate();
}
// Handle actions with early return
@@ -3,6 +3,7 @@
#include <I18n.h>
#include "HalDisplay.h"
#include "components/TouchRegistry.h"
#include "components/UITheme.h"
ConfirmationActivity::ConfirmationActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
@@ -46,8 +47,26 @@ void ConfirmationActivity::render(RenderLock&& lock) {
// Draw Body
if (!safeBody.empty()) {
renderer.drawCenteredText(fontId, currentY, safeBody.c_str(), true, EpdFontFamily::REGULAR);
currentY += lineHeight;
}
// On-screen Cancel / Confirm buttons (also tappable). The footer hints below map
// the same actions to Left/Right for button devices, but on touch-only devices
// the hints are hidden, so these are the only affordance.
const int btnH = lineHeight + 20;
const int totalW = renderer.getScreenWidth() - margin * 2;
const int btnW = (totalW - spacing) / 2;
const int btnY = currentY + spacing * 3;
const Rect cancelRect{margin, btnY, btnW, btnH};
const Rect confirmRect{margin + btnW + spacing, btnY, btnW, btnH};
renderer.drawRect(cancelRect.x, cancelRect.y, cancelRect.width, cancelRect.height);
renderer.drawRect(confirmRect.x, confirmRect.y, confirmRect.width, confirmRect.height);
const int btnTextY = btnY + (btnH - lineHeight) / 2;
UITheme::drawCenteredText(renderer, cancelRect, fontId, btnTextY, I18N.get(StrId::STR_CANCEL));
UITheme::drawCenteredText(renderer, confirmRect, fontId, btnTextY, I18N.get(StrId::STR_CONFIRM));
TouchRegistry::getInstance().add(cancelRect, 0, TouchRegistry::Item);
TouchRegistry::getInstance().add(confirmRect, 1, TouchRegistry::Item);
// Draw UI Elements
const auto labels = mappedInput.mapLabels("", "", I18N.get(StrId::STR_CANCEL), I18N.get(StrId::STR_CONFIRM));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -56,6 +75,16 @@ void ConfirmationActivity::render(RenderLock&& lock) {
}
void ConfirmationActivity::loop() {
// Tap the on-screen buttons: id 1 = Confirm, id 0 = Cancel.
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId)) {
ActivityResult res;
res.isCancelled = (tappedId != 1);
setResult(std::move(res));
finish();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
ActivityResult res;
res.isCancelled = false;
+14 -3
View File
@@ -337,13 +337,24 @@ void KeyboardEntryActivity::loop() {
}
}
// A tap selects the key and presses it in one gesture. Encoded id = row*100+col
// (bottom function row = getContentRowCount()). Skipped in cursor mode, where a
// tap on a key would be ambiguous with cursor editing.
// A tap selects the key and presses it. Encoded id = row*100+col (bottom function
// row = getContentRowCount()). A touch-and-hold inserts the alternate character
// (numbers/symbols on a letter), mirroring the button long-press — wasItemLongPressed
// 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;
if (!cursorMode && mappedInput.wasItemTapped(tappedKey)) {
selectedRow = tappedKey / 100;
selectedCol = tappedKey % 100;
int longKey = -1;
if (mappedInput.wasItemLongPressed(longKey)) {
const char alt = getAlternativeChar();
if (alt != '\0') {
insertChar(alt);
requestUpdate();
return;
}
}
if (handleKeyPress()) {
requestUpdate();
}