Change touch behavior

This commit is contained in:
j2 2026-09-26 10:17:55 -05:00
parent 918a1941d0
commit cee71cc445
7 changed files with 105 additions and 104 deletions

View File

@ -215,29 +215,39 @@ If the board is not detected or an upload repeatedly fails, force download mode:
| Control | Reading view | Menu or picker |
| --- | --- | --- |
| BOOT / GPIO0 | Open menu | Close/cancel without saving |
| Touch edge Home button | Play or pause | Open highlighted option / apply |
| BOOT / GPIO0 | Play or pause | Select / confirm |
| Touchscreen | Open menu | No action |
| Upper-right / GPIO12 | Previous paragraph or section | Move selection backward |
| Lower-right / GPIO16 | Next paragraph or section | Move selection forward |
The touch-edge Home button has no action.
Do not hold BOOT while pressing RESET during normal use; that combination is
the manual download mode used for recovery above.
The menu opens with **Back** selected. BOOT on Back returns to reading. Pickers
start at the current setting and move forward when confirmed. The Font option
selects style first, then size; the size preview displays the word “size” in
the selected font and size. The selected font style also applies to the status,
footer, menus, and picker labels.
The default is 300 WPM. While paused, the backlight dims after 20 seconds,
switches off after one minute, and the ESP32 deep-sleeps after ten minutes.
GPIO12 wakes it.
The footer shows the chapter title, its fixed total word count, and reading
progress as a rounded percentage.
## Resume and power behavior
The reader saves the selected section, paragraph-level position, WPM,
brightness, and font settings in the ESP32's nonvolatile storage. On the next
boot or wake it opens the same section's title card and remains paused;
pressing the touch-edge Home button begins at the saved paragraph. It opens the
pressing BOOT begins at the saved paragraph. It opens the
saved section directly, then falls back to the first TOC-listed section if the
card has changed.
When continuous reading reaches the end of a section, it opens the next
section's title card and pauses. Press the touch-edge Home button to begin that
section's title card and pauses. Press BOOT to begin that
section.
The physical power switch only disconnects the battery. USB power is sufficient

View File

@ -12,6 +12,11 @@ Add selectable reading profiles for punctuation and paragraph pause lengths,
long-word delay, unfamiliar-word timing, and WPM presets. Keep a plain/default
profile that preserves the current cadence.
Replace fixed 2×/3× sidecar word-delay values with categorical timing tags.
Map each category to a configurable delay in reader settings, so changing a
delay does not require regenerating sidecars. Version the format change and
keep existing v1/v2 sidecars readable.
## Dialogue and character colors
Add optional color rendering for dialogue and character names. The converter

View File

