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();