#include "MappedInputManager.h" #include #include #include #include "CrossPointSettings.h" #include "components/UITheme.h" bool MappedInputManager::isNavDirectionSwapped() const { // Key the swap on the orientation the screen is *actually* rendered at, not the persisted reader // setting. The reader (and its modal menus) render rotated, so navigation/labels flip there; the // home and settings UI render in portrait, so they never flip even when a rotated reader is configured. const auto orientation = renderer.getOrientation(); return SETTINGS.frontButtonFollowOrientation && (orientation == GfxRenderer::PortraitInverted || orientation == GfxRenderer::LandscapeCounterClockwise); } bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { const auto sideLayout = SETTINGS.sideButtonLayout; switch (button) { case Button::Back: // Logical Back maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonBack); case Button::Confirm: // Logical Confirm maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonConfirm); case Button::Left: // Logical Left maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonLeft); case Button::Right: // Logical Right maps to user-configured front button. return (gpio.*fn)(SETTINGS.frontButtonRight); case Button::Up: // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_UP); case Button::Down: // Side buttons remain fixed for Up/Down. return (gpio.*fn)(HalGPIO::BTN_DOWN); case Button::Power: // Power button bypasses remapping. return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: // Reader page navigation uses side buttons and can be swapped via settings. switch (sideLayout) { case CrossPointSettings::PREV_NEXT: return (gpio.*fn)(HalGPIO::BTN_UP); case CrossPointSettings::NEXT_PREV: return (gpio.*fn)(HalGPIO::BTN_DOWN); case CrossPointSettings::SIDE_BUTTONS_DISABLED: default: return false; } case Button::PageForward: // Reader page navigation uses side buttons and can be swapped via settings. switch (sideLayout) { case CrossPointSettings::PREV_NEXT: return (gpio.*fn)(HalGPIO::BTN_DOWN); case CrossPointSettings::NEXT_PREV: return (gpio.*fn)(HalGPIO::BTN_UP); case CrossPointSettings::SIDE_BUTTONS_DISABLED: default: return false; } case Button::NavNext: // Logical "next item" navigation: side Down + front Right, with the control axis flipped in // INVERTED / LANDSCAPE_CCW (frontButtonFollowOrientation) so it matches the rotated hint labels. return isNavDirectionSwapped() ? (mapButton(Button::Up, fn) || mapButton(Button::Left, fn)) : (mapButton(Button::Down, fn) || mapButton(Button::Right, fn)); case Button::NavPrevious: // Logical "previous item" navigation: side Up + front Left, axis-flipped in the same orientations. return isNavDirectionSwapped() ? (mapButton(Button::Down, fn) || mapButton(Button::Right, fn)) : (mapButton(Button::Up, fn) || mapButton(Button::Left, fn)); } return false; } namespace { constexpr float LEFT_EDGE_BACK_GESTURE_FRAC_X = 0.25f; constexpr float BOTTOM_EDGE_BACK_GESTURE_FRAC_Y = 0.14f; constexpr float TOP_EDGE_MENU_GESTURE_FRAC_Y = 0.14f; constexpr unsigned long TOUCH_DOWN_SELECT_DELAY_MS = 90; constexpr unsigned long TOUCH_HELD_OVERRIDE_WINDOW_MS = 250; } // namespace bool MappedInputManager::hasTouch() const { return gpio.hasTouch(); } void MappedInputManager::rememberTouchHeldTime() const { touchHeldOverrideValid = true; touchHeldOverrideMs = gpio.lastTouchHeldMs(); touchHeldOverrideAt = millis(); } bool MappedInputManager::wasScreenTapped(int& x, int& y) const { float nx = 0.0f; float ny = 0.0f; if (!gpio.wasTouchTap(nx, ny)) return false; renderer.tapToLogical(nx, ny, x, y); rememberTouchHeldTime(); return true; } bool MappedInputManager::wasScreenTouchDown(int& x, int& y) const { float nx = 0.0f; float ny = 0.0f; unsigned long heldMs = 0; if (!gpio.isTouchTapCandidate(nx, ny, heldMs)) return false; if (heldMs < TOUCH_DOWN_SELECT_DELAY_MS) return false; renderer.tapToLogical(nx, ny, x, y); return true; } bool MappedInputManager::isScreenTouchHeld(int& x, int& y) const { // Live contact position while the finger is down (no tap-slop gate) — drag tracking. float nx = 0.0f; float ny = 0.0f; if (!gpio.isTouchHeldAt(nx, ny)) return false; renderer.tapToLogical(nx, ny, x, y); return true; } bool MappedInputManager::wasTapInRect(const int x, const int y, const int width, const int height) const { int tx = 0; int ty = 0; return wasScreenTapped(tx, ty) && tx >= x && tx < x + width && ty >= y && ty < y + height; } bool MappedInputManager::listItemFromPoint(const int x, const int y, int& index, const int itemCount, const int selectedIndex, const int listTop, const int listHeight, const bool hasSubtitle) const { (void)x; if (itemCount <= 0) return false; if (y < listTop || y >= listTop + listHeight) return false; const auto& theme = UITheme::getInstance().getTheme(); const int rowStep = theme.getListRowStep(hasSubtitle); if (rowStep <= 0) return false; const int pageItems = theme.getListPageItems(listHeight, hasSubtitle); if (pageItems <= 0) return false; const int pageStart = std::max(0, selectedIndex / pageItems) * pageItems; const int row = (y - listTop) / rowStep; const int tapped = pageStart + row; if (row < 0 || row >= pageItems || tapped >= itemCount) return false; index = tapped; return true; } bool MappedInputManager::wasListItemTapped(int& index, const int itemCount, const int selectedIndex, const int listTop, const int listHeight, const bool hasSubtitle) const { int tx = 0; int ty = 0; return wasScreenTapped(tx, ty) && listItemFromPoint(tx, ty, index, itemCount, selectedIndex, listTop, listHeight, hasSubtitle); } bool MappedInputManager::wasListItemTouchedDown(int& index, const int itemCount, const int selectedIndex, const int listTop, const int listHeight, const bool hasSubtitle) const { int tx = 0; int ty = 0; return wasScreenTouchDown(tx, ty) && listItemFromPoint(tx, ty, index, itemCount, selectedIndex, listTop, listHeight, hasSubtitle); } MappedInputManager::RowTouch MappedInputManager::rowTouch(int& row, const int top, const int rowStep, const int rowCount, const int xStart, const int xEnd, const int rowHeight) const { if (rowStep <= 0 || rowCount <= 0) return RowTouch::None; const auto hit = [&](const int x, const int y) { if (x < xStart || x >= xEnd || y < top) return false; const int r = (y - top) / rowStep; if (r >= rowCount) return false; if (rowHeight > 0 && (y - top) % rowStep >= rowHeight) return false; row = r; return true; }; int x = 0; int y = 0; if (wasScreenTouchDown(x, y) && hit(x, y)) return RowTouch::Down; if (wasScreenTapped(x, y) && hit(x, y)) return RowTouch::Tap; return RowTouch::None; } MappedInputManager::RowTouch MappedInputManager::colTouch(int& col, const int left, const int colStep, const int colCount, const int yStart, const int yEnd, const int colWidth) const { if (colStep <= 0 || colCount <= 0) return RowTouch::None; const auto hit = [&](const int x, const int y) { if (y < yStart || y >= yEnd || x < left) return false; const int c = (x - left) / colStep; if (c >= colCount) return false; if (colWidth > 0 && (x - left) % colStep >= colWidth) return false; col = c; return true; }; int x = 0; int y = 0; if (wasScreenTouchDown(x, y) && hit(x, y)) return RowTouch::Down; if (wasScreenTapped(x, y) && hit(x, y)) return RowTouch::Tap; return RowTouch::None; } bool MappedInputManager::decodeSwipe(int& sx, int& sy, int& ex, int& ey) const { float nxs = 0.0f; float nys = 0.0f; float nxe = 0.0f; float nye = 0.0f; if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return false; renderer.tapToLogical(nxs, nys, sx, sy); renderer.tapToLogical(nxe, nye, ex, ey); return true; } MappedInputManager::SwipeDir MappedInputManager::wasSwipe() const { int sx = 0; int sy = 0; int ex = 0; int ey = 0; if (!decodeSwipe(sx, sy, ex, ey)) return SwipeDir::None; const int dx = ex - sx; const int dy = ey - sy; if (std::abs(dx) >= std::abs(dy)) { return dx < 0 ? SwipeDir::Left : SwipeDir::Right; } return dy < 0 ? SwipeDir::Up : SwipeDir::Down; } bool MappedInputManager::wasBackGesture() const { // Back = left-to-right swipe starting near the left edge. Edge-anchored so that // mid-screen horizontal swipes stay available to activities that consume // SwipeDir::Left/Right (e.g. percent selection, image viewer). int sx = 0; int sy = 0; int ex = 0; int ey = 0; if (!decodeSwipe(sx, sy, ex, ey)) return false; const bool hit = sx <= renderer.getScreenWidth() * LEFT_EDGE_BACK_GESTURE_FRAC_X && ex > sx && std::abs(ex - sx) > std::abs(ey - sy); if (hit) rememberTouchHeldTime(); return hit; } bool MappedInputManager::wasMenuGesture() const { // Downward swipe starting at the top edge (mirror of the bottom-edge home gesture). int sx = 0; int sy = 0; int ex = 0; int ey = 0; if (!decodeSwipe(sx, sy, ex, ey)) return false; const int topEdgeBottom = static_cast(renderer.getScreenHeight() * TOP_EDGE_MENU_GESTURE_FRAC_Y); const bool hit = sy <= topEdgeBottom && ey > sy && std::abs(ey - sy) > std::abs(ex - sx); if (hit) rememberTouchHeldTime(); return hit; } bool MappedInputManager::wasHomeGesture() const { int sx = 0; int sy = 0; int ex = 0; int ey = 0; if (decodeSwipe(sx, sy, ex, ey)) { const int bottomEdgeTop = renderer.getScreenHeight() - static_cast(renderer.getScreenHeight() * BOTTOM_EDGE_BACK_GESTURE_FRAC_Y); if (sy >= bottomEdgeTop && ey < sy && std::abs(ey - sy) > std::abs(ex - sx)) { rememberTouchHeldTime(); return true; } } return false; } bool MappedInputManager::wasPressed(const Button button) const { if (button == Button::Back && wasBackGesture()) return true; return mapButton(button, &HalGPIO::wasPressed); } bool MappedInputManager::wasReleased(const Button button) const { if (button == Button::Back && wasBackGesture()) return true; return mapButton(button, &HalGPIO::wasReleased); } bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); } bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); } bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); } unsigned long MappedInputManager::getHeldTime() const { if (!gpio.wasAnyPressed() && !gpio.wasAnyReleased() && touchHeldOverrideValid && millis() - touchHeldOverrideAt <= TOUCH_HELD_OVERRIDE_WINDOW_MS) { return touchHeldOverrideMs; } touchHeldOverrideValid = false; return gpio.getHeldTime(); } MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const { // Swap previous/next labels to match the page turn direction swap in INVERTED and LANDSCAPE_CCW. const bool swapLabels = isNavDirectionSwapped(); const char* leftLabel = swapLabels ? next : previous; const char* rightLabel = swapLabels ? previous : next; // Build the label order based on the configured hardware mapping. auto labelForHardware = [&](uint8_t hw) -> const char* { // Compare against configured logical roles and return the matching label. if (hw == SETTINGS.frontButtonBack) { return back; } if (hw == SETTINGS.frontButtonConfirm) { return confirm; } if (hw == SETTINGS.frontButtonLeft) { return leftLabel; } if (hw == SETTINGS.frontButtonRight) { return rightLabel; } return ""; }; return {labelForHardware(HalGPIO::BTN_BACK), labelForHardware(HalGPIO::BTN_CONFIRM), labelForHardware(HalGPIO::BTN_LEFT), labelForHardware(HalGPIO::BTN_RIGHT)}; } int MappedInputManager::getPressedFrontButton() const { // Scan the raw front buttons in hardware order. // This bypasses remapping so the remap activity can capture physical presses. if (gpio.wasPressed(HalGPIO::BTN_BACK)) { return HalGPIO::BTN_BACK; } if (gpio.wasPressed(HalGPIO::BTN_CONFIRM)) { return HalGPIO::BTN_CONFIRM; } if (gpio.wasPressed(HalGPIO::BTN_LEFT)) { return HalGPIO::BTN_LEFT; } if (gpio.wasPressed(HalGPIO::BTN_RIGHT)) { return HalGPIO::BTN_RIGHT; } return -1; }