@ -9,7 +9,7 @@ class Board {
bool begin();
bool beginSd();
void setReadingContext(const String& bookTitle, const String& chapterTitle,
uint8_t progressPercent);
uint32_t wordCount, uint8_t progressPercent);
void drawWord(const String& word, uint16_t wpm, uint8_t styleFlags = 0);
void drawChapterTitle(const String& chapter, uint16_t wpm);
void drawTheEnd(uint16_t wpm);
@ -35,6 +35,7 @@ class Board {
int batteryPercent() const;
void setLandscape();
void setReaderFont(uint8_t style, uint8_t size, uint8_t styleFlags = 0);
void setUiFont(uint8_t style);
void drawStatus(uint16_t wpm);
void drawFooter();
String ellipsize(const String& text, int16_t maximumWidth) const;
@ -54,6 +55,8 @@ class Board {
String bookTitle_;
String chapterTitle_;
String displayedChapterTitle_;
uint32_t wordCount_ = 0;
uint32_t displayedWordCount_ = 0;
uint8_t progressPercent_ = 0;
DrawBounds previousWordBounds_;
};

View File

@ -26,6 +26,7 @@ class Reader {
bool nextParagraph();
bool lastParagraph();
uint32_t currentIndex() const;
uint32_t wordCount() const;
uint8_t progressPercent() const;
uint32_t delayMs(uint16_t wpm) const;
uint8_t currentStyleFlags() const;

View File

@ -51,11 +51,6 @@ const GFXfont* const kReaderFonts[3][4][4] = {
&FreeMonoBoldOblique24pt7b},
},
};
String fontSizeLabel(uint8_t size) {
const uint8_t whole = size / 10;
return size % 10 == 0 ? String(whole) : String(whole) + ".5";
}
}
bool Board::begin() {
@ -75,7 +70,7 @@ bool Board::begin() {
// manager on the shared I2C bus.
powerMonitorAvailable_ = powerMonitor.init(
Wire, config::kI2cSda, config::kI2cScl, SY6970_SLAVE_ADDRESS);
if (powerMonitorAvailable_) powerMonitor.enableADCMeasure();
if (powerMonitorAvailable_) powerMonitor.enableMeasure();
return true;
}
@ -104,7 +99,7 @@ int Board::batteryPercent() const {
}
void Board::setReadingContext(const String& bookTitle, const String& chapterTitle,
uint8_t progressPercent) {
uint32_t wordCount, uint8_t progressPercent) {
if (bookTitle_ != bookTitle) {
bookTitle_ = bookTitle;
statusDrawn_ = false;
@ -116,6 +111,7 @@ void Board::setReadingContext(const String& bookTitle, const String& chapterTitl
chapterTitle_ = chapterTitle;
footerDrawn_ = false;
}
wordCount_ = wordCount;
progressPercent_ = progressPercent;
}
@ -165,25 +161,28 @@ void Board::setReaderFont(uint8_t style, uint8_t size, uint8_t styleFlags) {
display_->setTextSize(scale);
}
void Board::setUiFont(uint8_t style) {
setReaderFont(style, config::kMinimumFontSize);
}
void Board::drawStatus(uint16_t wpm) {
constexpr int16_t kMargin = 8;
constexpr int16_t kBatteryWidth = 28;
constexpr int16_t kBatteryHeight = 12;
constexpr int16_t kBatteryTipWidth = 3;
display_->setTextFont(2);
display_->setTextSize(1);
setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE);
display_->setTextDatum(TL_DATUM);
display_->setTextDatum(ML_DATUM);
const String wpmLabel = String(wpm) + " WPM";
display_->drawString(wpmLabel, kMargin, kMargin);
display_->drawString(wpmLabel, kMargin, kStatusHeight / 2);
const int16_t batteryX = display_->width() - kMargin - kBatteryTipWidth - kBatteryWidth;
const int16_t batteryY = kMargin + 2;
const int percentage = batteryPercent();
const String label = percentage < 0 ? "--" : String(percentage) + "%";
display_->setTextDatum(TR_DATUM);
display_->drawString(label, batteryX - 5, kMargin);
display_->setTextDatum(MR_DATUM);
display_->drawString(label, batteryX - 5, kStatusHeight / 2);
display_->drawRoundRect(batteryX, batteryY, kBatteryWidth, kBatteryHeight, 2, TFT_WHITE);
display_->fillRect(batteryX + kBatteryWidth, batteryY + 4, kBatteryTipWidth, 4, TFT_WHITE);
if (percentage > 0) {
@ -197,8 +196,9 @@ void Board::drawStatus(uint16_t wpm) {
const int16_t maximumWidth =
2 * min(centerX - left, right - centerX);
if (maximumWidth > 0 && !bookTitle_.isEmpty()) {
display_->setTextDatum(TC_DATUM);
display_->drawString(ellipsize(bookTitle_, maximumWidth), centerX, kMargin);
display_->setTextDatum(MC_DATUM);
display_->drawString(ellipsize(bookTitle_, maximumWidth), centerX,
kStatusHeight / 2);
}
}
@ -216,25 +216,28 @@ String Board::ellipsize(const String& text, int16_t maximumWidth) const {
void Board::drawFooter() {
if (footerDrawn_ && displayedChapterTitle_ == chapterTitle_ &&
displayedWordCount_ == wordCount_ &&
displayedProgressPercent_ == progressPercent_) {
return;
}
display_->fillRect(0, display_->height() - kFooterHeight, display_->width(),
kFooterHeight, TFT_BLACK);
display_->setTextFont(2);
display_->setTextSize(1);
setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE);
const String progress = String(progressPercent_) + "%";
const String details = String(wordCount_) + " words - " +
String(progressPercent_) + "%";
const String separator = " - ";
const int16_t maximumChapterWidth =
display_->width() - display_->textWidth(progress) - 16;
display_->width() - display_->textWidth(separator + details) - 16;
const String chapter = ellipsize(chapterTitle_, maximumChapterWidth);
const String label = chapter.isEmpty() ? progress : chapter + " " + progress;
display_->setTextDatum(TC_DATUM);
const String label = chapter.isEmpty() ? details : chapter + separator + details;
display_->setTextDatum(MC_DATUM);
display_->drawString(label, display_->width() / 2,
display_->height() - kFooterHeight + 5);
display_->height() - kFooterHeight / 2);
footerDrawn_ = true;
displayedChapterTitle_ = chapterTitle_;
displayedWordCount_ = wordCount_;
displayedProgressPercent_ = progressPercent_;
}
@ -317,8 +320,7 @@ void Board::drawMenu(uint8_t selectedOption) {
footerDrawn_ = false;
displayedProgressPercent_ = 255;
previousWordBounds_.valid = false;
display_->setTextFont(2);
display_->setTextSize(1);
setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE);
display_->setTextDatum(TC_DATUM);
display_->drawString("MENU", display_->width() / 2, 18);
@ -326,8 +328,8 @@ void Board::drawMenu(uint8_t selectedOption) {
constexpr int16_t kRowHeight = 58;
constexpr int16_t kFirstRowY = 60;
constexpr int16_t kRowSpacing = 68;
const char* labels[] = {"Book selection", "Chapter selection", "Brightness",
"Font style", "Font size"};
const char* labels[] = {"Back", "Book selection", "Chapter selection",
"Brightness", "Font"};
display_->setTextDatum(ML_DATUM);
for (uint8_t index = 0; index < 5; ++index) {
const int16_t y = kFirstRowY + index * kRowSpacing;
@ -341,13 +343,11 @@ void Board::drawMenu(uint8_t selectedOption) {
display_->drawString(labels[index], 24, y + kRowHeight / 2);
}
display_->setTextColor(TFT_WHITE);
display_->setTextFont(1);
display_->setTextSize(1);
display_->setTextDatum(BC_DATUM);
display_->drawString("BACK / FORWARD to choose", display_->width() / 2,
display_->height() - 30);
display_->drawString("HOME open / BOOT close", display_->width() / 2,
display_->height() - 12);
display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36);
display_->drawString("BOOT select", display_->width() / 2,
display_->height() - 8);
}
void Board::drawPicker(const String& title, const String& value) {
@ -357,27 +357,26 @@ void Board::drawPicker(const String& title, const String& value) {
display_->fillScreen(TFT_BLACK);
statusDrawn_ = false;
previousWordBounds_.valid = false;
display_->setTextFont(2);
display_->setTextSize(1);
setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE);
display_->setTextDatum(TC_DATUM);
display_->drawString(title, display_->width() / 2, 16);
display_->setTextFont(1);
const size_t glyphs = value.length() == 0 ? 1 : value.length();
uint8_t scale = display_->width() / (glyphs * 7);
if (scale < 2) scale = 2;
if (scale > 6) scale = 6;
uint8_t scale = 3;
display_->setTextSize(scale);
while (scale > 1 && display_->textWidth(value) > display_->width() - 16) {
display_->setTextSize(--scale);
}
display_->setTextDatum(MC_DATUM);
display_->drawString(value, display_->width() / 2, display_->height() / 2 - 8);
display_->drawString(ellipsize(value, display_->width() - 16),
display_->width() / 2, display_->height() / 2 - 8);
display_->setTextSize(1);
setUiFont(fontStyle_);
display_->setTextDatum(BC_DATUM);
display_->drawString("BACK / FORWARD to choose", display_->width() / 2,
display_->height() - 30);
display_->drawString("HOME apply / BOOT cancel", display_->width() / 2,
display_->height() - 12);
display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36);
display_->drawString("BOOT apply", display_->width() / 2,
display_->height() - 8);
}
void Board::drawFontStylePicker(uint8_t style, uint8_t size) {
@ -387,8 +386,7 @@ void Board::drawFontStylePicker(uint8_t style, uint8_t size) {
display_->fillScreen(TFT_BLACK);
statusDrawn_ = false;
previousWordBounds_.valid = false;
display_->setTextFont(2);
display_->setTextSize(1);
setUiFont(style);
display_->setTextColor(TFT_WHITE);
display_->setTextDatum(TC_DATUM);
display_->drawString("FONT STYLE", display_->width() / 2, 16);
@ -398,16 +396,14 @@ void Board::drawFontStylePicker(uint8_t style, uint8_t size) {
display_->setTextDatum(MC_DATUM);
display_->drawString("Sample", display_->width() / 2, display_->height() / 2 - 8);
display_->setTextFont(2);
display_->setTextSize(1);
setUiFont(style);
display_->setTextDatum(TC_DATUM);
display_->drawString(names[constrain(style, 0, 2)], display_->width() / 2, 48);
display_->setTextFont(1);
display_->setTextDatum(BC_DATUM);
display_->drawString("BACK / FORWARD to choose", display_->width() / 2,
display_->height() - 30);
display_->drawString("HOME apply / BOOT cancel", display_->width() / 2,
display_->height() - 12);
display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36);
display_->drawString("BOOT next", display_->width() / 2,
display_->height() - 8);
}
void Board::drawFontSizePicker(uint8_t style, uint8_t size) {
@ -417,24 +413,22 @@ void Board::drawFontSizePicker(uint8_t style, uint8_t size) {
display_->fillScreen(TFT_BLACK);
statusDrawn_ = false;
previousWordBounds_.valid = false;
display_->setTextFont(2);
display_->setTextSize(1);
setUiFont(style);
display_->setTextColor(TFT_WHITE);
display_->setTextDatum(TC_DATUM);
display_->drawString("FONT SIZE", display_->width() / 2, 16);
setReaderFont(style, size);
display_->setTextDatum(MC_DATUM);
display_->drawString(fontSizeLabel(size), display_->width() / 2,
display_->drawString("size", display_->width() / 2,
display_->height() / 2 - 8);
display_->setTextFont(1);
display_->setTextSize(1);
setUiFont(style);
display_->setTextDatum(BC_DATUM);
display_->drawString("BACK / FORWARD to choose", display_->width() / 2,
display_->height() - 30);
display_->drawString("HOME apply / BOOT cancel", display_->width() / 2,
display_->height() - 12);
display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36);
display_->drawString("BOOT apply", display_->width() / 2,
display_->height() - 8);
}
void Board::setBrightness(uint8_t level) {

View File

@ -280,6 +280,10 @@ bool Reader::lastParagraph() {
uint32_t Reader::currentIndex() const { return index_; }
uint32_t Reader::wordCount() const {
return static_cast<uint32_t>(tokens_.size());
}
uint8_t Reader::progressPercent() const {
if (tokens_.empty()) return 0;
if (tokens_.size() == 1) return 100;

View File

@ -25,8 +25,7 @@ uint32_t lastInteractionMs = 0;
uint32_t nextWordMs = 0;
TouchDrvCSTXXX touch;
bool touchAvailable = false;
bool touchPlayPauseRequested = false;
uint32_t lastTouchHomeMs = 0;
bool screenWasTouched = false;
enum class Screen {
kReader,
@ -61,7 +60,7 @@ struct Button {
uint32_t changedAt = 0;
};
Button menu{config::kButtonPlayPause};
Button boot{config::kButtonPlayPause};
Button previous{config::kButtonPrevious};
Button next{config::kButtonNext};
@ -92,7 +91,8 @@ void persistState() {
void setReadingContext(uint8_t progressPercent) {
board.setReadingContext(library.bookName(state.bookPath),
library.chapterName(state.bookPath), progressPercent);
library.chapterName(state.bookPath), reader.wordCount(),
progressPercent);
}
void showCurrentWord() {
@ -143,12 +143,6 @@ void returnToReader() {
showReaderScreen();
}
void closeMenuWithoutSaving() {
// Brightness is the only picker that changes hardware before confirmation.
board.setPreferredBrightness(state.brightness);
returnToReader();
}
void drawPicker() {
switch (screen) {
case Screen::kBookPicker:
@ -222,11 +216,15 @@ void changePicker(int direction) {
}
void applyPicker() {
if (screen == Screen::kFontStylePicker) {
screen = Screen::kFontSizePicker;
drawPicker();
return;
}
if (screen == Screen::kBrightnessPicker) {
state.brightness = pendingBrightness;
board.setPreferredBrightness(state.brightness);
} else if (screen == Screen::kFontStylePicker ||
screen == Screen::kFontSizePicker) {
} else if (screen == Screen::kFontSizePicker) {
state.fontStyle = pendingFontStyle;
state.fontSize = pendingFontSize;
board.setFont(state.fontStyle, state.fontSize);
@ -322,13 +320,6 @@ void navigateNextPaused() {
if (positionChanged) persistState();
}
void onTouchHome(void*) {
constexpr uint32_t kTouchDebounceMs = 200;
if (millis() - lastTouchHomeMs < kTouchDebounceMs) return;
lastTouchHomeMs = millis();
touchPlayPauseRequested = true;
}
void confirmMenuSelection() {
if (screen == Screen::kReader) {
togglePlayback();
@ -336,11 +327,11 @@ void confirmMenuSelection() {
}
if (screen == Screen::kMenu) {
switch (menuSelection) {
case 0: openPicker(Screen::kBookPicker); break;
case 1: openPicker(Screen::kChapterPicker); break;
case 2: openPicker(Screen::kBrightnessPicker); break;
case 3: openPicker(Screen::kFontStylePicker); break;
case 4: openPicker(Screen::kFontSizePicker); break;
case 0: returnToReader(); break;
case 1: openPicker(Screen::kBookPicker); break;
case 2: openPicker(Screen::kChapterPicker); break;
case 3: openPicker(Screen::kBrightnessPicker); break;
case 4: openPicker(Screen::kFontStylePicker); break;
}
return;
}
@ -355,7 +346,6 @@ void beginTouch() {
if (!touchAvailable) return;
configureTouchLandscape();
touch.setHomeButtonCallback(onTouchHome);
}
void configureTouchLandscape() {
@ -368,11 +358,9 @@ void configureTouchLandscape() {
void handleTouch() {
if (!touchAvailable) return;
touch.getTouchPoints(); // Polling delivers home-button callbacks.
if (touchPlayPauseRequested) {
touchPlayPauseRequested = false;
confirmMenuSelection();
}
const bool screenTouched = touch.getTouchPoints().hasPoints();
if (screenTouched && !screenWasTouched && screen == Screen::kReader) openMenu();
screenWasTouched = screenTouched;
}
void wakeDisplay() {
@ -394,12 +382,8 @@ void enterDeepSleep() {
}
void handleInput() {
if (released(menu)) {
if (screen == Screen::kReader) {
openMenu();
} else {
closeMenuWithoutSaving();
}
if (released(boot)) {
confirmMenuSelection();
return;
}