Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
12 kB · 281 lines
C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282#pragma once
#include <CrossPointSettings.h>#include <GfxRenderer.h>#include <HalGPIO.h>#include <HalTiltSensor.h>#include <Logging.h>
#include <cctype>#include <string_view>
#include "MappedInputManager.h"#include "activities/ActivityManager.h"
namespace ReaderUtils {
constexpr unsigned long GO_HOME_MS = 1000;constexpr unsigned long GO_BACK_OR_HOME_MS = GO_HOME_MS;constexpr unsigned long SKIP_HOLD_MS = 700;constexpr unsigned long BOOKMARK_HOLD_MS = 400;constexpr unsigned long BOOKMARK_MESSAGE_DURATION_MS = 2500;
inline bool gestureAllowsSwipe(const uint8_t gesture) { return gesture == CrossPointSettings::TAP_AND_SWIPE || gesture == CrossPointSettings::SWIPE_ONLY;}
inline bool gestureAllowsTap(const uint8_t gesture) { return gesture == CrossPointSettings::TAP_AND_SWIPE || gesture == CrossPointSettings::TAP_ONLY || gesture == CrossPointSettings::INVERTED_TAP;}
inline bool isRtlBookLanguage(std::string_view tag) { if (tag.size() < 2 || (tag.size() > 2 && tag[2] != '-' && tag[2] != '_')) return false; const auto first = std::tolower(static_cast<unsigned char>(tag[0])); const auto second = std::tolower(static_cast<unsigned char>(tag[1])); return (first == 'h' && second == 'e') || (first == 'i' && second == 'w') || (first == 'a' && second == 'r') || (first == 'f' && second == 'a');}
inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) { switch (orientation) { case CrossPointSettings::ORIENTATION::PORTRAIT: renderer.setOrientation(GfxRenderer::Orientation::Portrait); break; case CrossPointSettings::ORIENTATION::LANDSCAPE_CW: renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); break; case CrossPointSettings::ORIENTATION::INVERTED: renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted); break; case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW: renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise); break; default: break; }}
struct PageTurnResult { bool prev; bool next; bool fromTilt;};
inline PageTurnResult detectPageTurn(const MappedInputManager& input) { const bool usePress = SETTINGS.longPressButtonBehavior == SETTINGS.OFF; const bool tiltNext = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedForward(); const bool tiltPrev = SETTINGS.tiltPageTurn && halTiltSensor.wasTiltedBack(); const bool swapFront = input.isNavDirectionSwapped(); const auto prevButton = swapFront ? MappedInputManager::Button::Right : MappedInputManager::Button::Left; const auto nextButton = swapFront ? MappedInputManager::Button::Left : MappedInputManager::Button::Right; const auto pageButtonTriggered = [&](const MappedInputManager::Button button) { if (usePress) return input.wasPressed(button); return input.wasLongPressed(button, SKIP_HOLD_MS) || input.wasReleased(button); }; const bool prev = tiltPrev || (pageButtonTriggered(MappedInputManager::Button::PageBack) || pageButtonTriggered(prevButton)); const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && input.wasReleased(MappedInputManager::Button::Power); const bool next = input.homeButtonAction() == HomeButtonAction::NextPage || tiltNext || pageButtonTriggered(MappedInputManager::Button::PageForward) || powerTurn || pageButtonTriggered(nextButton); return {prev, next, tiltPrev || tiltNext};}
struct TouchPageTurn { bool prev; bool next; unsigned long heldMs;};
inline TouchPageTurn detectTouchPageTurn(const GfxRenderer& renderer, const MappedInputManager& input, const bool rtlBook = false) { TouchPageTurn result{false, false, 0}; if (!SETTINGS.touchReaderControls || !input.hasTouch()) { return result; }
// A slow swipe never becomes a long-press chapter skip. const auto dir = input.wasSwipe(); if (dir != MappedInputManager::SwipeDir::None) { result.next = dir == (rtlBook ? MappedInputManager::SwipeDir::Right : MappedInputManager::SwipeDir::Left) && gestureAllowsSwipe(SETTINGS.pageTurnGesture); result.prev = dir == (rtlBook ? MappedInputManager::SwipeDir::Left : MappedInputManager::SwipeDir::Right) && gestureAllowsSwipe(SETTINGS.previousPageGesture); return result; }
const bool nextTaps = gestureAllowsTap(SETTINGS.pageTurnGesture); const bool prevTaps = gestureAllowsTap(SETTINGS.previousPageGesture); if (!nextTaps && !prevTaps) { return result; }
int x = 0; int y = 0; if (!input.wasScreenTapped(x, y)) { return result; }
const int width = renderer.getScreenWidth(); const int height = renderer.getScreenHeight(); // The centered reader-menu tap target (isTouchMenuTap below) keeps priority // over the page-turn zones. if (SETTINGS.showReaderMenu == CrossPointSettings::READER_MENU_TAP && x >= width / 3 && x < width - width / 3 && y >= height / 3 && y < height - height / 3) { return result; }
// Give the whole page to the sole tap-enabled direction. When both accept // taps, split at the left third. RTL books and Inverted Tap each reverse // the shared zones. const bool inverted = (SETTINGS.pageTurnGesture == CrossPointSettings::INVERTED_TAP || SETTINGS.previousPageGesture == CrossPointSettings::INVERTED_TAP) != rtlBook; const bool nextZone = inverted ? x < (width * 2) / 3 : x >= width / 3; result.next = nextTaps && (!prevTaps || nextZone); result.prev = prevTaps && (!nextTaps || !nextZone); result.heldMs = gpio.lastTouchHeldMs(); return result;}
// Tap in the center third of the screen: the tap path into the reader menu on// every touch board. detectTouchPageTurn() excludes this centered rectangle,// so it remains free in tap mode. The Off/Swipe Up// alternatives are only surfaced on home-key boards (SettingsList), where the// menu stays reachable through the key's long-press function.inline bool isTouchMenuTap(const GfxRenderer& renderer, const MappedInputManager& input) { if (!input.hasTouch()) return false; if (SETTINGS.showReaderMenu != CrossPointSettings::READER_MENU_TAP) return false; int x = 0; int y = 0; if (!input.wasScreenTapped(x, y)) return false; const int width = renderer.getScreenWidth(); const int height = renderer.getScreenHeight(); const int zoneWidth = width / 3; const int zoneHeight = height / 3; return x >= zoneWidth && x < width - zoneWidth && y >= zoneHeight && y < height - zoneHeight;}
// Reader menu opens on the menu edge-swipe or a center-third tap. Home-key// actions are configured separately from screen gestures.// Menu gestures honor showReaderMenu independently of touchReaderControls,// which only gates page-turn touch zones in detectTouchPageTurn().inline bool isTouchMenuGesture(const GfxRenderer& renderer, const MappedInputManager& input) { if (!input.hasTouch()) return false; if (input.wasMenuGesture()) return true; // Bottom-edge up-swipe variant: only selectable on home-key boards, where // Home is the capacitive key and the bottom edge is otherwise unused. if (SETTINGS.showReaderMenu == CrossPointSettings::READER_MENU_SWIPE_UP && input.wasReaderMenuSwipeUp()) { return true; } return isTouchMenuTap(renderer, input);}
// One helper, blocking or deferred: the async form starts the refresh and// returns so the caller can overlap CPU work with the panel's refresh time.// Async callers must not touch the framebuffer until// renderer.waitRefreshComplete() and must rebuild the differential baseline// before the next page turn (the tiled grayscale cleanup does).inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh, bool async = false) { const auto mode = (pagesUntilFullRefresh <= 1) ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH; if (async) { renderer.displayBufferAsync(mode); } else { renderer.displayBuffer(mode); } if (pagesUntilFullRefresh <= 1) { pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { pagesUntilFullRefresh--; }}
// Display the B/W base of a page whose grayscale pass follows. Panels that// combine the base (Paper Mono) defer the activation so base + gray planes go// out as one waveform — displaying the base separately makes the gray pass// re-drive the whole text body (a visible flash). Other panels display// normally. Same refresh-cadence bookkeeping as displayWithRefreshCycle.inline void displayBaseWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) { if (renderer.grayscaleCapabilities().base != HalDisplay::GrayscaleBase::Combined) { displayWithRefreshCycle(renderer, pagesUntilFullRefresh); return; } const auto mode = (pagesUntilFullRefresh <= 1) ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH; renderer.displayGrayscaleBase(mode); if (pagesUntilFullRefresh <= 1) { pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { pagesUntilFullRefresh--; }}
// Grayscale anti-aliasing pass. Renders content twice (LSB + MSB) to build// the grayscale buffer. Only the content callback is re-rendered — status bars// and other overlays should be drawn before calling this.// Kept as a template to avoid std::function overhead; instantiated once per reader type.template <typename RenderFn>void renderAntiAliased(GfxRenderer& renderer, RenderFn&& renderFn) { if (!renderer.storeBwBuffer()) { LOG_ERR("READER", "Failed to store BW buffer for anti-aliasing"); // A combined-base panel may still hold a deferred B/W activation; flush it // so the page reaches the panel even without its grays. if (renderer.grayscaleCapabilities().base == HalDisplay::GrayscaleBase::Combined) renderer.cleanupGrayscaleWithFrameBuffer(); return; }
renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); renderFn(); renderer.copyGrayscaleLsbBuffers();
renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); renderFn(); renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer(); renderer.setRenderMode(GfxRenderer::BW);
renderer.restoreBwBuffer();}
struct BackNavCallback { void* ctx; void (*fn)(void*);};
// Returns true if the back button was consumed (caller should return).// Long press (>= GO_BACK_OR_HOME_MS):// - default: go to file browser// - with backShortToFileBrowser: go home// Short press (< GO_BACK_OR_HOME_MS):// - default: go home// - with backShortToFileBrowser: go to file browser.inline bool handleBackNavigation(const MappedInputManager& mappedInput, ActivityManager& activityManager, const char* filePath, BackNavCallback goHome) { // The reading surface deliberately has no left-edge swipe-to-exit path: in // swipe page-turn mode a right swipe must page back instead. Home remains // available through the board's dedicated Home gesture/key. Back swipes stay // available in menus and other activities; only this reader-surface handler // ignores them. Physical Back buttons are unaffected: isPressed() is // button-only, and this guard skips just the gesture's own release frame. if (mappedInput.wasBackGesture()) { return false; }
const bool backTriggered = mappedInput.wasLongPressed(MappedInputManager::Button::Back, GO_BACK_OR_HOME_MS) || mappedInput.wasReleased(MappedInputManager::Button::Back); if (!backTriggered) return false;
const bool longPress = mappedInput.getHeldTime() >= GO_BACK_OR_HOME_MS; if (longPress != SETTINGS.backShortToFileBrowser) { activityManager.goToFileBrowser(filePath); } else { goHome.fn(goHome.ctx); } return true;}
} // namespace ReaderUtils