Clean up dialogue detection, add dialogue and non-dialogue text coloring settings

This commit is contained in:
j2 2026-10-14 23:47:08 -05:00
parent e06b886c28
commit e64062c2c8
9 changed files with 280 additions and 44 deletions

View File

@ -1,5 +1,7 @@
# T-Display S3 Pro RSVP Reader # T-Display S3 Pro RSVP Reader
![Animated preview of the RSVP reader display](docs/reader-preview.gif)
An offline RSVP reader for the LilyGO T-Display S3 Pro. It renders one word at An offline RSVP reader for the LilyGO T-Display S3 Pro. It renders one word at
a time on a black, landscape display and never starts Wi-Fi or Bluetooth. a time on a black, landscape display and never starts Wi-Fi or Bluetooth.
Detected dialogue appears grey when Colors is on; other words appear white. Detected dialogue appears grey when Colors is on; other words appear white.
@ -136,10 +138,14 @@ section. Its versioned, delta-encoded records preserve italic and bold EPUB
markup—including semantic tags, CSS classes, and inline CSS—reading-timing markup—including semantic tags, CSS classes, and inline CSS—reading-timing
metadata, and a word color flag. Quoted dialogue and wholly italic or uppercase metadata, and a word color flag. Quoted dialogue and wholly italic or uppercase
punctuated clauses receive the `Other` flag; speaker attribution is not used punctuated clauses receive the `Other` flag; speaker attribution is not used
during conversion. With coloring enabled, the device shows those words in grey during conversion. Flagged dialogue defaults to gray, and other words default
and other words in white. The reader menu has a persisted **Colors: On/Off** to white. The **Colors** submenu stores separate
setting. The fingerprint covers the source `.txt` bytes, before the device Text and Dialogue colors, selected from white, gray, and the nine hues in
applies display-only punctuation normalization. During conversion, the `docs/history/PLAN.md`. Text color applies to non-dialogue words and the reader
header and footer; Dialogue color applies to flagged dialogue words. Its live
preview shows “text” and italic “dialogue”. The fingerprint covers the source
`.txt` bytes, before the device applies display-only punctuation normalization.
During conversion, the
desktop-only English vocabulary classifier marks each unfamiliar word. It desktop-only English vocabulary classifier marks each unfamiliar word. It
combines a dictionary with word-frequency data and recognizes normal English combines a dictionary with word-frequency data and recognizes normal English
inflections, contractions, possessives, and hyphenated compounds. The first inflections, contractions, possessives, and hyphenated compounds. The first

View File

