Fill Reader pages with numbered subsections
This commit is contained in:
1 parent
6236eec386
commit
bee8cf7c4e
5 files changed
+268
-119
No files matched your search
@@ -27,9 +27,9 @@ class ReaderScreen : public UIScreen {
|
||||
_display->setTextSize(1);
|
||||
const int width = _display->width();
|
||||
const int header_height = _display->textLineHeight();
|
||||
// Narrow/rotated panels need separate reference and progress lines.
|
||||
// Narrow/rotated panels need separate start/end reference lines.
|
||||
const bool stacked_header = width < _display->getTextWidth("88:88")
|
||||
+ _display->getTextWidth("88/88") + 4;
|
||||
+ _display->getTextWidth("88:88") + 4;
|
||||
const int header_content_height = header_height * (stacked_header ? 2 : 1);
|
||||
#if UI_READER_TOUCH_BAR
|
||||
const int padded_header_height = mesh::ui::readerTouchHeaderHeight(header_content_height);
|
||||
@@ -76,62 +76,108 @@ class ReaderScreen : public UIScreen {
|
||||
return body.getTextWidth(filtered);
|
||||
};
|
||||
|
||||
// At most two lookups: the current verse, then its adjacent verse when
|
||||
// navigation crosses a boundary. No recursive decode or second buffer.
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
const LookupResult result = mesh::readReaderVerse(
|
||||
referenceAt(pos.verse), scratch, sizeof(scratch), text);
|
||||
if (result != LookupResult::Found) {
|
||||
if (draw) {
|
||||
body.setColor(UIColor::warning_txt);
|
||||
body.drawTextEllipsized(0, top, width, "Reader unavailable");
|
||||
#if UI_BUTTON_READER_HINT
|
||||
#if UI_READER_TOUCH_BAR
|
||||
mesh::ui::drawReaderTouchBar(hint_text, _touch_bar);
|
||||
#else
|
||||
mesh::ui::drawButtonReaderHint(hint_text, hint);
|
||||
#endif
|
||||
#endif
|
||||
} else _task->showAlert("Reader data error", 1500);
|
||||
return;
|
||||
}
|
||||
// Stream adjacent verses through one decode buffer. Number labels are
|
||||
// display-only, so existing raw-text bookmarks remain compatible.
|
||||
uint16_t loaded_verse = kVerseCount;
|
||||
LookupResult result = LookupResult::Found;
|
||||
auto load = [&](uint16_t verse) {
|
||||
if (loaded_verse == verse) return true;
|
||||
result = mesh::readReaderVerse(referenceAt(verse), scratch, sizeof(scratch), text);
|
||||
if (result != LookupResult::Found) return false;
|
||||
memmove(scratch, text, strlen(text) + 1);
|
||||
const ReaderPage page = readerPage(scratch, pos.offset, width, rows, measure);
|
||||
pos.offset = page.start; // clamp stale bookmarks after a layout change
|
||||
if (direction > 0) {
|
||||
if (scratch[page.next]) pos.offset = page.next;
|
||||
else if (pos.verse + 1 < kVerseCount) { ++pos.verse; pos.offset = 0; }
|
||||
direction = 0;
|
||||
continue;
|
||||
loaded_verse = verse;
|
||||
return true;
|
||||
};
|
||||
char line[kReaderLineBytes];
|
||||
auto nextLine = [&](uint16_t verse, uint16_t offset) {
|
||||
char number[5] = {};
|
||||
if (!offset)
|
||||
snprintf(number, sizeof(number), "%u ", referenceAt(verse).verse);
|
||||
return readerLine(scratch, offset, width, measure, line, number);
|
||||
};
|
||||
|
||||
if (load(pos.verse)) {
|
||||
// Resume on the line containing the saved word after font/rotation
|
||||
// changes, without tying the bookmark to a page or a number label.
|
||||
uint16_t offset = 0;
|
||||
while (scratch[offset]) {
|
||||
const uint16_t next = nextLine(pos.verse, offset);
|
||||
if (next > pos.offset || !scratch[next]) break;
|
||||
offset = next;
|
||||
}
|
||||
pos.offset = offset;
|
||||
|
||||
if (direction < 0) {
|
||||
if (page.start) pos.offset = page.previous;
|
||||
else if (pos.verse) { --pos.verse; pos.offset = kBlockSize - 1; }
|
||||
direction = 0;
|
||||
continue;
|
||||
int remaining = rows;
|
||||
while (load(pos.verse)) {
|
||||
uint16_t count = 0;
|
||||
offset = 0;
|
||||
while (scratch[offset] && offset < pos.offset) {
|
||||
offset = nextLine(pos.verse, offset);
|
||||
++count;
|
||||
}
|
||||
if (count >= remaining) {
|
||||
offset = 0;
|
||||
for (int skip = count - remaining; skip > 0; --skip)
|
||||
offset = nextLine(pos.verse, offset);
|
||||
pos.offset = offset;
|
||||
break;
|
||||
}
|
||||
remaining -= count;
|
||||
if (!pos.verse) { pos.offset = 0; break; }
|
||||
--pos.verse;
|
||||
pos.offset = kBlockSize - 1;
|
||||
}
|
||||
}
|
||||
_bookmark.move(pos, millis());
|
||||
if (!draw) return;
|
||||
char label[32], progress[16];
|
||||
const Reference ref = referenceAt(pos.verse);
|
||||
snprintf(label, sizeof(label), "Reader %u:%u", ref.chapter, ref.verse);
|
||||
snprintf(progress, sizeof(progress), "%u/%u", page.part, page.parts);
|
||||
const int progress_width = _display->getTextWidth(progress);
|
||||
if (stacked_header || _display->getTextWidth(label) > width - progress_width - 4)
|
||||
snprintf(label, sizeof(label), "%u:%u", ref.chapter, ref.verse);
|
||||
_display->setColor(UIColor::title_txt);
|
||||
_display->drawTextEllipsized(0, header_text_y, stacked_header ? width : width - progress_width - 4, label);
|
||||
_display->drawTextRightAlign(width, header_text_y + (stacked_header ? header_height : 0), progress);
|
||||
_display->drawRect(0, top - 2, width, 1);
|
||||
body.setColor(UIColor::primary_txt);
|
||||
uint16_t offset = page.start;
|
||||
for (int row = 0; row < rows && scratch[offset]; ++row) {
|
||||
char line[kReaderLineBytes], filtered[kReaderLineBytes];
|
||||
offset = readerLine(scratch, offset, width, measure, line);
|
||||
body.translateUTF8ToBlocks(filtered, line, sizeof(filtered));
|
||||
body.setCursor(0, top + row * line_height);
|
||||
body.print(filtered);
|
||||
|
||||
Position cursor = pos, last = pos;
|
||||
if (direction >= 0 && result == LookupResult::Found) {
|
||||
body.setColor(UIColor::primary_txt);
|
||||
for (int row = 0; row < rows && cursor.verse < kVerseCount; ++row) {
|
||||
if (!load(cursor.verse)) break;
|
||||
last = cursor;
|
||||
cursor.offset = nextLine(cursor.verse, cursor.offset);
|
||||
if (draw) {
|
||||
char filtered[kReaderLineBytes];
|
||||
body.translateUTF8ToBlocks(filtered, line, sizeof(filtered));
|
||||
body.setCursor(0, top + row * line_height);
|
||||
body.print(filtered);
|
||||
}
|
||||
if (!scratch[cursor.offset]) { ++cursor.verse; cursor.offset = 0; }
|
||||
}
|
||||
// At the end keep the final page visible; never persist an EOF cursor.
|
||||
if (direction > 0 && cursor.verse < kVerseCount) pos = cursor;
|
||||
}
|
||||
if (result == LookupResult::Found) {
|
||||
_bookmark.move(pos, millis());
|
||||
if (!draw) return;
|
||||
char label[32], progress[16];
|
||||
const Reference ref = referenceAt(pos.verse);
|
||||
const Reference end = referenceAt(last.verse);
|
||||
snprintf(label, sizeof(label), "Reader %u:%u", ref.chapter, ref.verse);
|
||||
if (last.verse != pos.verse)
|
||||
snprintf(progress, sizeof(progress), "%u:%u",
|
||||
end.chapter, end.verse);
|
||||
else progress[0] = 0;
|
||||
const int progress_width = _display->getTextWidth(progress);
|
||||
if (stacked_header || _display->getTextWidth(label) > width - progress_width - 4)
|
||||
snprintf(label, sizeof(label), "%u:%u", ref.chapter, ref.verse);
|
||||
_display->setColor(UIColor::title_txt);
|
||||
_display->drawTextEllipsized(0, header_text_y, stacked_header ? width : width - progress_width - 4, label);
|
||||
_display->drawTextRightAlign(width, header_text_y + (stacked_header ? header_height : 0), progress);
|
||||
_display->drawRect(0, top - 2, width, 1);
|
||||
} else if (draw) {
|
||||
body.setColor(UIColor::window_bkg);
|
||||
body.fillRect(0, top, width, bottom - top);
|
||||
}
|
||||
}
|
||||
if (result != LookupResult::Found) {
|
||||
if (draw) {
|
||||
body.setColor(UIColor::warning_txt);
|
||||
body.drawTextEllipsized(0, top, width, "Reader unavailable");
|
||||
} else _task->showAlert("Reader data error", 1500);
|
||||
}
|
||||
if (draw) {
|
||||
#if UI_BUTTON_READER_HINT
|
||||
#if UI_READER_TOUCH_BAR
|
||||
mesh::ui::drawReaderTouchBar(hint_text, _touch_bar);
|
||||
@@ -139,7 +185,6 @@ class ReaderScreen : public UIScreen {
|
||||
mesh::ui::drawButtonReaderHint(hint_text, hint);
|
||||
#endif
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,20 @@ constexpr size_t kReaderLineBytes = 128;
|
||||
// word or cut a UTF-8 codepoint, including overlong words on narrow displays.
|
||||
template <typename Measure>
|
||||
uint16_t readerLine(const char* text, uint16_t start, int width,
|
||||
Measure measure, char (&line)[kReaderLineBytes]) {
|
||||
Measure measure, char (&line)[kReaderLineBytes],
|
||||
const char* prefix = "") {
|
||||
// Number labels take display space but never become bookmark offsets.
|
||||
size_t prefix_bytes = strlen(prefix);
|
||||
if (prefix_bytes > sizeof(line) - 5) prefix_bytes = sizeof(line) - 5;
|
||||
memcpy(line, prefix, prefix_bytes);
|
||||
size_t count = 0, last_space = 0;
|
||||
while (text[start + count]) {
|
||||
size_t bytes = 1;
|
||||
while (bytes < 4 && text[start + count + bytes]
|
||||
&& (static_cast<uint8_t>(text[start + count + bytes]) & 0xc0) == 0x80) ++bytes;
|
||||
if (count + bytes >= sizeof(line)) break;
|
||||
memcpy(line + count, text + start + count, bytes);
|
||||
line[count + bytes] = 0;
|
||||
if (prefix_bytes + count + bytes >= sizeof(line)) break;
|
||||
memcpy(line + prefix_bytes + count, text + start + count, bytes);
|
||||
line[prefix_bytes + count + bytes] = 0;
|
||||
const int pixels = static_cast<int>(measure(line));
|
||||
if (pixels > width && count != 0) break;
|
||||
if (text[start + count] == ' ') last_space = count;
|
||||
@@ -48,8 +53,8 @@ uint16_t readerLine(const char* text, uint16_t start, int width,
|
||||
if (pixels > width) break; // one too-wide glyph still makes progress
|
||||
}
|
||||
if (text[start + count] && last_space > 0) count = last_space;
|
||||
memcpy(line, text + start, count);
|
||||
line[count] = 0;
|
||||
memcpy(line + prefix_bytes, text + start, count);
|
||||
line[prefix_bytes + count] = 0;
|
||||
uint16_t next = static_cast<uint16_t>(start + count);
|
||||
while (text[next] == ' ') ++next;
|
||||
return next;
|
||||
|
||||
+123
-46
@@ -136,6 +136,58 @@ static Position pos(uint16_t verse, uint16_t offset = 0) {
|
||||
Position p; p.verse = verse; p.offset = offset; return p;
|
||||
}
|
||||
|
||||
struct ExpectedLine {
|
||||
Position start;
|
||||
std::string display;
|
||||
};
|
||||
|
||||
// Construct the visible stream independently of readerLine()/readerPage().
|
||||
// The numeric prefix consumes display width, but never bookmark text bytes.
|
||||
template <typename Measure>
|
||||
static std::vector<ExpectedLine> wrapVerse(uint16_t verse, const char* text,
|
||||
int width, Measure measure) {
|
||||
std::vector<ExpectedLine> result;
|
||||
const std::string source(text);
|
||||
const std::string number = std::to_string(referenceAt(verse).verse) + " ";
|
||||
std::string rebuilt;
|
||||
for (size_t offset = 0; offset < source.size();) {
|
||||
const std::string prefix = offset == 0 ? number : "";
|
||||
size_t count = 0;
|
||||
while (offset + count < source.size() && prefix.size() + count < 127) {
|
||||
const std::string candidate = prefix + source.substr(offset, count + 1);
|
||||
if (count && measure(candidate.c_str()) > width) break;
|
||||
++count; // One body glyph always advances, even on a narrow panel.
|
||||
}
|
||||
assert(count);
|
||||
if (offset + count < source.size()) {
|
||||
const size_t space = source.rfind(' ', offset + count - 1);
|
||||
if (space != std::string::npos && space > offset) count = space - offset;
|
||||
}
|
||||
const std::string body = source.substr(offset, count);
|
||||
result.push_back({pos(verse, static_cast<uint16_t>(offset)), prefix + body});
|
||||
rebuilt += body;
|
||||
offset += count;
|
||||
while (offset < source.size() && source[offset] == ' ') ++offset;
|
||||
}
|
||||
assert(!result.empty() && noSpaces(rebuilt) == noSpaces(source));
|
||||
assert(result.front().display.compare(0, number.size(), number) == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Measure>
|
||||
static Position containingLine(uint16_t verse, uint16_t offset, int width,
|
||||
Measure measure) {
|
||||
char scratch[kBlockSize];
|
||||
const char* text;
|
||||
assert(mesh::readReaderVerse(referenceAt(verse), scratch, sizeof(scratch), text)
|
||||
== LookupResult::Found);
|
||||
const auto lines = wrapVerse(verse, text, width, measure);
|
||||
Position result = lines.front().start;
|
||||
for (const auto& line : lines)
|
||||
if (line.start.offset <= offset) result = line.start;
|
||||
return result;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc > 1) {
|
||||
if (argc > 2) assert(std::freopen(argv[2], "w", stdout));
|
||||
@@ -168,7 +220,8 @@ int main(int argc, char** argv) {
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
// Every verse/part, in both directions, at common small-screen sizes.
|
||||
// Every numbered verse, packed into full pages in both directions, at
|
||||
// common small-screen sizes including the MeshPocket's native dimensions.
|
||||
const int cases[][3] = {
|
||||
{128,64,6}, {64,128,6}, {160,80,0}, {80,160,0}, {240,135,0},
|
||||
{159,80,6}, {80,159,6}, {320,240,0}, {240,320,0}, {250,122,0}, {200,200,0},
|
||||
@@ -233,47 +286,76 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
std::vector<std::string> expected_pages;
|
||||
std::vector<ExpectedLine> expected_lines;
|
||||
char scratch[kBlockSize];
|
||||
for (uint16_t verse = 0; verse < kVerseCount; ++verse) {
|
||||
const char* text;
|
||||
assert(mesh::readReaderVerse(referenceAt(verse), scratch, sizeof(scratch), text) == LookupResult::Found);
|
||||
memmove(scratch, text, strlen(text) + 1);
|
||||
std::string rebuilt;
|
||||
uint16_t offset = 0;
|
||||
do {
|
||||
const auto page = readerPage(scratch, offset, display.width(), rows, measure);
|
||||
assert(page.start == offset && page.next > offset);
|
||||
expected.clear();
|
||||
uint16_t next = offset;
|
||||
for (int row = 0; row < rows && scratch[next]; ++row) {
|
||||
char line[kReaderLineBytes];
|
||||
next = readerLine(scratch, next, display.width(), measure, line);
|
||||
rebuilt += line;
|
||||
expected_text.setCursor(0, top + row * line_height);
|
||||
expected_text.print(line);
|
||||
}
|
||||
assert(next == page.next);
|
||||
display.clear(); screen.render(display);
|
||||
const std::string body = display.body();
|
||||
assert(!body.empty());
|
||||
assert(body == expected.body());
|
||||
assert(display.getTextWidth("ABC") == 18); // header/driver font unchanged
|
||||
expected_pages.push_back(body);
|
||||
screen.handleInput(KEY_NEXT);
|
||||
offset = page.next;
|
||||
} while (scratch[offset]);
|
||||
if (noSpaces(rebuilt) != noSpaces(scratch)) {
|
||||
std::fprintf(stderr, "%dx%d verse index %u\nExpected: %s\nActual: %s\n",
|
||||
display.width(), display.height(), verse, scratch, rebuilt.c_str());
|
||||
return 1;
|
||||
}
|
||||
const auto verse_lines = wrapVerse(verse, text, display.width(), measure);
|
||||
expected_lines.insert(expected_lines.end(), verse_lines.begin(), verse_lines.end());
|
||||
}
|
||||
std::vector<std::string> expected_pages;
|
||||
std::vector<Position> expected_positions;
|
||||
bool packed_verses = false, crossed_chapter = false;
|
||||
const int body_rows = std::max(1, rows);
|
||||
for (size_t first = 0; first < expected_lines.size(); first += body_rows) {
|
||||
expected.clear();
|
||||
const size_t end = std::min(first + body_rows, expected_lines.size());
|
||||
for (size_t index = first; index < end; ++index) {
|
||||
expected_text.setCursor(0, top + static_cast<int>(index - first) * line_height);
|
||||
expected_text.print(expected_lines[index].display.c_str());
|
||||
}
|
||||
const Position start = expected_lines[first].start;
|
||||
const Position last = expected_lines[end - 1].start;
|
||||
packed_verses |= start.verse != last.verse;
|
||||
crossed_chapter |= referenceAt(start.verse).chapter != referenceAt(last.verse).chapter;
|
||||
display.clear(); screen.render(display);
|
||||
const std::string body = display.body();
|
||||
if (body != expected.body()) {
|
||||
std::fprintf(stderr, "%dx%d continuous page at verse %u offset %u\n",
|
||||
display.width(), display.height(), start.verse, start.offset);
|
||||
assert(false);
|
||||
}
|
||||
assert(!body.empty());
|
||||
const Reference first_ref = referenceAt(start.verse);
|
||||
const std::string first_label = std::to_string(first_ref.chapter) + ":"
|
||||
+ std::to_string(first_ref.verse);
|
||||
// Both full references must survive narrow headers, particularly the
|
||||
// 64-pixel portrait case once chapter and verse each have two digits.
|
||||
assert(std::any_of(display.lines.begin(), display.lines.end(),
|
||||
[&](const Display::Line& line) {
|
||||
return line.y < top && line.x == 0
|
||||
&& (line.text == first_label || line.text == "Reader " + first_label);
|
||||
}));
|
||||
if (last.verse != start.verse) {
|
||||
const Reference last_ref = referenceAt(last.verse);
|
||||
const std::string last_label = std::to_string(last_ref.chapter) + ":"
|
||||
+ std::to_string(last_ref.verse);
|
||||
assert(std::any_of(display.lines.begin(), display.lines.end(),
|
||||
[&](const Display::Line& line) {
|
||||
return line.y < top && line.x > 0 && line.text == last_label;
|
||||
}));
|
||||
}
|
||||
assert(display.getTextWidth("ABC") == 18); // header/driver font unchanged
|
||||
assert(screen.flush());
|
||||
Position saved;
|
||||
the_mesh.loadReaderBookmark(saved);
|
||||
assert(saved == start);
|
||||
expected_positions.push_back(start);
|
||||
expected_pages.push_back(body);
|
||||
screen.handleInput(KEY_NEXT);
|
||||
}
|
||||
if (body_rows > 1) assert(packed_verses);
|
||||
if (body_rows > 2) assert(crossed_chapter);
|
||||
display.clear(); screen.render(display);
|
||||
assert(display.body() == expected_pages.back()); // no wrap at end
|
||||
for (size_t page = expected_pages.size(); page > 0; --page) {
|
||||
display.clear(); screen.render(display);
|
||||
assert(display.body() == expected_pages[page - 1]);
|
||||
assert(screen.flush());
|
||||
Position saved;
|
||||
the_mesh.loadReaderBookmark(saved);
|
||||
assert(saved == expected_positions[page - 1]);
|
||||
screen.handleInput(KEY_PREV);
|
||||
}
|
||||
display.clear(); screen.render(display);
|
||||
@@ -304,7 +386,7 @@ int main(int argc, char** argv) {
|
||||
touch_gesture(x, screen.readerTouchBar()->top, x, screen.readerTouchBar()->top);
|
||||
};
|
||||
tap(2); assert(screen.flush());
|
||||
Position saved; assert(the_mesh.loadReaderBookmark(saved) && saved.verse == 1);
|
||||
Position saved; assert(the_mesh.loadReaderBookmark(saved) && saved == expected_positions[1]);
|
||||
tap(1); assert(screen.flush());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
tap(3); assert(screen.flush());
|
||||
@@ -314,11 +396,11 @@ int main(int argc, char** argv) {
|
||||
tap(4); assert(task.closed);
|
||||
task.closed = false;
|
||||
touch_gesture(120,70,120,70); assert(screen.flush());
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved.verse == 1);
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved == expected_positions[1]);
|
||||
touch_gesture(40,70,40,70); assert(screen.flush());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
touch_gesture(120,70,40,70); assert(screen.flush());
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved.verse == 1);
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved == expected_positions[1]);
|
||||
touch_gesture(40,70,120,70); assert(screen.flush());
|
||||
assert(!the_mesh.loadReaderBookmark(saved) && saved.atStart());
|
||||
touch_gesture(80,100,80,50); assert(screen.flush());
|
||||
@@ -397,13 +479,11 @@ int main(int argc, char** argv) {
|
||||
Display migrated_display(128,64);
|
||||
mesh::ui::SmallMessageText compact(migrated_display);
|
||||
const auto compact_measure = [&compact](const char* line) { return compact.getTextWidth(line); };
|
||||
const auto compact_page = readerPage(text, legacy.next, 128,
|
||||
compact.lineCount(12, migrated_display.bodyBottom()), compact_measure);
|
||||
assert(compact_page.start <= legacy.next && compact_page.next > legacy.next);
|
||||
const Position compact_line = containingLine(91, legacy.next, 128, compact_measure);
|
||||
assert(compact_line.offset <= legacy.next);
|
||||
ReaderScreen migrated(&task, &migrated_display);
|
||||
migrated.open(); migrated.render(migrated_display); assert(migrated.flush());
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved == pos(91, compact_page.start));
|
||||
assert(compact_page.parts == 1); // reader 3:16 now fits in one V4 screen
|
||||
assert(the_mesh.loadReaderBookmark(saved) && saved == compact_line);
|
||||
|
||||
// Reflow an already-open reader when panel geometry crosses the 5px/6px
|
||||
// threshold, and when a tiny panel rotates into the stacked-header layout.
|
||||
@@ -418,14 +498,11 @@ int main(int argc, char** argv) {
|
||||
const bool small = changed.useSmallMessageFont();
|
||||
DisplayDriver& new_font = small ? static_cast<DisplayDriver&>(small_font) : changed;
|
||||
auto new_measure = [&new_font](const char* line) { return new_font.getTextWidth(line); };
|
||||
const auto new_page = readerPage(text, before.offset, changed.width(),
|
||||
small ? small_font.lineCount(changed.bodyY(), changed.bodyBottom())
|
||||
: (changed.bodyBottom() - changed.bodyY()) / 10,
|
||||
new_measure);
|
||||
assert(new_page.start <= before.offset && new_page.next > before.offset);
|
||||
const Position new_line = containingLine(91, before.offset, changed.width(), new_measure);
|
||||
assert(new_line.offset <= before.offset);
|
||||
active.render(changed); assert(active.flush());
|
||||
Position after; assert(the_mesh.loadReaderBookmark(after));
|
||||
assert(after == pos(91, new_page.start));
|
||||
assert(after == new_line);
|
||||
const auto rendered = changed.body();
|
||||
ReaderScreen reopened(&task, &changed);
|
||||
reopened.open(); changed.clear(); reopened.render(changed);
|
||||
|
||||
@@ -103,7 +103,27 @@ class CompanionReaderTest(unittest.TestCase):
|
||||
f"-DUI_SMALL_MESSAGE_FONT={small_font}", "-DUI_BUTTON_READER_HINT=1",
|
||||
"-DUI_READER_TOUCH_BAR=0", "-c", str(FIXTURE / "test_reader_screen.cpp"),
|
||||
"-o", str(directory / (name + ".o"))])
|
||||
ui = frame(frames(name), "ReaderScreen::process(") + lookup + decoder_bytes
|
||||
# The screen loads a verse, then wraps it: decoding and line
|
||||
# measurement never nest. Keep the larger real call path,
|
||||
# including emitted lambda/template helpers on each path.
|
||||
screen_frames = frames(name)
|
||||
entry = frame(screen_frames, "void ReaderScreen::process(int, bool)")
|
||||
load = frame(screen_frames, "ReaderScreen::process(int, bool)::<lambda(uint16_t)>")
|
||||
wrap = frame(screen_frames, "ReaderScreen::process(int, bool)::<lambda(uint16_t, uint16_t)>")
|
||||
line_frames = [count for function, count in screen_frames.items()
|
||||
if "mesh::bible::readerLine(" in function
|
||||
and "ReaderScreen::process(" in function]
|
||||
self.assertEqual(len(line_frames), 1)
|
||||
measure_frames = [count for function, count in screen_frames.items()
|
||||
if "ReaderScreen::process(int, bool)::<lambda(const char*)>" in function
|
||||
and " [with " not in function]
|
||||
self.assertLessEqual(len(measure_frames), 1)
|
||||
# Any newly emitted helper must be placed in this call graph,
|
||||
# rather than silently disappearing from the stack estimate.
|
||||
self.assertEqual(sum("ReaderScreen::process(" in function for function in screen_frames),
|
||||
4 + len(measure_frames))
|
||||
ui = entry + max(load + lookup + decoder_bytes,
|
||||
wrap + sum(line_frames) + sum(measure_frames))
|
||||
ui_max = max(ui_max, ui)
|
||||
self.assertLessEqual(ui + caller_reserve, loop_bytes)
|
||||
self.assertEqual(packer.BLOCK_SIZE, 4096)
|
||||
@@ -273,6 +293,7 @@ namespace mesh {
|
||||
bool morseTimingActive(const Radio* radio) { return radio && radio->active; }
|
||||
namespace power { bool handleRegulatorCommand(const char*,char*,size_t) { return false; } }
|
||||
namespace leds { bool handleCommand(const char*,char*,size_t) { return false; } }
|
||||
namespace esp32 { bool handlePowerTestCommand(const char*,char*,size_t,bool) { return false; } }
|
||||
}
|
||||
@PREDICATE@
|
||||
struct MyMesh {
|
||||
|
||||
+13
-12
@@ -28,18 +28,19 @@ roles omit the corpus. `COMPANION_FEATURE_READER=0` excludes it from a Full buil
|
||||
## On-device Reader
|
||||
|
||||
Full Companions using the `ui-new` display (including the V4) also offer a
|
||||
Reader from the radio-settings page:
|
||||
Reader from the Network/radio-settings page:
|
||||
|
||||
- Long press opens at the first reference, or resumes the saved position.
|
||||
- Single click advances one screenful, then to the next verse.
|
||||
- Double click goes back one screenful; at a verse boundary it shows the
|
||||
previous verse's last screenful.
|
||||
- Single click advances one screenful; double click goes back one screenful.
|
||||
Pages continue across verse and chapter boundaries to fill the available rows.
|
||||
- Three taps jump to verse one of the next chapter; four taps jump to verse
|
||||
one of the previous chapter. Navigation stops at the first and last chapters.
|
||||
- Long press exits to the radio page. It does not invoke early-boot CLI rescue
|
||||
while opening or closing the reader.
|
||||
|
||||
The header shows the reference and part count. Single-button builds alternate
|
||||
Each verse starts with its subsection number. The header shows the first and
|
||||
last references on the screen instead of a verse-local part count. This layout
|
||||
applies to all boards with Reader support. Single-button builds alternate
|
||||
`<- 2 tap 1 tap -> long press: exit` and `<<- 4 tap 3 tap ->>` in the footer
|
||||
every three seconds, matching the message controls (which change channels).
|
||||
These are taps of the user button. Narrower displays use `hold: X` for exit
|
||||
@@ -57,14 +58,14 @@ the display.
|
||||
|
||||
Sub-160px displays use the same adaptive font selector as received messages:
|
||||
Squeezed Regular 6 on panels at least 128x64 (or 64x128 when rotated), and
|
||||
5px Picopixel on smaller panels. The reference and part counter retain their
|
||||
5px Picopixel on smaller panels. The reference range retains its
|
||||
normal header font, stacking on narrow rotated screens to avoid overlap. On
|
||||
the V4's 128x64 display, five complete 8-pixel-spaced body rows fit above the
|
||||
navigation hint, and reference 3:16 fits on one screen. Wrapping and pagination use the selected font's
|
||||
actual character widths and line height. Any panel with either dimension at
|
||||
least 160px uses its normal font, including rotated 80x160 panels. Larger display classes, and builds with
|
||||
`UI_SMALL_MESSAGE_FONT=0`, retain the regular body font. Existing bookmarks
|
||||
resume at the page containing the same text offset after a font/layout change.
|
||||
resume at the line containing the same text offset after a font/layout change.
|
||||
|
||||
The T096 and other ST7735 boards use native 160x80 coordinates and the normal
|
||||
body font (five 10-pixel-spaced rows between the header and button hint). The existing color
|
||||
@@ -163,9 +164,9 @@ the screen reader's small cursor object is described above. The temporary
|
||||
|
||||
The native Reader suite also cross-compiles the production Reader and C
|
||||
`tinf` decoder with Cortex-M4 GCC 14.2 and `-fstack-usage`. With 4 KiB blocks,
|
||||
the terminal decode frames use at most 6,264 bytes including the bounded
|
||||
the terminal decode frames use at most 6,304 bytes including the bounded
|
||||
page wrapper; the adaptive-font/button Reader screen's local decode path is
|
||||
6,400 bytes. The check reserves another 1,536 bytes for outer callers within
|
||||
6,456 bytes. The check reserves another 1,536 bytes for outer callers within
|
||||
the unchanged 8 KiB nRF52 loop stack. This is a compiler-derived bound on
|
||||
these paths, not a physical stack high-water measurement. Decoder frames are
|
||||
counted in addition to output scratch; ordinary commands never reserve it.
|
||||
@@ -178,10 +179,10 @@ ASCII-normalized copy of the pinned plaintext source, check references across
|
||||
every block boundary, exercise malformed references and corrupted block
|
||||
bounds, and verify that unrelated build profiles exclude the data.
|
||||
|
||||
Reader tests additionally traverse every part of all 879 verses in both
|
||||
directions, with and without the button hint, using regular and adaptive
|
||||
Reader tests additionally traverse full pages spanning all 879 numbered verses
|
||||
in both directions, with and without the button hint, using regular and adaptive
|
||||
5px/6px fonts at 128x64, 64x128, 160x80,
|
||||
240x135, 72x40, 40x72, 128x32, 32x128, 64x48 and 48x64. They compare rendered
|
||||
240x135, 250x122, 320x240, 72x40, 40x72, 128x32, 32x128, 64x48 and 48x64. They compare rendered
|
||||
body pixels/text separately from the hint, check active-reader resizing across
|
||||
font thresholds, and cover resume
|
||||
within a verse, book/chapter boundaries, no writes at the start, clearing stale
|
||||
|
||||
Reference in new issue
Block a user