Add touch gesture support and LilyGo T5 S3 board
Implements bottom-edge swipe-up gesture detection and delayed touch-select with tracking state. Adds isTouchTapCandidate() to distinguish taps from swipes. Includes LilyGo T5 S3 board configuration with USB CDC logging transport for boards where Serial operator bool reports false incorrectly.
This commit is contained in:
@@ -63,8 +63,25 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
|
||||
// Top-left corner fallback, as a fraction of the logical screen. Generous to hit.
|
||||
static constexpr float BACK_GESTURE_FRAC_X = 0.22f;
|
||||
static constexpr float BACK_GESTURE_FRAC_Y = 0.12f;
|
||||
static constexpr float BOTTOM_EDGE_BACK_GESTURE_FRAC_Y = 0.14f;
|
||||
static constexpr unsigned long TOUCH_DOWN_SELECT_DELAY_MS = 90;
|
||||
|
||||
bool MappedInputManager::wasBottomEdgeSwipeUp() const {
|
||||
float nxs = 0.0f, nys = 0.0f, nxe = 0.0f, nye = 0.0f;
|
||||
if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return false;
|
||||
|
||||
int sx = 0, sy = 0, ex = 0, ey = 0;
|
||||
renderer.tapToLogical(nxs, nys, sx, sy);
|
||||
renderer.tapToLogical(nxe, nye, ex, ey);
|
||||
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
const int bottomEdgeTop = screenHeight - static_cast<int>(screenHeight * BOTTOM_EDGE_BACK_GESTURE_FRAC_Y);
|
||||
return sy >= bottomEdgeTop && ey < sy && std::abs(ey - sy) > std::abs(ex - sx);
|
||||
}
|
||||
|
||||
bool MappedInputManager::wasBackGesture() const {
|
||||
if (wasBottomEdgeSwipeUp()) return true;
|
||||
|
||||
float nx = 0.0f, ny = 0.0f;
|
||||
if (!gpio.wasTouchTap(nx, ny)) return false;
|
||||
int lx = 0, ly = 0;
|
||||
@@ -87,10 +104,36 @@ bool MappedInputManager::wasItemTapped(int& id) const {
|
||||
|
||||
bool MappedInputManager::wasItemTouchedDown(int& id) const {
|
||||
float nx = 0.0f, ny = 0.0f;
|
||||
if (!gpio.wasTouchDown(nx, ny)) return false;
|
||||
int lx = 0, ly = 0;
|
||||
renderer.tapToLogical(nx, ny, lx, ly);
|
||||
return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, id);
|
||||
unsigned long heldMs = 0;
|
||||
if (!gpio.isTouchTapCandidate(nx, ny, heldMs)) {
|
||||
touchSelectTracking = false;
|
||||
touchSelectEmitted = false;
|
||||
touchSelectId = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!touchSelectTracking) {
|
||||
float downNx = 0.0f, downNy = 0.0f;
|
||||
if (!gpio.wasTouchDown(downNx, downNy)) return false;
|
||||
|
||||
int lx = 0, ly = 0;
|
||||
renderer.tapToLogical(downNx, downNy, lx, ly);
|
||||
int candidateId = -1;
|
||||
if (!TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Item, candidateId)) {
|
||||
touchSelectTracking = false;
|
||||
touchSelectEmitted = false;
|
||||
touchSelectId = -1;
|
||||
return false;
|
||||
}
|
||||
touchSelectTracking = true;
|
||||
touchSelectEmitted = false;
|
||||
touchSelectId = candidateId;
|
||||
}
|
||||
|
||||
if (touchSelectEmitted || heldMs < TOUCH_DOWN_SELECT_DELAY_MS) return false;
|
||||
touchSelectEmitted = true;
|
||||
id = touchSelectId;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MappedInputManager::wasItemLongPressed(int& id) const {
|
||||
@@ -122,6 +165,7 @@ bool MappedInputManager::wasCoverTapped(int& id) const {
|
||||
bool MappedInputManager::wasListScroll(int& index, int count, int pageItems) const {
|
||||
if (count <= 0) return false;
|
||||
if (pageItems < 1) pageItems = 1;
|
||||
if (wasBottomEdgeSwipeUp()) return false;
|
||||
const SwipeDir swipe = wasSwipe();
|
||||
if (swipe == SwipeDir::Up) {
|
||||
index = std::min(index + pageItems, count - 1);
|
||||
@@ -135,6 +179,8 @@ bool MappedInputManager::wasListScroll(int& index, int count, int pageItems) con
|
||||
}
|
||||
|
||||
MappedInputManager::SwipeDir MappedInputManager::wasSwipe() const {
|
||||
if (wasBottomEdgeSwipeUp()) return SwipeDir::None;
|
||||
|
||||
float nxs = 0.0f, nys = 0.0f, nxe = 0.0f, nye = 0.0f;
|
||||
if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return SwipeDir::None;
|
||||
// Map both endpoints into the logical frame so the direction follows what the
|
||||
|
||||
@@ -23,13 +23,14 @@ class MappedInputManager {
|
||||
bool wasReleased(Button button) const;
|
||||
bool isPressed(Button button) const;
|
||||
// Touch "back" gesture: a tap on the theme's header Back target, or in the
|
||||
// top-left corner. Folded into Back's edges, so every screen gets it for free.
|
||||
// top-left corner, or a swipe up from the visible bottom edge. Folded into
|
||||
// Back's edges, so every screen gets it for free.
|
||||
bool wasBackGesture() const;
|
||||
// True (and writes the id) if a tap this frame hit a TouchRegistry item.
|
||||
// Activities treat the id as "select + activate". False on non-touch devices.
|
||||
bool wasItemTapped(int& id) const;
|
||||
// Press-edge of wasItemTapped: fires on touch-DOWN over an item so the activity
|
||||
// can show it selected before release. Mirrors button nav (move, then confirm).
|
||||
// Stable touch-down candidate: fires once when a touch remains over an item
|
||||
// briefly without crossing tap slop, so swipes do not show row selection.
|
||||
bool wasItemTouchedDown(int& id) const;
|
||||
// Subset of wasItemTapped's releases held past the long-press threshold (check
|
||||
// this first). Distinguishes tap vs press-and-hold.
|
||||
@@ -58,4 +59,9 @@ class MappedInputManager {
|
||||
GfxRenderer& renderer;
|
||||
|
||||
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
|
||||
bool wasBottomEdgeSwipeUp() const;
|
||||
|
||||
mutable bool touchSelectTracking = false;
|
||||
mutable bool touchSelectEmitted = false;
|
||||
mutable int touchSelectId = -1;
|
||||
};
|
||||
|
||||
@@ -214,8 +214,6 @@ void FileBrowserActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Touch-down moves the selector to the pressed entry (shows selected state); release
|
||||
// opens it below.
|
||||
int downId = -1;
|
||||
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(files.size())) {
|
||||
selectorIndex = downId;
|
||||
|
||||
@@ -181,7 +181,7 @@ void HomeActivity::loop() {
|
||||
|
||||
// Tap a menu button to select + activate it. The button menu registers
|
||||
// menu-local ids (drawn with selectorIndex offset by recentBooks.size()), so map
|
||||
// back into the global selector space. Touch-down shows it selected; release opens.
|
||||
// back into the global selector space.
|
||||
int downId = -1;
|
||||
if (mappedInput.wasItemTouchedDown(downId)) {
|
||||
selectorIndex = static_cast<int>(recentBooks.size()) + downId;
|
||||
@@ -274,8 +274,7 @@ void HomeActivity::render(RenderLock&&) {
|
||||
const int menuY = metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset;
|
||||
const int bottomReserve = (BaseTheme::showButtonHints() ? metrics.buttonHintsHeight : 0) + metrics.verticalSpacing;
|
||||
GUI.drawButtonMenu(
|
||||
renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve},
|
||||
static_cast<int>(menuItems.size()),
|
||||
renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve}, static_cast<int>(menuItems.size()),
|
||||
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
|
||||
[&menuItems](int index) { return std::string(menuItems[index]); },
|
||||
[&menuIcons](int index) { return menuIcons[index]; });
|
||||
|
||||
@@ -417,7 +417,7 @@ void WifiSelectionActivity::loop() {
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
// Touch: down-select highlights the pressed network, tap selects it (like Confirm).
|
||||
// Touch: stable press highlights; tap selects it (like Confirm). Drag/scroll contacts do not preselect a row.
|
||||
int downId = -1;
|
||||
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(networks.size())) {
|
||||
selectedNetworkIndex = downId;
|
||||
|
||||
@@ -3,11 +3,39 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); }
|
||||
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub ? epub->getTocItemsCount() : 0; }
|
||||
|
||||
void EpubReaderChapterSelectionActivity::cancelAndFinish() {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionActivity::activateSelection(int index) {
|
||||
const int totalItems = getTotalItems();
|
||||
if (!epub || totalItems <= 0) {
|
||||
cancelAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
index = std::clamp(index, 0, totalItems - 1);
|
||||
const auto tocItem = epub->getTocItem(index);
|
||||
if (tocItem.spineIndex < 0 || tocItem.spineIndex >= epub->getSpineItemsCount()) {
|
||||
cancelAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
selectorIndex = index;
|
||||
setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor});
|
||||
finish();
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
@@ -30,6 +58,12 @@ void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); }
|
||||
void EpubReaderChapterSelectionActivity::loop() {
|
||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||
const int totalItems = getTotalItems();
|
||||
if (!epub || totalItems <= 0) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
cancelAndFinish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
|
||||
if (mappedInput.wasListScroll(selectorIndex, totalItems, pageItems)) {
|
||||
@@ -45,23 +79,17 @@ void EpubReaderChapterSelectionActivity::loop() {
|
||||
|
||||
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;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
} else {
|
||||
setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor});
|
||||
finish();
|
||||
}
|
||||
const bool validTap = tapped && tappedId >= 0 && tappedId < totalItems;
|
||||
const bool confirm = mappedInput.wasReleased(MappedInputManager::Button::Confirm);
|
||||
if (validTap) {
|
||||
selectorIndex = tappedId;
|
||||
}
|
||||
if (validTap || confirm) {
|
||||
activateSelection(selectorIndex);
|
||||
return;
|
||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
cancelAndFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
buttonNavigator.onNextRelease([this, totalItems] {
|
||||
@@ -98,10 +126,12 @@ void EpubReaderChapterSelectionActivity::render(RenderLock&&) {
|
||||
const int contentHeight = screen.height - contentTop - metrics.verticalSpacing;
|
||||
|
||||
const int totalItems = getTotalItems();
|
||||
GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, selectorIndex,
|
||||
const int displayIndex = totalItems > 0 ? std::clamp(selectorIndex, 0, totalItems - 1) : 0;
|
||||
GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, displayIndex,
|
||||
[this](int index) {
|
||||
auto item = epub->getTocItem(index);
|
||||
std::string indent((item.level - 1) * 2, ' ');
|
||||
const int level = item.level > 0 ? item.level - 1 : 0;
|
||||
std::string indent(level * 2, ' ');
|
||||
return indent + item.title;
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ class EpubReaderChapterSelectionActivity final : public Activity {
|
||||
|
||||
// Total TOC items count
|
||||
int getTotalItems() const;
|
||||
void cancelAndFinish();
|
||||
void activateSelection(int index);
|
||||
|
||||
public:
|
||||
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
|
||||
@@ -64,8 +64,6 @@ void EpubReaderMenuActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Touch-down moves the selection to the pressed item (shows selected state), like
|
||||
// moving with Up/Down; release activates it below.
|
||||
int downId = -1;
|
||||
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < static_cast<int>(menuItems.size())) {
|
||||
selectedIndex = downId;
|
||||
|
||||
@@ -133,15 +133,14 @@ void SettingsActivity::loop() {
|
||||
} else if (swipe == MappedInputManager::SwipeDir::Up || swipe == MappedInputManager::SwipeDir::Down) {
|
||||
// Vertical swipe page-scrolls the list (touch nav without the side buttons).
|
||||
// count is settingsCount + 1 because row 0 is the category tab bar.
|
||||
const int page = std::max(
|
||||
1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false));
|
||||
const int page =
|
||||
std::max(1, UITheme::getNumberOfItemsPerPage(renderer, true, true, BaseTheme::showButtonHints(), false));
|
||||
mappedInput.wasListScroll(selectedSettingIndex, settingsCount + 1, page);
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Tap a settings row to select + activate it. Row 0 is the tab bar, so the list
|
||||
// is drawn at selectedSettingIndex - 1; map the tapped row back by +1. Touch-down
|
||||
// shows it selected; release toggles/activates below.
|
||||
// is drawn at selectedSettingIndex - 1; map the tapped row back by +1.
|
||||
int downId = -1;
|
||||
if (!swiped && mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
|
||||
selectedSettingIndex = downId + 1;
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
#include <HalStorage.h>
|
||||
#include <HalSystem.h>
|
||||
#include <HalTiltSensor.h>
|
||||
|
||||
#if defined(FREEINK_DEVICE_LILYGO) && FREEINK_DEVICE_LILYGO
|
||||
#include <BoardT5S3.h>
|
||||
#endif
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <SPI.h>
|
||||
@@ -367,6 +371,12 @@ void setup() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(FREEINK_DEVICE_LILYGO) && FREEINK_DEVICE_LILYGO
|
||||
// LilyGo T5 S3 board-support owns the shared I2C setup plus non-reader
|
||||
// peripherals that can otherwise contend with SD/display pins.
|
||||
BoardT5S3::begin();
|
||||
#endif
|
||||
|
||||
HalSystem::begin();
|
||||
|
||||
// Read-and-clear so a panic later in setup() doesn't loop into silent reboot.
|
||||
|
||||
Reference in New Issue
Block a user