From 72946108942b071b184025b28d589844caf3c778 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Mon, 15 Jun 2026 15:32:22 -0400 Subject: [PATCH] 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. --- lib/GfxRenderer/GfxRenderer.cpp | 32 +++++++++++ lib/GfxRenderer/GfxRenderer.h | 5 ++ lib/hal/HalGPIO.cpp | 2 + lib/hal/HalGPIO.h | 3 ++ src/MappedInputManager.cpp | 17 ++++++ src/MappedInputManager.h | 18 +++++-- src/activities/ActivityManager.cpp | 6 +++ src/activities/home/FileBrowserActivity.cpp | 10 +++- src/activities/home/HomeActivity.cpp | 12 ++++- src/activities/home/RecentBooksActivity.cpp | 5 +- .../network/NetworkModeSelectionActivity.cpp | 7 ++- .../reader/EpubReaderBookmarksActivity.cpp | 5 +- .../EpubReaderChapterSelectionActivity.cpp | 5 +- .../reader/EpubReaderMenuActivity.cpp | 9 +++- .../settings/FontSelectionActivity.cpp | 7 +++ .../settings/LanguageSelectActivity.cpp | 7 +++ .../settings/OpdsServerListActivity.cpp | 7 +++ src/activities/settings/SettingsActivity.cpp | 11 ++++ src/components/TouchRegistry.cpp | 41 ++++++++++++++ src/components/TouchRegistry.h | 53 +++++++++++++++++++ src/components/themes/BaseTheme.cpp | 11 ++++ src/components/themes/BaseTheme.h | 3 ++ src/components/themes/lyra/LyraTheme.cpp | 4 ++ .../themes/roundedraff/RoundedRaffTheme.cpp | 3 ++ src/main.cpp | 6 ++- 25 files changed, 275 insertions(+), 14 deletions(-) create mode 100644 src/components/TouchRegistry.cpp create mode 100644 src/components/TouchRegistry.h diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 759b2ad4..fb084e03 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -1296,6 +1296,38 @@ int GfxRenderer::getScreenHeight() const { return panelWidth; } +void GfxRenderer::tapToLogical(float nx, float ny, int& outX, int& outY) const { + // Native panel pixel of the tap (wasTouchTap normalizes over the native panel). + int phyX = static_cast(nx * panelWidth); + int phyY = static_cast(ny * panelHeight); + if (phyX < 0) phyX = 0; + if (phyX > panelWidth - 1) phyX = panelWidth - 1; + if (phyY < 0) phyY = 0; + if (phyY > panelHeight - 1) phyY = panelHeight - 1; + + // Inverse of rotateCoordinates() (see the forward transform above): map a + // physical/native point back into the current orientation's logical frame. + switch (orientation) { + case Portrait: // forward: phyX=logY, phyY=panelHeight-1-logX + outX = panelHeight - 1 - phyY; + outY = phyX; + break; + case PortraitInverted: // forward: phyX=panelWidth-1-logY, phyY=logX + outX = phyY; + outY = panelWidth - 1 - phyX; + break; + case LandscapeClockwise: // forward: phyX=panelWidth-1-logX, phyY=panelHeight-1-logY + outX = panelWidth - 1 - phyX; + outY = panelHeight - 1 - phyY; + break; + case LandscapeCounterClockwise: // forward: identity + default: + outX = phyX; + outY = phyY; + break; + } +} + // Translate a logical rect through rotateCoordinates and take the bounding // box of its four corners on the physical panel. Output coords are inclusive // and clamped. Returns false if the rect ends up fully off-panel. diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 924e5c0b..e21675e8 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -135,6 +135,11 @@ class GfxRenderer { void clearScreen(uint8_t color = 0xFF) const; void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; + // Map a touch tap (normalized 0..1 in panel-native orientation, from + // InputManager::wasTouchTap) to logical screen coordinates matching the Rects the + // UI draws in. Inverse of rotateCoordinates() for the current orientation. + void tapToLogical(float nx, float ny, int& outX, int& outY) const; + // Tiled grayscale strip target. While active, drawPixel() and clearScreen() // operate on `scratch` (panelWidthBytes * stripRows bytes, holding physical // rows [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index c7cc4622..43023d45 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -245,6 +245,8 @@ unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPower bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouchTap(nx, ny); } +bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); } + 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 c9c3d373..94d86177 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -77,6 +77,9 @@ class HalGPIO { // primitive; see MappedInputManager for the top-left = Back mapping.) bool wasTouchTap(float& nx, float& ny) const; + // True if a touch controller is present/active (runtime gate; false on the C3). + bool hasTouch() const; + // Setup wake up GPIO and enter deep sleep void startDeepSleep(); diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 68757f2d..317958a8 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -1,6 +1,9 @@ #include "MappedInputManager.h" +#include + #include "CrossPointSettings.h" +#include "components/TouchRegistry.h" bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { const auto sideLayout = SETTINGS.sideButtonLayout; @@ -62,9 +65,23 @@ static constexpr float BACK_GESTURE_FRAC_Y = 0.12f; bool MappedInputManager::wasBackGesture() const { float nx = 0.0f, ny = 0.0f; 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; + renderer.tapToLogical(nx, ny, lx, ly); + int id = 0; + if (TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Back, id)) return true; + // Fallback corner gesture (panel-native, generous; works even with no Back target). return nx <= BACK_GESTURE_FRAC_X && ny <= BACK_GESTURE_FRAC_Y; } +bool MappedInputManager::wasItemTapped(int& id) const { + float nx = 0.0f, ny = 0.0f; + if (!gpio.wasTouchTap(nx, ny)) return false; + int lx = 0, ly = 0; + renderer.tapToLogical(nx, ny, lx, ly); + return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id); +} + bool MappedInputManager::wasPressed(const Button button) const { // A top-left tap fires on the release frame; expose it on Back's press edge too // so menus that act on wasPressed(Back) also respond. Deliberately NOT folded diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index fef4bf50..5480e40e 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -2,6 +2,8 @@ #include +class GfxRenderer; + class MappedInputManager { public: enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward }; @@ -13,17 +15,22 @@ class MappedInputManager { const char* btn4; }; - explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {} + MappedInputManager(HalGPIO& gpio, GfxRenderer& renderer) : gpio(gpio), renderer(renderer) {} void update() const { gpio.update(); } bool wasPressed(Button button) const; bool wasReleased(Button button) const; bool isPressed(Button button) const; - // Reusable touch "back" gesture: a tap released in the top-left corner. Folded - // into Back's press/release edges, so every screen gets it with no per-activity - // code and no coordinates passed. False on non-touch devices. - // NOTE: v1 uses panel-native coordinates (not yet orientation-mapped). + // Reusable touch "back" gesture: a tap released in the top-left corner, OR a tap + // on the header back area registered by the theme. Folded into Back's press/ + // release edges, so every screen gets it with no per-activity code. False on + // non-touch devices. bool wasBackGesture() const; + // One-shot: if a tap this frame hit a registered interactive element (theme + // draw methods register them via TouchRegistry), returns true and writes the + // 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 wasAnyPressed() const; bool wasAnyReleased() const; unsigned long getHeldTime() const; @@ -33,6 +40,7 @@ class MappedInputManager { private: HalGPIO& gpio; + GfxRenderer& renderer; bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; }; diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 0cf672c3..20519e8c 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -4,6 +4,8 @@ #include +#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; diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index 256a6755..7ee4ef17 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -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(files.size())) { + selectorIndex = tappedId; + } + + if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (lockNextConfirmRelease) { lockNextConfirmRelease = false; return; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 28743264..d9fdd148 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -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(recentBooks.size()) + tappedId; + } + + if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (selectorIndex < recentBooks.size()) { onSelectBook(recentBooks[selectorIndex].path); } else { diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 7b4a0117..97a0a974 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -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(recentBooks.size())) selectorIndex = tappedId; + if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!recentBooks.empty() && selectorIndex < static_cast(recentBooks.size())) { LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str()); onSelectBook(recentBooks[selectorIndex].path); diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index f8e45307..c2f80414 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -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(MENU_ITEM_COUNT)) selectedIndex = tappedId; + if (tapped || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { NetworkMode mode = NetworkMode::JOIN_NETWORK; if (selectedIndex == 1) { mode = NetworkMode::CONNECT_CALIBRE; diff --git a/src/activities/reader/EpubReaderBookmarksActivity.cpp b/src/activities/reader/EpubReaderBookmarksActivity.cpp index fab839e7..79345c3a 100644 --- a/src/activities/reader/EpubReaderBookmarksActivity.cpp +++ b/src/activities/reader/EpubReaderBookmarksActivity.cpp @@ -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(bookmarks.size())) selectorIndex = tappedId; + if (tapped || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Open if (bookmarks.empty()) { return; } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 6306346c..8dddd247 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -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; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 634db50c..a36a1ba5 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -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(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. diff --git a/src/activities/settings/FontSelectionActivity.cpp b/src/activities/settings/FontSelectionActivity.cpp index 302379cd..a75b5a97 100644 --- a/src/activities/settings/FontSelectionActivity.cpp +++ b/src/activities/settings/FontSelectionActivity.cpp @@ -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; diff --git a/src/activities/settings/LanguageSelectActivity.cpp b/src/activities/settings/LanguageSelectActivity.cpp index 231cba52..b6e63870 100644 --- a/src/activities/settings/LanguageSelectActivity.cpp +++ b/src/activities/settings/LanguageSelectActivity.cpp @@ -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; diff --git a/src/activities/settings/OpdsServerListActivity.cpp b/src/activities/settings/OpdsServerListActivity.cpp index 30557703..c6381850 100644 --- a/src/activities/settings/OpdsServerListActivity.cpp +++ b/src/activities/settings/OpdsServerListActivity.cpp @@ -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; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 8c0a7ef6..d11a1f14 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -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) { diff --git a/src/components/TouchRegistry.cpp b/src/components/TouchRegistry.cpp new file mode 100644 index 00000000..501411d9 --- /dev/null +++ b/src/components/TouchRegistry.cpp @@ -0,0 +1,41 @@ +#include "TouchRegistry.h" + +TouchRegistry& TouchRegistry::getInstance() { + static TouchRegistry instance; + return instance; +} + +void TouchRegistry::beginFrame() { + if (!enabled_) return; + counts_[backIndex()] = 0; +} + +void TouchRegistry::add(const Rect& rect, int id, Kind kind) { + if (!enabled_) return; + const uint8_t b = backIndex(); + size_t& n = counts_[b]; + if (n >= CAPACITY) return; // silently drop overflow; CAPACITY sized for a screen + buffers_[b][n] = Target{rect, static_cast(id), static_cast(kind)}; + ++n; +} + +void TouchRegistry::publish() { + if (!enabled_) return; + // Flip live to the buffer we just filled. Release so the reader sees the writes. + live_.store(backIndex(), std::memory_order_release); +} + +bool TouchRegistry::hitTest(int x, int y, Kind kind, int& outId) const { + if (!enabled_) return false; + const uint8_t b = live_.load(std::memory_order_acquire); + const size_t n = counts_[b]; + const auto& buf = buffers_[b]; + // Last-registered wins (topmost / most recently drawn). + for (size_t i = n; i-- > 0;) { + if (buf[i].kind == kind && buf[i].rect.contains(x, y)) { + outId = buf[i].id; + return true; + } + } + return false; +} diff --git a/src/components/TouchRegistry.h b/src/components/TouchRegistry.h new file mode 100644 index 00000000..c5313d69 --- /dev/null +++ b/src/components/TouchRegistry.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#include "components/themes/BaseTheme.h" // Rect + +// Frame-scoped registry of tappable UI elements. Theme draw methods record each +// interactive element's LOGICAL rect + id during render() (on the render task); +// the input layer hit-tests a tap against it during the next loop() (main task). +// +// Lock-free single-writer (render) / single-reader (loop) via double buffering: +// 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 { + public: + enum Kind : uint8_t { Item = 0, Back = 1 }; + + static TouchRegistry& getInstance(); + + void setEnabled(bool enabled) { enabled_ = enabled; } + bool isEnabled() const { return enabled_; } + + // Render task: clear the back buffer, append targets, then publish. + void beginFrame(); + void add(const Rect& rect, int id, Kind kind); + void publish(); + + // Main/loop task: find the topmost target of `kind` containing the logical point. + // Returns true and writes its id to outId on a hit. + bool hitTest(int x, int y, Kind kind, int& outId) const; + + private: + static constexpr size_t CAPACITY = 48; // worst case excl. keyboard grid (Phase 2) + + struct Target { + Rect rect; + int16_t id; + uint8_t kind; + }; + + uint8_t backIndex() const { return live_.load(std::memory_order_relaxed) ^ 1u; } + + std::array, 2> buffers_{}; + std::array counts_{0, 0}; + std::atomic live_{0}; + bool enabled_ = false; +}; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index a4e35c35..b44d8c5b 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -12,6 +12,7 @@ #include "I18n.h" #include "RecentBooksStore.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" #include "fontIds.h" @@ -303,6 +304,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, const auto pageStartIndex = selectedIndex / pageItems * pageItems; for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) { const int itemY = rect.y + (i % pageItems) * rowHeight; + TouchRegistry::getInstance().add(Rect{rect.x, itemY - 2, rect.width, rowHeight}, i, TouchRegistry::Item); int rowTextWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2; std::string valueText; @@ -353,6 +355,11 @@ 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 { + // Left strip of the header is a tap-to-go-back zone (orientation-mapped; the + // 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); + // Hide last battery draw constexpr int maxBatteryWidth = 80; renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth, @@ -669,6 +676,10 @@ void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount for (int i = 0; i < buttonCount; ++i) { const int tileY = BaseMetrics::values.verticalSpacing + rect.y + static_cast(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing); + TouchRegistry::getInstance().add( + Rect{rect.x + BaseMetrics::values.contentSidePadding, tileY, + rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight}, + i, TouchRegistry::Item); const bool selected = selectedIndex == i; diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 75c6d1be..e0dd7003 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -16,6 +16,9 @@ struct Rect { int height; explicit Rect(int x = 0, int y = 0, int width = 0, int height = 0) : x(x), y(y), width(width), height(height) {} + + // Logical-coordinate point hit-test (used for touch target hit-testing). + bool contains(int px, int py) const { return px >= x && px < x + width && py >= y && py < y + height; } }; struct TabInfo { diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index a39d0f5e..cf60bdd1 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -11,6 +11,7 @@ #include #include "RecentBooksStore.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" #include "components/icons/book.h" #include "components/icons/book24.h" @@ -106,6 +107,7 @@ void LyraTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const { renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); + TouchRegistry::getInstance().add(Rect{rect.x, rect.y, 64, rect.height + 8}, -1, TouchRegistry::Back); const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; @@ -258,6 +260,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int iconY = (rowSubtitle != nullptr) ? 16 : 10; for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) { const int itemY = rect.y + (i % pageItems) * rowHeight; + TouchRegistry::getInstance().add(Rect{rect.x, itemY, rect.width, rowHeight}, i, TouchRegistry::Item); int rowTextWidth = textWidth; // Draw name @@ -517,6 +520,7 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding, rect.y + i * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth, LyraMetrics::values.menuRowHeight}; + TouchRegistry::getInstance().add(tileRect, i, TouchRegistry::Item); const bool selected = selectedIndex == i; diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.cpp b/src/components/themes/roundedraff/RoundedRaffTheme.cpp index 4a46f459..4f2cdbe2 100644 --- a/src/components/themes/roundedraff/RoundedRaffTheme.cpp +++ b/src/components/themes/roundedraff/RoundedRaffTheme.cpp @@ -9,6 +9,7 @@ #include #include "RecentBooksStore.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" #include "components/icons/cover.h" #include "fontIds.h" @@ -216,6 +217,7 @@ void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int butt renderer.truncatedText(kTitleFontId, label.c_str(), maxLabelWidth, EpdFontFamily::BOLD); const int rowWidth = std::min( menuMaxWidth, renderer.getTextWidth(kTitleFontId, truncatedLabel.c_str(), EpdFontFamily::BOLD) + kRowPaddingX); + TouchRegistry::getInstance().add(Rect{rowX, rowY, rowWidth, rowHeight}, i, TouchRegistry::Item); const bool isSelected = selectedIndex == i; renderer.fillRoundedRect(rowX, rowY, rowWidth, rowHeight, kMenuRadius, isSelected ? Color::Black : Color::White); const int textY = rowY + (rowHeight - textLineHeight) / 2; @@ -329,6 +331,7 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) { const int rowY = rect.y + (i % pageItems) * rowStep; + TouchRegistry::getInstance().add(Rect{rowX, rowY, rowWidth, rowHeight}, i, TouchRegistry::Item); const bool isSelected = i == selectedIndex; renderer.fillRoundedRect(rowX, rowY, rowWidth, rowHeight, kRowRadius, isSelected ? Color::Black : Color::White); diff --git a/src/main.cpp b/src/main.cpp index 9c71ba13..7a7b2bb3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,14 +28,15 @@ #include "activities/Activity.h" #include "activities/ActivityManager.h" #include "activities/settings/SdFirmwareUpdateActivity.h" +#include "components/TouchRegistry.h" #include "components/UITheme.h" #include "fontIds.h" #include "images/LoadingIcon.h" #include "util/ButtonNavigator.h" #include "util/ScreenshotUtil.h" -MappedInputManager mappedInputManager(gpio); GfxRenderer renderer(display); +MappedInputManager mappedInputManager(gpio, renderer); ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; SdCardFontSystem sdFontSystem; @@ -339,6 +340,9 @@ void setup() { silentRebootTarget = 0; gpio.begin(); + // Enable touch-target registration only on boards with a touch panel (no-op cost + // on the C3). Must come after gpio.begin() so the controller has been probed. + TouchRegistry::getInstance().setEnabled(gpio.hasTouch()); powerManager.begin(); halTiltSensor.begin(); halClock.begin();