@ -51,7 +51,7 @@ def identify_dialogue(paragraph, word_start, italic_word_indexes, paragraph_inde
def _styled_dialogue_word_indexes(paragraph, word_start, italic_word_indexes): def _styled_dialogue_word_indexes(paragraph, word_start, italic_word_indexes):
"""Find words in wholly italic or uppercase punctuated clauses.""" """Find italic or uppercase punctuated clauses, bridging one italic gap."""
words = list(WORD.finditer(paragraph)) words = list(WORD.finditer(paragraph))
clauses = {} clauses = {}
@ -62,9 +62,12 @@ def _styled_dialogue_word_indexes(paragraph, word_start, italic_word_indexes):
clause = clauses.setdefault( clause = clauses.setdefault(
clause_number, clause_number,
{"italic": True, "uppercase": True, "last_start": match.start(), {"italic": True, "uppercase": True, "last_start": match.start(),
"first_start": match.start(), "word_indexes": []}, "first_start": match.start(), "word_indexes": [],
"italic_flags": []},
)
clause["italic_flags"].append(
word_start + word_number in italic_word_indexes
) )
clause["italic"] &= word_start + word_number in italic_word_indexes
clause["uppercase"] &= bool(letters) and letters.isupper() clause["uppercase"] &= bool(letters) and letters.isupper()
clause["last_start"] = match.start() clause["last_start"] = match.start()
clause["word_indexes"].append(word_start + word_number) clause["word_indexes"].append(word_start + word_number)
@ -77,7 +80,16 @@ def _styled_dialogue_word_indexes(paragraph, word_start, italic_word_indexes):
punctuated = CLAUSE_PUNCTUATION.search( punctuated = CLAUSE_PUNCTUATION.search(
paragraph, clause["last_start"], boundary_end paragraph, clause["last_start"], boundary_end
) is not None ) is not None
if punctuated and (clause["italic"] or clause["uppercase"]): italic_flags = clause["italic_flags"]
italic_clause = all(italic_flags)
if not italic_clause and italic_flags.count(False) == 1:
gap = italic_flags.index(False)
italic_clause = (
0 < gap < len(italic_flags) - 1
and all(italic_flags[:gap])
and all(italic_flags[gap + 1:])
)
if punctuated and (italic_clause or clause["uppercase"]):
dialogue_indexes.update(clause["word_indexes"]) dialogue_indexes.update(clause["word_indexes"])
return dialogue_indexes return dialogue_indexes
@ -100,19 +112,30 @@ def dialogue_word_indexes(text, style_records):
paragraph, word_start, italic_indexes paragraph, word_start, italic_indexes
)) ))
single_quotes = list(PAIRED_SINGLE_QUOTES.finditer(paragraph)) single_quotes = list(PAIRED_SINGLE_QUOTES.finditer(paragraph))
continuation_quote = in_double_quote and paragraph.startswith('"')
for word_number, match in enumerate(words): for word_number, match in enumerate(words):
quoted_word = False quoted_word = False
for position in range(match.start(), match.end()): for position in range(match.start(), match.end()):
character = paragraph[position] character = paragraph[position]
if character == '"': if character == '"':
quoted_word = True quoted_word = True
if continuation_quote and position == 0: is_opening_mark = position == match.start()
continuation_quote = False is_closing_mark = position == match.end() - 1
else: if is_opening_mark and is_closing_mark:
in_double_quote = not in_double_quote in_double_quote = not in_double_quote
elif is_opening_mark:
in_double_quote = True
elif is_closing_mark:
if in_double_quote:
in_double_quote = False
else:
dialogue_indexes.update(
word_start + leading_word
for leading_word in range(word_number + 1)
)
elif in_double_quote: elif in_double_quote:
quoted_word = True quoted_word = True
if in_double_quote:
quoted_word = True
if quoted_word or any( if quoted_word or any(
match.start() < quote.end() and match.end() > quote.start() match.start() < quote.end() and match.end() > quote.start()
for quote in single_quotes for quote in single_quotes

BIN
docs/reader-preview.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@ -12,9 +12,14 @@ class Board {
uint32_t wordCount, uint8_t progressPercent); uint32_t wordCount, uint8_t progressPercent);
void drawWord(const String& word, uint16_t wpm, uint8_t styleFlags = 0, void drawWord(const String& word, uint16_t wpm, uint8_t styleFlags = 0,
uint8_t wordColorFlag = 0); uint8_t wordColorFlag = 0);
void setReadingColors(uint16_t textColor, uint16_t dialogueColor);
void drawChapterTitle(const String& chapter, uint16_t wpm); void drawChapterTitle(const String& chapter, uint16_t wpm);
void drawTheEnd(uint16_t wpm); void drawTheEnd(uint16_t wpm);
void drawMenu(uint8_t selectedOption, bool colorsEnabled); void drawMenu(uint8_t selectedOption);
void drawColorsMenu(uint8_t selectedOption, uint8_t textColor,
uint8_t dialogueColor);
void drawColorPicker(bool dialogueSetting, uint8_t selectedColor,
uint16_t textColor, uint16_t dialogueColor);
void drawPicker(const String& title, const String& value); void drawPicker(const String& title, const String& value);
void drawFontStylePicker(uint8_t style, uint8_t size); void drawFontStylePicker(uint8_t style, uint8_t size);
void drawFontSizePicker(uint8_t style, uint8_t size); void drawFontSizePicker(uint8_t style, uint8_t size);
@ -46,6 +51,8 @@ class Board {
uint8_t preferredBrightness_ = 5; uint8_t preferredBrightness_ = 5;
uint8_t fontStyle_ = 0; uint8_t fontStyle_ = 0;
uint8_t fontSize_ = 20; uint8_t fontSize_ = 20;
uint16_t textColor_ = 0xFFFF;
uint16_t dialogueColor_ = 0xB596;
bool landscape_ = false; bool landscape_ = false;
bool powerMonitorAvailable_ = false; bool powerMonitorAvailable_ = false;
bool statusDrawn_ = false; bool statusDrawn_ = false;

View File

@ -11,6 +11,7 @@ constexpr uint16_t rgb565(uint8_t red, uint8_t green, uint8_t blue) {
} }
constexpr uint16_t kOtherGrey = rgb565(176, 176, 176); constexpr uint16_t kOtherGrey = rgb565(176, 176, 176);
constexpr uint16_t kWhite = rgb565(255, 255, 255);
constexpr uint16_t kHues[] = {0, 30, 60, 120, 180, 225, 270, 300, 330}; constexpr uint16_t kHues[] = {0, 30, 60, 120, 180, 225, 270, 300, 330};
constexpr uint16_t kColors[] = { constexpr uint16_t kColors[] = {
@ -18,6 +19,26 @@ constexpr uint16_t kColors[] = {
rgb565(128, 255, 128), rgb565(128, 255, 255), rgb565(128, 159, 255), rgb565(128, 255, 128), rgb565(128, 255, 255), rgb565(128, 159, 255),
rgb565(191, 128, 255), rgb565(255, 128, 255), rgb565(255, 128, 191), rgb565(191, 128, 255), rgb565(255, 128, 255), rgb565(255, 128, 191),
}; };
constexpr const char* kHueNames[] = {
"Red", "Orange", "Yellow", "Green", "Cyan", "Blue",
"Violet", "Magenta", "Pink",
};
// The color picker includes the two existing neutral choices before the hue
// palette documented in docs/history/PLAN.md.
constexpr uint8_t kChoiceCount = 11;
inline uint16_t colorForChoice(uint8_t choice) {
if (choice == 0) return kWhite;
if (choice == 1) return kOtherGrey;
return choice < kChoiceCount ? kColors[choice - 2] : kWhite;
}
inline const char* nameForChoice(uint8_t choice) {
if (choice == 0) return "White";
if (choice == 1) return "Gray";
return choice < kChoiceCount ? kHueNames[choice - 2] : "White";
}
inline int indexForHue(uint16_t hue) { inline int indexForHue(uint16_t hue) {
for (size_t index = 0; index < sizeof(kHues) / sizeof(kHues[0]); ++index) { for (size_t index = 0; index < sizeof(kHues) / sizeof(kHues[0]); ++index) {

View File

@ -9,7 +9,8 @@ struct SavedState {
uint8_t brightness = 5; uint8_t brightness = 5;
uint8_t fontStyle = 0; uint8_t fontStyle = 0;
uint8_t fontSize = 20; uint8_t fontSize = 20;
bool colorsEnabled = true; uint8_t textColor = 0;
uint8_t dialogueColor = 1;
}; };
class Settings { class Settings {

View File

@ -173,7 +173,7 @@ void Board::drawStatus(uint16_t wpm) {
constexpr int16_t kBatteryTipWidth = 3; constexpr int16_t kBatteryTipWidth = 3;
setUiFont(fontStyle_); setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE); display_->setTextColor(textColor_);
display_->setTextDatum(ML_DATUM); display_->setTextDatum(ML_DATUM);
const String wpmLabel = String(wpm) + " WPM"; const String wpmLabel = String(wpm) + " WPM";
display_->drawString(wpmLabel, kMargin, kStatusHeight / 2); display_->drawString(wpmLabel, kMargin, kStatusHeight / 2);
@ -184,11 +184,14 @@ void Board::drawStatus(uint16_t wpm) {
const String label = percentage < 0 ? "--" : String(percentage) + "%"; const String label = percentage < 0 ? "--" : String(percentage) + "%";
display_->setTextDatum(MR_DATUM); display_->setTextDatum(MR_DATUM);
display_->drawString(label, batteryX - 5, kStatusHeight / 2); display_->drawString(label, batteryX - 5, kStatusHeight / 2);
display_->drawRoundRect(batteryX, batteryY, kBatteryWidth, kBatteryHeight, 2, TFT_WHITE); display_->drawRoundRect(batteryX, batteryY, kBatteryWidth, kBatteryHeight, 2,
display_->fillRect(batteryX + kBatteryWidth, batteryY + 4, kBatteryTipWidth, 4, TFT_WHITE); textColor_);
display_->fillRect(batteryX + kBatteryWidth, batteryY + 4, kBatteryTipWidth, 4,
textColor_);
if (percentage > 0) { if (percentage > 0) {
const int16_t fillWidth = (kBatteryWidth - 4) * percentage / 100; const int16_t fillWidth = (kBatteryWidth - 4) * percentage / 100;
display_->fillRect(batteryX + 2, batteryY + 2, fillWidth, kBatteryHeight - 4, TFT_WHITE); display_->fillRect(batteryX + 2, batteryY + 2, fillWidth,
kBatteryHeight - 4, textColor_);
} }
const int16_t left = kMargin + display_->textWidth(wpmLabel) + 8; const int16_t left = kMargin + display_->textWidth(wpmLabel) + 8;
@ -225,7 +228,7 @@ void Board::drawFooter() {
display_->fillRect(0, display_->height() - kFooterHeight, display_->width(), display_->fillRect(0, display_->height() - kFooterHeight, display_->width(),
kFooterHeight, TFT_BLACK); kFooterHeight, TFT_BLACK);
setUiFont(fontStyle_); setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE); display_->setTextColor(textColor_);
const String details = String(wordCount_) + " words - " + const String details = String(wordCount_) + " words - " +
String(progressPercent_) + "%"; String(progressPercent_) + "%";
const String separator = " - "; const String separator = " - ";
@ -242,6 +245,15 @@ void Board::drawFooter() {
displayedProgressPercent_ = progressPercent_; displayedProgressPercent_ = progressPercent_;
} }
void Board::setReadingColors(uint16_t textColor, uint16_t dialogueColor) {
if (textColor_ == textColor && dialogueColor_ == dialogueColor) return;
textColor_ = textColor;
dialogueColor_ = dialogueColor;
statusDrawn_ = false;
footerDrawn_ = false;
displayedProgressPercent_ = 255;
}
void Board::drawWord(const String& word, uint16_t wpm, uint8_t styleFlags, void Board::drawWord(const String& word, uint16_t wpm, uint8_t styleFlags,
uint8_t wordColorFlag) { uint8_t wordColorFlag) {
if (display_ == nullptr) return; if (display_ == nullptr) return;
@ -272,7 +284,7 @@ void Board::drawWord(const String& word, uint16_t wpm, uint8_t styleFlags,
renderedSize -= config::kFontSizeStep; renderedSize -= config::kFontSizeStep;
setReaderFont(fontStyle_, renderedSize, styleFlags); setReaderFont(fontStyle_, renderedSize, styleFlags);
} }
display_->setTextColor(wordColorFlag == 1 ? palette::kOtherGrey : TFT_WHITE); display_->setTextColor(wordColorFlag == 1 ? dialogueColor_ : textColor_);
display_->setTextDatum(MC_DATUM); display_->setTextDatum(MC_DATUM);
const int16_t centerX = display_->width() / 2; const int16_t centerX = display_->width() / 2;
@ -310,7 +322,7 @@ void Board::drawChapterTitle(const String& chapter, uint16_t wpm) {
void Board::drawTheEnd(uint16_t wpm) { drawWord("The End", wpm); } void Board::drawTheEnd(uint16_t wpm) { drawWord("The End", wpm); }
void Board::drawMenu(uint8_t selectedOption, bool colorsEnabled) { void Board::drawMenu(uint8_t selectedOption) {
if (display_ == nullptr) return; if (display_ == nullptr) return;
if (landscape_) { if (landscape_) {
@ -323,7 +335,7 @@ void Board::drawMenu(uint8_t selectedOption, bool colorsEnabled) {
displayedProgressPercent_ = 255; displayedProgressPercent_ = 255;
previousWordBounds_.valid = false; previousWordBounds_.valid = false;
setUiFont(fontStyle_); setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE); display_->setTextColor(textColor_);
display_->setTextDatum(TC_DATUM); display_->setTextDatum(TC_DATUM);
display_->drawString("MENU", display_->width() / 2, 18); display_->drawString("MENU", display_->width() / 2, 18);
@ -331,21 +343,22 @@ void Board::drawMenu(uint8_t selectedOption, bool colorsEnabled) {
constexpr int16_t kFirstRowY = 54; constexpr int16_t kFirstRowY = 54;
constexpr int16_t kRowSpacing = 56; constexpr int16_t kRowSpacing = 56;
const char* labels[] = {"Back", "Book selection", "Chapter selection", const char* labels[] = {"Back", "Book selection", "Chapter selection",
"Brightness", "Font", "Brightness", "Font", "Colors"};
colorsEnabled ? "Colors: On" : "Colors: Off"};
display_->setTextDatum(ML_DATUM); display_->setTextDatum(ML_DATUM);
for (uint8_t index = 0; index < 6; ++index) { for (uint8_t index = 0; index < 6; ++index) {
const int16_t y = kFirstRowY + index * kRowSpacing; const int16_t y = kFirstRowY + index * kRowSpacing;
if (index == selectedOption) { if (index == selectedOption) {
display_->fillRoundRect(10, y, display_->width() - 20, kRowHeight, 5, TFT_WHITE); display_->fillRoundRect(10, y, display_->width() - 20, kRowHeight, 5,
textColor_);
display_->setTextColor(TFT_BLACK); display_->setTextColor(TFT_BLACK);
} else { } else {
display_->drawRoundRect(10, y, display_->width() - 20, kRowHeight, 5, TFT_WHITE); display_->drawRoundRect(10, y, display_->width() - 20, kRowHeight, 5,
display_->setTextColor(TFT_WHITE); textColor_);
display_->setTextColor(textColor_);
} }
display_->drawString(labels[index], 24, y + kRowHeight / 2); display_->drawString(labels[index], 24, y + kRowHeight / 2);
} }
display_->setTextColor(TFT_WHITE); display_->setTextColor(textColor_);
display_->setTextDatum(BC_DATUM); display_->setTextDatum(BC_DATUM);
display_->drawString("PREV/NEXT choose", display_->width() / 2, display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36); display_->height() - 36);
@ -353,6 +366,85 @@ void Board::drawMenu(uint8_t selectedOption, bool colorsEnabled) {
display_->height() - 8); display_->height() - 8);
} }
void Board::drawColorsMenu(uint8_t selectedOption, uint8_t textColor,
uint8_t dialogueColor) {
if (display_ == nullptr) return;
if (landscape_) {
display_->setRotation(0);
landscape_ = false;
}
display_->fillScreen(TFT_BLACK);
statusDrawn_ = false;
footerDrawn_ = false;
previousWordBounds_.valid = false;
setUiFont(fontStyle_);
display_->setTextColor(textColor_);
display_->setTextDatum(TC_DATUM);
display_->drawString("COLORS", display_->width() / 2, 18);
const char* labels[] = {"Text", "Dialogue", "Back"};
const uint8_t choices[] = {textColor, dialogueColor, 0};
constexpr int16_t kRowHeight = 48;
constexpr int16_t kFirstRowY = 64;
constexpr int16_t kRowSpacing = 58;
display_->setTextDatum(ML_DATUM);
for (uint8_t index = 0; index < 3; ++index) {
const int16_t y = kFirstRowY + index * kRowSpacing;
if (index == selectedOption) {
display_->fillRoundRect(10, y, display_->width() - 20, kRowHeight, 5,
textColor_);
display_->setTextColor(TFT_BLACK);
} else {
display_->drawRoundRect(10, y, display_->width() - 20, kRowHeight, 5,
textColor_);
display_->setTextColor(textColor_);
}
const String label = index < 2
? String(labels[index]) + ": " +
palette::nameForChoice(choices[index])
: String(labels[index]);
display_->drawString(label, 24, y + kRowHeight / 2);
}
display_->setTextColor(textColor_);
display_->setTextDatum(BC_DATUM);
display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36);
display_->drawString("BOOT select", display_->width() / 2,
display_->height() - 8);
}
void Board::drawColorPicker(bool dialogueSetting, uint8_t selectedColor,
uint16_t textColor, uint16_t dialogueColor) {
if (display_ == nullptr) return;
setLandscape();
display_->fillScreen(TFT_BLACK);
statusDrawn_ = false;
footerDrawn_ = false;
previousWordBounds_.valid = false;
setUiFont(fontStyle_);
display_->setTextColor(textColor_);
display_->setTextDatum(TC_DATUM);
display_->drawString(dialogueSetting ? "DIALOGUE COLOR" : "TEXT COLOR",
display_->width() / 2, 12);
display_->setTextColor(palette::colorForChoice(selectedColor));
display_->drawString(palette::nameForChoice(selectedColor),
display_->width() / 2, 54);
setReaderFont(fontStyle_, fontSize_, dialogueSetting ? kItalicStyle : 0);
display_->setTextDatum(MC_DATUM);
display_->setTextColor(dialogueSetting ? dialogueColor : textColor);
display_->drawString(dialogueSetting ? "dialogue" : "text",
display_->width() / 2, 118);
setUiFont(fontStyle_);
display_->setTextColor(textColor_);
display_->setTextDatum(BC_DATUM);
display_->drawString("PREV/NEXT color", display_->width() / 2,
display_->height() - 36);
display_->drawString("BOOT save", display_->width() / 2,
display_->height() - 8);
}
void Board::drawPicker(const String& title, const String& value) { void Board::drawPicker(const String& title, const String& value) {
if (display_ == nullptr) return; if (display_ == nullptr) return;
setLandscape(); setLandscape();
@ -361,7 +453,7 @@ void Board::drawPicker(const String& title, const String& value) {
statusDrawn_ = false; statusDrawn_ = false;
previousWordBounds_.valid = false; previousWordBounds_.valid = false;
setUiFont(fontStyle_); setUiFont(fontStyle_);
display_->setTextColor(TFT_WHITE); display_->setTextColor(textColor_);
display_->setTextDatum(TC_DATUM); display_->setTextDatum(TC_DATUM);
display_->drawString(title, display_->width() / 2, 16); display_->drawString(title, display_->width() / 2, 16);
@ -375,6 +467,7 @@ void Board::drawPicker(const String& title, const String& value) {
display_->width() / 2, display_->height() / 2 - 8); display_->width() / 2, display_->height() / 2 - 8);
setUiFont(fontStyle_); setUiFont(fontStyle_);
display_->setTextColor(textColor_);
display_->setTextDatum(BC_DATUM); display_->setTextDatum(BC_DATUM);
display_->drawString("PREV/NEXT choose", display_->width() / 2, display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36); display_->height() - 36);
@ -390,18 +483,21 @@ void Board::drawFontStylePicker(uint8_t style, uint8_t size) {
statusDrawn_ = false; statusDrawn_ = false;
previousWordBounds_.valid = false; previousWordBounds_.valid = false;
setUiFont(style); setUiFont(style);
display_->setTextColor(TFT_WHITE); display_->setTextColor(textColor_);
display_->setTextDatum(TC_DATUM); display_->setTextDatum(TC_DATUM);
display_->drawString("FONT STYLE", display_->width() / 2, 16); display_->drawString("FONT STYLE", display_->width() / 2, 16);
const char* names[] = {"Sans-serif", "Serif", "Monospace"}; const char* names[] = {"Sans-serif", "Serif", "Monospace"};
setReaderFont(style, size); setReaderFont(style, size);
display_->setTextColor(textColor_);
display_->setTextDatum(MC_DATUM); display_->setTextDatum(MC_DATUM);
display_->drawString("Sample", display_->width() / 2, display_->height() / 2 - 8); display_->drawString("Sample", display_->width() / 2, display_->height() / 2 - 8);
setUiFont(style); setUiFont(style);
display_->setTextColor(textColor_);
display_->setTextDatum(TC_DATUM); display_->setTextDatum(TC_DATUM);
display_->drawString(names[constrain(style, 0, 2)], display_->width() / 2, 48); display_->drawString(names[constrain(style, 0, 2)], display_->width() / 2, 48);
display_->setTextColor(textColor_);
display_->setTextDatum(BC_DATUM); display_->setTextDatum(BC_DATUM);
display_->drawString("PREV/NEXT choose", display_->width() / 2, display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36); display_->height() - 36);
@ -417,16 +513,18 @@ void Board::drawFontSizePicker(uint8_t style, uint8_t size) {
statusDrawn_ = false; statusDrawn_ = false;
previousWordBounds_.valid = false; previousWordBounds_.valid = false;
setUiFont(style); setUiFont(style);
display_->setTextColor(TFT_WHITE); display_->setTextColor(textColor_);
display_->setTextDatum(TC_DATUM); display_->setTextDatum(TC_DATUM);
display_->drawString("FONT SIZE", display_->width() / 2, 16); display_->drawString("FONT SIZE", display_->width() / 2, 16);
setReaderFont(style, size); setReaderFont(style, size);
display_->setTextColor(textColor_);
display_->setTextDatum(MC_DATUM); display_->setTextDatum(MC_DATUM);
display_->drawString("size", display_->width() / 2, display_->drawString("size", display_->width() / 2,
display_->height() / 2 - 8); display_->height() / 2 - 8);
setUiFont(style); setUiFont(style);
display_->setTextColor(textColor_);
display_->setTextDatum(BC_DATUM); display_->setTextDatum(BC_DATUM);
display_->drawString("PREV/NEXT choose", display_->width() / 2, display_->drawString("PREV/NEXT choose", display_->width() / 2,
display_->height() - 36); display_->height() - 36);

View File

@ -18,7 +18,8 @@ SavedState Settings::load() const {
state.brightness = preferences.getUChar("brightness", 5); state.brightness = preferences.getUChar("brightness", 5);
state.fontStyle = preferences.getUChar("font_style", 0); state.fontStyle = preferences.getUChar("font_style", 0);
state.fontSize = preferences.getUChar("font_size", 20); state.fontSize = preferences.getUChar("font_size", 20);
state.colorsEnabled = preferences.getBool("colors", true); state.textColor = preferences.getUChar("text_color", 0);
state.dialogueColor = preferences.getUChar("dialogue_color", 1);
preferences.end(); preferences.end();
return state; return state;
} }
@ -46,9 +47,13 @@ void Settings::save(const SavedState& state) const {
if (!preferences.isKey("font_size") || preferences.getUChar("font_size") != state.fontSize) { if (!preferences.isKey("font_size") || preferences.getUChar("font_size") != state.fontSize) {
preferences.putUChar("font_size", state.fontSize); preferences.putUChar("font_size", state.fontSize);
} }
if (!preferences.isKey("colors") || if (!preferences.isKey("text_color") ||
preferences.getBool("colors") != state.colorsEnabled) { preferences.getUChar("text_color") != state.textColor) {
preferences.putBool("colors", state.colorsEnabled); preferences.putUChar("text_color", state.textColor);
}
if (!preferences.isKey("dialogue_color") ||
preferences.getUChar("dialogue_color") != state.dialogueColor) {
preferences.putUChar("dialogue_color", state.dialogueColor);
} }
preferences.end(); preferences.end();
} }

View File

@ -9,6 +9,7 @@
#include "Board.h" #include "Board.h"
#include "Config.h" #include "Config.h"
#include "Library.h" #include "Library.h"
#include "Palette.h"
#include "Reader.h" #include "Reader.h"
#include "Settings.h" #include "Settings.h"
@ -35,6 +36,9 @@ enum class Screen {
kBrightnessPicker, kBrightnessPicker,
kFontStylePicker, kFontStylePicker,
kFontSizePicker, kFontSizePicker,
kColorsMenu,
kTextColorPicker,
kDialogueColorPicker,
}; };
enum class ReaderView { enum class ReaderView {
@ -49,7 +53,9 @@ String pendingChapterPath;
uint8_t pendingBrightness = config::kNormalBrightness; uint8_t pendingBrightness = config::kNormalBrightness;
uint8_t pendingFontStyle = config::kDefaultFontStyle; uint8_t pendingFontStyle = config::kDefaultFontStyle;
uint8_t pendingFontSize = config::kDefaultFontSize; uint8_t pendingFontSize = config::kDefaultFontSize;
uint8_t pendingColor = 0;
uint8_t menuSelection = 0; uint8_t menuSelection = 0;
uint8_t colorsMenuSelection = 0;
struct Button { struct Button {
explicit Button(uint8_t buttonPin) : pin(buttonPin) {} explicit Button(uint8_t buttonPin) : pin(buttonPin) {}
@ -97,14 +103,18 @@ void setReadingContext(uint8_t progressPercent) {
void showCurrentWord() { void showCurrentWord() {
setReadingContext(reader.progressPercent()); setReadingContext(reader.progressPercent());
board.setReadingColors(palette::colorForChoice(state.textColor),
palette::colorForChoice(state.dialogueColor));
board.drawWord(reader.currentWord(), state.wpm, reader.currentStyleFlags(), board.drawWord(reader.currentWord(), state.wpm, reader.currentStyleFlags(),
state.colorsEnabled ? reader.currentColorFlag() : 0); reader.currentColorFlag());
} }
void wakeDisplay(); void wakeDisplay();
void configureTouchLandscape(); void configureTouchLandscape();
void showReaderScreen() { void showReaderScreen() {
board.setReadingColors(palette::colorForChoice(state.textColor),
palette::colorForChoice(state.dialogueColor));
switch (readerView) { switch (readerView) {
case ReaderView::kChapterTitle: case ReaderView::kChapterTitle:
setReadingContext(0); setReadingContext(0);
@ -161,6 +171,19 @@ void drawPicker() {
case Screen::kFontSizePicker: case Screen::kFontSizePicker:
board.drawFontSizePicker(pendingFontStyle, pendingFontSize); board.drawFontSizePicker(pendingFontStyle, pendingFontSize);
break; break;
case Screen::kColorsMenu:
board.drawColorsMenu(colorsMenuSelection, state.textColor, state.dialogueColor);
break;
case Screen::kTextColorPicker:
case Screen::kDialogueColorPicker:
board.drawColorPicker(screen == Screen::kDialogueColorPicker, pendingColor,
palette::colorForChoice(
screen == Screen::kTextColorPicker ? pendingColor
: state.textColor),
palette::colorForChoice(
screen == Screen::kDialogueColorPicker ? pendingColor
: state.dialogueColor));
break;
default: default:
break; break;
} }
@ -173,7 +196,22 @@ void openMenu() {
menuSelection = 0; menuSelection = 0;
screen = Screen::kMenu; screen = Screen::kMenu;
wakeDisplay(); wakeDisplay();
board.drawMenu(menuSelection, state.colorsEnabled); board.drawMenu(menuSelection);
}
void openColorsMenu() {
colorsMenuSelection = 0;
screen = Screen::kColorsMenu;
configureTouchLandscape();
drawPicker();
}
void openColorPicker(Screen picker) {
screen = picker;
pendingColor = picker == Screen::kTextColorPicker ? state.textColor
: state.dialogueColor;
configureTouchLandscape();
drawPicker();
} }
void openPicker(Screen picker) { void openPicker(Screen picker) {
@ -210,6 +248,11 @@ void changePicker(int direction) {
static_cast<int>(pendingFontSize) + direction * config::kFontSizeStep, static_cast<int>(pendingFontSize) + direction * config::kFontSizeStep,
config::kMinimumFontSize, config::kMaximumFontSize); config::kMinimumFontSize, config::kMaximumFontSize);
break; break;
case Screen::kTextColorPicker:
case Screen::kDialogueColorPicker:
pendingColor = (pendingColor + palette::kChoiceCount + direction) %
palette::kChoiceCount;
break;
default: default:
return; return;
} }
@ -217,6 +260,22 @@ void changePicker(int direction) {
} }
void applyPicker() { void applyPicker() {
if (screen == Screen::kTextColorPicker) {
state.textColor = pendingColor;
persistState();
screen = Screen::kColorsMenu;
colorsMenuSelection = 1;
drawPicker();
return;
}
if (screen == Screen::kDialogueColorPicker) {
state.dialogueColor = pendingColor;
persistState();
screen = Screen::kColorsMenu;
colorsMenuSelection = 2;
drawPicker();
return;
}
if (screen == Screen::kFontStylePicker) { if (screen == Screen::kFontStylePicker) {
screen = Screen::kFontSizePicker; screen = Screen::kFontSizePicker;
drawPicker(); drawPicker();
@ -333,14 +392,20 @@ void confirmMenuSelection() {
case 2: openPicker(Screen::kChapterPicker); break; case 2: openPicker(Screen::kChapterPicker); break;
case 3: openPicker(Screen::kBrightnessPicker); break; case 3: openPicker(Screen::kBrightnessPicker); break;
case 4: openPicker(Screen::kFontStylePicker); break; case 4: openPicker(Screen::kFontStylePicker); break;
case 5: case 5: openColorsMenu(); break;
state.colorsEnabled = !state.colorsEnabled;
persistState();
board.drawMenu(menuSelection, state.colorsEnabled);
break;
} }
return; return;
} }
if (screen == Screen::kColorsMenu) {
if (colorsMenuSelection == 2) {
screen = Screen::kMenu;
board.drawMenu(menuSelection);
return;
}
openColorPicker(colorsMenuSelection == 0 ? Screen::kTextColorPicker
: Screen::kDialogueColorPicker);
return;
}
applyPicker(); applyPicker();
} }
@ -397,7 +462,12 @@ void handleInput() {
wakeDisplay(); wakeDisplay();
if (screen == Screen::kMenu) { if (screen == Screen::kMenu) {
menuSelection = (menuSelection + 5) % 6; menuSelection = (menuSelection + 5) % 6;
board.drawMenu(menuSelection, state.colorsEnabled); board.drawMenu(menuSelection);
return;
}
if (screen == Screen::kColorsMenu) {
colorsMenuSelection = (colorsMenuSelection + 2) % 3;
drawPicker();
return; return;
} }
if (screen != Screen::kReader) { if (screen != Screen::kReader) {
@ -417,7 +487,12 @@ void handleInput() {
wakeDisplay(); wakeDisplay();
if (screen == Screen::kMenu) { if (screen == Screen::kMenu) {
menuSelection = (menuSelection + 1) % 6; menuSelection = (menuSelection + 1) % 6;
board.drawMenu(menuSelection, state.colorsEnabled); board.drawMenu(menuSelection);
return;
}
if (screen == Screen::kColorsMenu) {
colorsMenuSelection = (colorsMenuSelection + 1) % 3;
drawPicker();
return; return;
} }
if (screen != Screen::kReader) { if (screen != Screen::kReader) {