Add touch-to-logical coordinate mapping for UI

Implements tapToLogical() to convert normalized touch coordinates to orientation-aware logical screen coordinates. Adds TouchRegistry hit testing for interactive UI elements and header back button. Touch gestures now work correctly across all screen orientations (Portrait, Landscape, etc.) by inverting the rotateCoordinates transform.
This commit is contained in:
Justin Mitchell
2026-06-15 15:32:22 -04:00
parent a51098725d
commit 7294610894
25 changed files with 275 additions and 14 deletions
+6
View File
@@ -4,6 +4,8 @@
#include <algorithm>
#include "components/TouchRegistry.h"
#include "OpdsServerStore.h"
#include "boot_sleep/BootActivity.h"
#include "boot_sleep/SleepActivity.h"
@@ -48,7 +50,11 @@ void ActivityManager::renderTaskLoop() {
RenderLock lock;
if (currentActivity) {
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
// Touch targets are rebuilt every frame: clear before the activity draws, then
// publish so the next loop() hit-tests against exactly what's on screen.
TouchRegistry::getInstance().beginFrame();
currentActivity->render(std::move(lock));
TouchRegistry::getInstance().publish();
}
// Notify any task blocked in requestUpdateAndWait() that the render is done.
TaskHandle_t waiter = nullptr;
+9 -1
View File
@@ -206,7 +206,15 @@ 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);
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// 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;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(files.size())) {
selectorIndex = tappedId;
}
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (lockNextConfirmRelease) {
lockNextConfirmRelease = false;
return;
+11 -1
View File
@@ -179,7 +179,17 @@ void HomeActivity::loop() {
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// A tap on a menu button selects + activates it. The button menu registers
// 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.)
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped) {
selectorIndex = static_cast<int>(recentBooks.size()) + tappedId;
}
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (selectorIndex < recentBooks.size()) {
onSelectBook(recentBooks[selectorIndex].path);
} else {
+4 -1
View File
@@ -63,7 +63,10 @@ void RecentBooksActivity::loop() {
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(recentBooks.size())) selectorIndex = tappedId;
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
onSelectBook(recentBooks[selectorIndex].path);
@@ -30,8 +30,11 @@ void NetworkModeSelectionActivity::loop() {
return;
}
// Handle confirm button - select current option
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
// Handle confirm button (or a tap) - select current option
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(MENU_ITEM_COUNT)) selectedIndex = tappedId;
if (tapped || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
NetworkMode mode = NetworkMode::JOIN_NETWORK;
if (selectedIndex == 1) {
mode = NetworkMode::CONNECT_CALIBRE;
@@ -103,7 +103,10 @@ void EpubReaderBookmarksActivity::loop() {
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Open
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;
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Open
if (bookmarks.empty()) {
return;
}
@@ -31,7 +31,10 @@ void EpubReaderChapterSelectionActivity::loop() {
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
const int totalItems = getTotalItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < totalItems) selectorIndex = tappedId;
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto tocItem = epub->getTocItem(selectorIndex);
if (tocItem.spineIndex == -1) {
ActivityResult result;
@@ -57,7 +57,14 @@ void EpubReaderMenuActivity::loop() {
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// A tap selects the item and activates it in one gesture (falls into Confirm below).
int tappedId = -1;
const bool tapped = mappedInput.wasItemTapped(tappedId);
if (tapped && tappedId >= 0 && tappedId < static_cast<int>(menuItems.size())) {
selectedIndex = tappedId;
}
if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
const auto selectedAction = menuItems[selectedIndex].action;
if (selectedAction == MenuAction::ROTATE_SCREEN) {
// Cycle orientation preview locally; actual rotation happens on menu exit.
@@ -54,6 +54,13 @@ void FontSelectionActivity::loop() {
return;
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex_ = tappedId;
handleSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
@@ -32,6 +32,13 @@ void LanguageSelectActivity::loop() {
return;
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex = tappedId;
handleSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
@@ -41,6 +41,13 @@ void OpdsServerListActivity::loop() {
return;
}
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0) {
selectedIndex = tappedId;
handleSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
@@ -115,6 +115,17 @@ void SettingsActivity::onExit() {
void SettingsActivity::loop() {
bool hasChangedCategory = false;
// 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.)
int tappedId = -1;
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) {
selectedSettingIndex = tappedId + 1;
toggleCurrentSetting();
requestUpdate();
return;
}
// Handle actions with early return
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (selectedSettingIndex == 0) {