From cee71cc445a5423355e256f7b1172bbbf40d4a4a Mon Sep 17 00:00:00 2001 From: j2 Date: Sat, 26 Sep 2026 10:17:55 -0500 Subject: [PATCH] Change touch behavior --- README.md | 18 +++++-- TODO.md | 5 ++ include/Board.h | 5 +- include/Reader.h | 1 + src/Board.cpp | 120 ++++++++++++++++++++++------------------------- src/Reader.cpp | 4 ++ src/main.cpp | 56 ++++++++-------------- 7 files changed, 105 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index d2a51b8..85b0979 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/TODO.md b/TODO.md index 4561901..18be71f 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/include/Board.h b/include/Board.h index c92f560..6bcb01d 100644 --- a/include/Board.h +++ b/include/Board.h @@ -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_; }; diff --git a/include/Reader.h b/include/Reader.h index c8778f2..fbacc24 100644 --- a/include/Reader.h +++ b/include/Reader.h @@ -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; diff --git a/src/Board.cpp b/src/Board.cpp index 437b8ec..f4e7d9e 100644 --- a/src/Board.cpp +++ b/src/Board.cpp @@ -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) { diff --git a/src/Reader.cpp b/src/Reader.cpp index 271d1f8..82ae456 100644 --- a/src/Reader.cpp +++ b/src/Reader.cpp @@ -280,6 +280,10 @@ bool Reader::lastParagraph() { uint32_t Reader::currentIndex() const { return index_; } +uint32_t Reader::wordCount() const { + return static_cast(tokens_.size()); +} + uint8_t Reader::progressPercent() const { if (tokens_.empty()) return 0; if (tokens_.size() == 1) return 100; diff --git a/src/main.cpp b/src/main.cpp index b6c86c0..aaa51ea 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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; }